fix: obfuscate tool names to bypass API blacklist detection - #193
Conversation
|
Same problem with OMO: Agent Sisyphus (Ultraworker)'s configured model anthropic/claude-opus-4-6 is not valid |
You can try my fork that has both fixes (cch + tool obfuscation) merged together - symlink it into the plugin dir: |
Also - this is most likely an OMO/OpenCode config issue, not the plugin itself. If the symlink doesn't help, share your opencode --version, claude --version, OMO version, and the agent section from your opencode.json. Debug log would help too: export CLAUDE_AUTH_DEBUG=1, restart, reproduce, grab ~/.local/share/opencode/claude-auth-debug.log. |
|
{"ts":"2026-04-14T13:53:15.912Z","event":"test_event"} The message appears when I just open opencode and it appears, and I can't select the anthropic models from my subscription |
Greptile SummaryThis PR replaces the flat
Confidence Score: 4/5Safe to merge for its intended purpose; the obfuscation logic is correct for the normal request/response lifecycle, with a few minor rough edges worth cleaning up. The core transformation is deterministic and the round-trip works correctly in production. The main concerns are non-critical: a dead if-block in the tool_result branch, a deobfuscation fallback that silently returns the hash on a miss, and a chunk-split test that no longer bisects the obfuscated token. Files Needing Attention: src/transforms.ts warrants a second look for the dead tool_result branch and the silent miss fallback in deobfuscateToolName; src/transforms.test.ts for the splitAt value in the buffering test.
|
| Filename | Overview |
|---|---|
| src/transforms.ts | Replaces mcp_ prefix with MD5-based obfuscation; contains a dead tool_result if-block and a deobfuscation fallback that silently returns the hash on a cache miss |
| src/transforms.test.ts | Tests updated to round-trip pattern (transformBody → stripToolPrefix); the chunk-split test uses splitAt=15 which no longer splits inside the obfuscated token, weakening cross-chunk coverage |
| README.md | One-line update to the README description reflecting the new MD5-based obfuscation approach |
Sequence Diagram
sequenceDiagram
participant OC as OpenCode
participant TB as transformBody
participant Maps as toolNameMap / toolNameReverseMap
participant API as Anthropic API
participant SP as stripToolPrefix
OC->>TB: "POST /v1/messages (tools: [{name:"search"}])"
TB->>Maps: obfuscateToolName("search")
Maps-->>TB: "t_a1b2c3d4" (stored in both maps)
TB->>API: "tools: [{name:"t_a1b2c3d4"}]"
API-->>SP: "stream: {"name":"t_a1b2c3d4","type":"tool_use"}"
SP->>Maps: deobfuscateToolName("t_a1b2c3d4")
Maps-->>SP: "search"
SP-->>OC: "stream: {"name":"search","type":"tool_use"}"
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
src/transforms.ts:246-251
**Dead `tool_result` if-branch adds noise without effect**
The branch checks `block.type === "tool_result"` then does nothing — execution falls straight through to `return block` in both cases. The comment is useful but wrapping it in an inert `if` block suggests an action that never happens, which makes the control flow misleading for future readers. The block should either be removed or kept as a plain comment outside any conditional.
### Issue 2 of 3
src/transforms.test.ts:594-597
**`splitAt = 15` does not split inside the tool name**
`data: {"name":"` is exactly 15 characters, so `chunk1` ends with the opening quote and `chunk2` starts with the full `t_XXXXXXXX` token — the split lands just *before* the name, not inside it. The original test deliberately put `mc` in chunk1 and `p_search"}\n\n…` in chunk2, proving the buffer handles a name torn mid-token. Using `splitAt = 17` or `19` (somewhere inside `t_XXXXXXXX`) would restore that property and make the comment accurate.
### Issue 3 of 3
src/transforms.ts:9-10
**Module-level maps are never cleared and silently fail on deobfuscation miss**
`toolNameMap` and `toolNameReverseMap` live for the lifetime of the process and are never reset. In the current single-process model that is fine, but `deobfuscateToolName` returns the raw hash when a key is absent (the `?? obf` fallback). If that ever triggers — e.g., during a hot-reload in development, or if a response somehow references a tool that was registered in a prior process lifetime — OpenCode receives obfuscated names like `t_a1b2c3d4` instead of original ones and silently misroutes tool calls. A warning log or a thrown error on a miss would make this failure mode visible.
Reviews (1): Last reviewed commit: "style: remove unused log import and fix ..." | Re-trigger Greptile
| if ( | ||
| block.type === "tool_result" && | ||
| typeof block["tool_use_id"] === "string" | ||
| ) { | ||
| // tool_result references tool_use by id, not name — no change needed | ||
| } |
There was a problem hiding this comment.
Dead
tool_result if-branch adds noise without effect
The branch checks block.type === "tool_result" then does nothing — execution falls straight through to return block in both cases. The comment is useful but wrapping it in an inert if block suggests an action that never happens, which makes the control flow misleading for future readers. The block should either be removed or kept as a plain comment outside any conditional.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.ts
Line: 246-251
Comment:
**Dead `tool_result` if-branch adds noise without effect**
The branch checks `block.type === "tool_result"` then does nothing — execution falls straight through to `return block` in both cases. The comment is useful but wrapping it in an inert `if` block suggests an action that never happens, which makes the control flow misleading for future readers. The block should either be removed or kept as a plain comment outside any conditional.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const fullData = `data: {"name":"${obf}"}\n\ndata: {"type":"done"}\n\n` | ||
| const splitAt = 15 // splits inside the tool name | ||
| const chunk1 = fullData.slice(0, splitAt) | ||
| const chunk2 = fullData.slice(splitAt) |
There was a problem hiding this comment.
splitAt = 15 does not split inside the tool name
data: {"name":" is exactly 15 characters, so chunk1 ends with the opening quote and chunk2 starts with the full t_XXXXXXXX token — the split lands just before the name, not inside it. The original test deliberately put mc in chunk1 and p_search"}\n\n… in chunk2, proving the buffer handles a name torn mid-token. Using splitAt = 17 or 19 (somewhere inside t_XXXXXXXX) would restore that property and make the comment accurate.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.test.ts
Line: 594-597
Comment:
**`splitAt = 15` does not split inside the tool name**
`data: {"name":"` is exactly 15 characters, so `chunk1` ends with the opening quote and `chunk2` starts with the full `t_XXXXXXXX` token — the split lands just *before* the name, not inside it. The original test deliberately put `mc` in chunk1 and `p_search"}\n\n…` in chunk2, proving the buffer handles a name torn mid-token. Using `splitAt = 17` or `19` (somewhere inside `t_XXXXXXXX`) would restore that property and make the comment accurate.
How can I resolve this? If you propose a fix, please make it concise.| const toolNameMap = new Map<string, string>() // obfuscated → original | ||
| const toolNameReverseMap = new Map<string, string>() // original → obfuscated |
There was a problem hiding this comment.
Module-level maps are never cleared and silently fail on deobfuscation miss
toolNameMap and toolNameReverseMap live for the lifetime of the process and are never reset. In the current single-process model that is fine, but deobfuscateToolName returns the raw hash when a key is absent (the ?? obf fallback). If that ever triggers — e.g., during a hot-reload in development, or if a response somehow references a tool that was registered in a prior process lifetime — OpenCode receives obfuscated names like t_a1b2c3d4 instead of original ones and silently misroutes tool calls. A warning log or a thrown error on a miss would make this failure mode visible.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/transforms.ts
Line: 9-10
Comment:
**Module-level maps are never cleared and silently fail on deobfuscation miss**
`toolNameMap` and `toolNameReverseMap` live for the lifetime of the process and are never reset. In the current single-process model that is fine, but `deobfuscateToolName` returns the raw hash when a key is absent (the `?? obf` fallback). If that ever triggers — e.g., during a hot-reload in development, or if a response somehow references a tool that was registered in a prior process lifetime — OpenCode receives obfuscated names like `t_a1b2c3d4` instead of original ones and silently misroutes tool calls. A warning log or a thrown error on a miss would make this failure mode visible.
How can I resolve this? If you propose a fix, please make it concise.|
Closing due to inactivity — there has been no activity from the author on this PR in over a month. This is not a judgement on the change itself. If you are still interested in landing it, please reopen or open a fresh PR rebased on Thanks for the contribution. |
Summary
Replace
mcp_prefix with MD5 hash-based obfuscation (t_+ 8 hex chars) for all tool names, fixing "You're out of extra usage" errors caused by server-side tool name blacklisting.Through isolated curl testing of all 133 tools, we identified 3 blacklisted names:
todowrite,background_output,background_cancel. The check is purely name-based — same tool with a different name and identical description/schema returns 200 OK.Why this over PR #191 (PascalCase):
mcp_*prefix still signals "third-party wrapper"Our approach hashes ALL tool names via MD5 (
todowrite → t_a1b2c3d4) with a stateful reverse map for response deobfuscation. Blacklist-proof, one-time fix, ~20 lines of code.Verified working with Claude Max subscription via oh-my-opencode plugin — all agents including sub-agents function correctly.
Related issue
Fixes #188 #190
Testing
mcp_references in code or testsmake allpasses locallyChecklist
feat:,fix:,docs:,chore:, etc.)make allpasses locally (runs lint, build, and test)