Decloak Claude tool names on every response path#722
Open
TechNickAI wants to merge 1 commit intodecolua:masterfrom
Open
Decloak Claude tool names on every response path#722TechNickAI wants to merge 1 commit intodecolua:masterfrom
TechNickAI wants to merge 1 commit intodecolua:masterfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cloakClaudeTools()renames client tool names with an_idesuffix 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: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:
\nsplit. Acontent_block_startthat arrived without a trailing newline stayed in the buffer untilwriter.close(), and the flush handler emitted it verbatim./v1/messages— full JSON bodies withcontent[].tool_use.namewere never walked.emit()time on post-translation shapes. The walker only knows Claude'stool_useshape, so it never matched OpenAI'sfunction.nameand the cloaked name slipped through.buffer.split("\n"), each line kept a trailing\r.trimStart()on thedata:payload left the\rin place,JSON.parsethrew, 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.toolNameMap?.size > 0. Dropped thesourceFormat === CLAUDEcheck, 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.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_startstranded in the flush buffer (the original symptom).trimStartedge case).toolNameMap— passes through unchanged (gate behavior pinned).content[]with multipletool_useblocks.