Skip to content

Decloak Claude tool names on every response path#722

Open
TechNickAI wants to merge 1 commit intodecolua:masterfrom
TechNickAI:submit/decloak-cloaked-tool-names
Open

Decloak Claude tool names on every response path#722
TechNickAI wants to merge 1 commit intodecolua:masterfrom
TechNickAI:submit/decloak-cloaked-tool-names

Conversation

@TechNickAI
Copy link
Copy Markdown

cloakClaudeTools() renames client tool names with an _ide suffix on the request path (anti-ban against the upstream provider), and the response path is supposed to strip the suffix before the client sees it. That symmetry was broken on several code paths, so Claude Code / OpenClaw users kept hitting things like:

I can't use the tool exec_ide here because it isn't available. I need to stop retrying it and answer without that tool.

The model's own tool registry has no read_ide / exec_ide / web_search_ide, so it refuses to invoke them — the session effectively loses tool access.

What was leaking

Four distinct paths, all rooted in the same asymmetry:

  1. Streaming passthrough, flush buffer — the SSE transform keeps an incomplete-line buffer and only decloaks lines that survive a \n split. A content_block_start that arrived without a trailing newline stayed in the buffer until writer.close(), and the flush handler emitted it verbatim.
  2. Non-streaming /v1/messages — full JSON bodies with content[].tool_use.name were never walked.
  3. Translate mode (OpenAI client → Claude provider) — decloak ran at emit() time on post-translation shapes. The walker only knows Claude's tool_use shape, so it never matched OpenAI's function.name and the cloaked name slipped through.
  4. CRLF line endings — after buffer.split("\n"), each line kept a trailing \r. trimStart() on the data: payload left the \r in place, JSON.parse threw, and the catch path returned the original cloaked line.

Approach

One choke point, input-side. All decloaking happens on raw Claude SSE lines after buffer.split("\n") and on the leftover buffer at flush time, before any translation or emission. The walker only has to understand Claude's shape, regardless of what the client ultimately receives.

  • decloakSSEText()decloakSSELine() — operates on one complete line at a time.
  • Gate simplified to toolNameMap?.size > 0. Dropped the sourceFormat === CLAUDE check, since that's what was letting translate mode through.
  • emit() is now a dumb encoder; decloak is removed from the output side entirely.
  • .trimStart().trim() on the payload so CRLF framing parses.
  • Non-streaming handler already calls decloakToolNames() on the parsed body; that path is unchanged but now covered by a regression test.

Tests

Seven regressions in tests/unit/claude-cloaking.test.js, each pinning a distinct failure mode:

  • content_block_start stranded in the flush buffer (the original symptom).
  • Translate mode: Claude provider → OpenAI client, full SSE sequence.
  • Chunk boundary split mid-cloaked-name across TCP reads.
  • Translate-mode flush buffer with no trailing newline.
  • CRLF line endings (the trimStart edge case).
  • Empty toolNameMap — passes through unchanged (gate behavior pinned).
  • Non-streaming content[] with multiple tool_use blocks.

Closes the "exec_ide" bug class where cloaked tool names (read_ide, exec_ide, web_search_ide, etc.) leaked to clients and caused models to refuse invoking their own tools:

  "I can't use the tool 'exec_ide' here because it isn't available.
   I need to stop retrying it and answer without that tool."

Scattered decloak helpers + missed egress paths replaced with a single-pipeline guarantee: raw Claude SSE lines are decloaked on the INPUT side of the transform, before parsing or translation, so every downstream stage (translator, emitter, passthrough, flush) sees real tool names regardless of client format.

- decloakToolNames walker (claudeCloaking.js) — shape-agnostic recursive rename
- decloakSSELine applied in stream.js transform + both flush branches
- Non-streaming handler decloaks once, AFTER raw-response log, BEFORE translation
- Handles CRLF line endings (trim, not trimStart)
- 7 regression tests: flush buffer, non-streaming, translate mode, chunk-boundary splits, translate flush, empty-map gate, CRLF
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.

1 participant