fix: register claude-code transforms so tool calls pass through - #735
fix: register claude-code transforms so tool calls pass through#735wayniacal wants to merge 1 commit into
Conversation
server.ts reads pipelineCtx.*, never the adapter methods, and ADAPTER_TRANSFORMS had no entry for adapter "claude-code". Claude Code clients therefore got the createRequestContext defaults, blockedTools empty and passthrough undefined, so the SDK subprocess ran the client's task internally on the proxy host with its own built-in Read/Write/Bash while the CLI executed the same tool_use blocks locally. Every side effect happened twice, milliseconds apart. Observed on a scheduled agent: one run produced three near identical API posts and two racing git pushes to the same branch, from three agents (the CLI, the SDK mirror of its turn, and a second SDK session spawned by the CLI's tools=0 structured-output request). Mirrors claudeCodeAdapter, held in sync by the parity tests. Same class of bug as rynfar#546.
|
I can review this tonight or tomorrow thanks for the submission |
|
Merged as 94ba52f (#743), cherry-picked so the commit keeps your authorship. Thank you — this is a serious bug and the write-up made it fast to confirm. The diagnosis is exactly right, including the part that's easy to miss: Your PascalCase reasoning is also right and worth stating for the record: reusing One correctionThe PR says const passthrough = adapter.instancePassthrough !== undefined
? adapter.instancePassthrough
: pipelineCtx.passthrough !== undefined
? pipelineCtx.passthrough
: envBool("PASSTHROUGH")That narrows the blast radius to default installs rather than everyone, and it explains why this went unnoticed on my side: this machine sets both On reproducing the doublingI confirmed the precondition on a default install: I did not see two writes in a single live run — that time the model used the bridged tool. The doubling is possible rather than guaranteed: with built-ins unblocked, the SDK may act host-side in addition to delegating, and which path it takes varies per run. That makes your transcript more valuable, not less — you caught the case where it took both, and the unblocked-built-ins state that allows it is deterministic, which is what the tests now pin. Added on topA server-level test in Your "is registered under the adapter's own name" test was the right instinct, by the way — asserting the entry exists rather than only that its values are correct is what catches this class. Filed separatelyVerifying this turned up something adjacent: a Closing this PR since the commit is on |
ADAPTER_TRANSFORMShas no entry forclaude-code, sogetAdapterTransforms("claude-code")returns[]and a Claude Code client keeps thecreateRequestContextdefaults:blockedTools: [],passthrough: undefined. The SDK subprocess then runs the client's task itself, on the proxy host, with its own built-in Read/Write/Bash, while the CLI executes the sametool_useblocks locally. Every side effect happens twice.claudeCodeAdapter.usesPassthrough()already returnsresolvePassthrough(true), but nothing calls it.server.tsreadspipelineCtx.*.MERIDIAN_PASSTHROUGH=1does not reach this path either.Same class as #546, where
openaihad to be added to the transform'sadaptersarray for the same reason.How it surfaced
A scheduled agent on 1.57.0 that edits a git repo and posts a summary through an HTTP API. One run produced three posts:
Proxy log for that run:
652b45ffis the CLI'stools=0structured-output request (conversation title). With no transform, that one also became a full agent with built-in tools, so it did the entire task independently: its own file edits, its own commits, a racing push to the same branch. Its transcript records it reading the other session's concurrent edits and concluding a linter had reformatted the file, then deleting 21 lines to "dedupe". It emitted the title at the end, after the side effects.Anything a Claude Code client does through Meridian is currently doubled this way. Writes, commits, HTTP calls.
Fix
transforms/claudecode.tsmirrorsclaudeCodeAdapter, registered underadapter.name("claude-code", not"claudecode"). Core tool names stay PascalCase, since Claude Code's toolkit isRead/Write/Edit/Bash/Glob/Grep; reusingopenCodeTransformsverbatim would hand the deferral logic OpenCode's lowercase names and defer the tools it means to load eagerly.Tests
Parity suite extended for
claude-code, including a check that the registry entry exists at all, since that is the failure here rather than a wrong value. Full suite passes (2106 + the isolated files, 0 failures).Verified against the live proxy with an agent asked to append one line to a file: