Skip to content

Instantly share code, notes, and snippets.

@bkataru
Created February 18, 2026 17:15
Show Gist options
  • Select an option

  • Save bkataru/27caa1aeb746efb06f83127cfcc586b0 to your computer and use it in GitHub Desktop.

Select an option

Save bkataru/27caa1aeb746efb06f83127cfcc586b0 to your computer and use it in GitHub Desktop.
opencode titlecase crash fix: TypeError when task() called without subagent_type in TypeScript project directories
# opencode `titlecase` Crash Fix — Binary Patch
## Problem
When using [opencode](https://github.com/anomalyco/opencode) (v1.2.6) with the [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) plugin in **TypeScript project directories**, the process crashes with:
```
TypeError: undefined is not an object (evaluating 'str3.replace')
at titlecase (src/util/locale.ts:3:12)
at task (src/cli/cmd/run.ts:170:24)
at loop (src/cli/cmd/run.ts:472:15)
```
### Root Cause
In `src/cli/cmd/run.ts`, line 170:
```typescript
Locale.titlecase(info.input.subagent_type)
```
This is called when the model invokes the `task()` tool. When the model omits `subagent_type` (which is optional in the schema), `info.input.subagent_type` is `undefined`. The `titlecase` function in `src/util/locale.ts` has no null guard:
```typescript
// src/util/locale.ts
function titlecase(str) {
return str.replace(/\b\w/g, (c) => c.toUpperCase());
// ^ crashes if str is undefined
}
```
### Why TypeScript Projects?
The oh-my-opencode `category-skill-reminder` hook fires after 3+ tool calls and injects a fake `task()` code block into the LLM output. In TypeScript projects the LLM (Qwen 3.5, GLM-4.7, etc.) tends to call the `task` tool more frequently, and sometimes without the `subagent_type` field — triggering the crash.
Even with `"disabled_hooks": ["category-skill-reminder"]` in oh-my-opencode config, the LLMs themselves generate `task()` calls without `subagent_type`, reproducing the crash.
---
## Upstream Fix
This was reported as issue #13933 on the opencode repo and a fix was proposed in PR #13955 — adding a null guard to `titlecase()`.
The correct fix in source is:
```typescript
// src/util/locale.ts
function titlecase(str: string | undefined): string {
if (!str) return "";
return str.replace(/\b\w/g, (c) => c.toUpperCase());
}
```
Or alternatively, the null guard at the call site in `run.ts`:
```typescript
Locale.titlecase(info.input.subagent_type ?? "")
```
---
## Binary Patch (Workaround for v1.2.6)
Since opencode is distributed as a compiled bun binary and v1.2.6 does not include the fix, the following in-place binary patch was applied to add a null guard without changing the file size.
### Patch Script
```python
#!/usr/bin/env python3
"""
Same-size binary patch for opencode v1.2.6
Adds null guard to the embedded titlecase() function.
Tested on: /root/.opencode/bin/opencode (153,940,481 bytes)
"""
import sys
binary_path = '/root/.opencode/bin/opencode'
# Original function (91 bytes, including trailing newline)
old = (
b'function titlecase(str3) {\n'
b' return str3.replace(/\\b\\w/g, (c2) => c2.toUpperCase());\n'
b' }\n'
)
# Patched function (must be exactly same length = 91 bytes)
# Returns "" for falsy input instead of crashing
# Trailing spaces inside function body act as padding to maintain byte count
new = (
b'function titlecase(str3){return str3?str3.replace(/\\b\\w/g,c2=>c2.toUpperCase()):""; }\n'
)
print(f"Old pattern length: {len(old)} bytes")
print(f"New pattern length: {len(new)} bytes")
if len(old) != len(new):
print(f"ERROR: Length mismatch! Cannot do same-size patch.")
sys.exit(1)
# Backup
import shutil
shutil.copy2(binary_path, binary_path + '.bak')
print(f"Backup saved to {binary_path}.bak")
with open(binary_path, 'rb') as f:
data = f.read()
count = data.count(old)
print(f"Found {count} occurrence(s) of the titlecase function pattern")
if count == 0:
print("ERROR: Pattern not found in binary!")
sys.exit(1)
if count > 1:
print(f"WARNING: Found {count} occurrences - will patch all of them")
patched = data.replace(old, new)
with open(binary_path, 'wb') as f:
f.write(patched)
print(f"Patch applied successfully! Patched {count} occurrence(s).")
print(f"File size: {len(patched)} bytes (unchanged: {len(data) == len(patched)})")
```
### Before vs After
**Before (crashes on undefined):**
```javascript
function titlecase(str3) {
return str3.replace(/\b\w/g, (c2) => c2.toUpperCase());
}
```
**After (null-safe, same 91 bytes):**
```javascript
function titlecase(str3){return str3?str3.replace(/\b\w/g,c2=>c2.toUpperCase()):""; }
```
### Applying the Patch
```bash
# Backup first (the script does this automatically)
cp /root/.opencode/bin/opencode /root/.opencode/bin/opencode.bak
# Run patch
python3 patch_opencode.py
# Verify
# Old pattern length: 91 bytes
# New pattern length: 91 bytes
# Backup saved to /root/.opencode/bin/opencode.bak
# Found 1 occurrence(s) of the titlecase function pattern
# Patch applied successfully! Patched 1 occurrence(s).
# File size: 153940481 bytes (unchanged: True)
```
---
## Verification
After patching, running opencode in a TypeScript project with multiple tool calls and sub-agent delegation (via oh-my-opencode's Sisyphus orchestration) completed successfully without any `TypeError`.
**Test command:**
```bash
cd /path/to/ts-project && opencode run "list all files and read package.json and tsconfig.json"
```
**Result:** Agent ran 10+ tool calls, spawned Sisyphus-Junior sub-agent, read multiple config files — exit code 0, no crash.
---
## Environment
| Component | Version |
|-----------|---------|
| opencode | v1.2.6 (anomalyco/opencode fork) |
| oh-my-opencode | v3.7.3 |
| Models used | Qwen 3.5 (397B), GLM-4.7, MiniMax M2.1 via NVIDIA NIM |
| OS | Linux (Ubuntu 24.04) |
| Platform | bun-compiled binary |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment