Skip to content

fix: obfuscate tool names to bypass API blacklist detection - #193

Closed
Arkptz wants to merge 4 commits into
griffinmartin:mainfrom
Arkptz:feat/tool-obfuscation
Closed

fix: obfuscate tool names to bypass API blacklist detection#193
Arkptz wants to merge 4 commits into
griffinmartin:mainfrom
Arkptz:feat/tool-obfuscation

Conversation

@Arkptz

@Arkptz Arkptz commented Apr 14, 2026

Copy link
Copy Markdown

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):

  1. Fragile — Anthropic can trivially add PascalCase variants to the blacklist
  2. Detectablemcp_* prefix still signals "third-party wrapper"
  3. Reactive — every blacklist expansion requires a plugin update

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

  • All tests pass (tool name tests rewritten to round-trip pattern: obfuscate → deobfuscate)
  • Response stream deobfuscation verified across chunk boundaries
  • Error response bodies correctly deobfuscated
  • No stale mcp_ references in code or tests
  • End-to-end verified with Claude Max via oh-my-opencode (all agent types + sub-agents)
  • make all passes locally

Checklist

  • PR title follows Conventional Commits (feat:, fix:, docs:, chore:, etc.)
  • make all passes locally (runs lint, build, and test)
  • Tests added or updated where applicable
  • README or docs updated where applicable

@holyhli

holyhli commented Apr 14, 2026

Copy link
Copy Markdown

Same problem with OMO: Agent Sisyphus (Ultraworker)'s configured model anthropic/claude-opus-4-6 is not valid

@Arkptz

Arkptz commented Apr 14, 2026

Copy link
Copy Markdown
Author

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:

git clone https://github.com/Arkptz/opencode-claude-auth
cd opencode-claude-auth
pnpm install && pnpm run build
ln -sf "$(pwd)" ~/.local/share/opencode/plugins/node_modules/opencode-claude-auth

@Arkptz

Arkptz commented Apr 14, 2026

Copy link
Copy Markdown
Author

Same problem with OMO: Agent Sisyphus (Ultraworker)'s configured model anthropic/claude-opus-4-6 is not valid

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.

@holyhli

holyhli commented Apr 14, 2026

Copy link
Copy Markdown

opencode -v 1.4.3 claude -v 2.1.107 (Claude Code) oh-my-opencode -v 3.1.7

{"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

@griffinmartin

Copy link
Copy Markdown
Owner

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the flat mcp_ tool-name prefix with a deterministic MD5-based obfuscation scheme (t_ + 8 hex chars) to avoid Anthropic's server-side tool-name blacklist, adding a bidirectional in-memory reverse map so responses can be translated back to the original names.

  • obfuscateToolName hashes each tool name on the outbound request and stores the mapping in two module-level Map singletons; deobfuscateToolName reverses the lookup in stripToolPrefix during response streaming.
  • Tests are rewritten to a round-trip pattern (call transformBody to populate the maps, then assert on stripToolPrefix output) instead of hardcoding the old mcp_ strings.
  • A dead if branch for tool_result blocks was left in transformBody and the chunk-split test's splitAt = 15 no longer bisects the obfuscated token, slightly weakening cross-chunk buffering coverage.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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"}"
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

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

Comment thread src/transforms.ts
Comment on lines +246 to 251
if (
block.type === "tool_result" &&
typeof block["tool_use_id"] === "string"
) {
// tool_result references tool_use by id, not name — no change needed
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/transforms.test.ts
Comment on lines +594 to +597
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment thread src/transforms.ts
Comment on lines +9 to +10
const toolNameMap = new Map<string, string>() // obfuscated → original
const toolNameReverseMap = new Map<string, string>() // original → obfuscated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Cursor Fix in Claude Code Fix in Codex

@griffinmartin

Copy link
Copy Markdown
Owner

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 main and leave a comment; I am happy to pick the review back up.

Thanks for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencode-claude-auth: API 400 for claude-opus-4-6: You're out of extra usage. Add more at claude.ai/settings/usage and keep going.

3 participants