fix: register claude-code transforms so tool calls pass through - #743
Merged
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 #546.
The transform-parity tests added with the fix assert the transform's VALUES, and every one of them passed while this was broken -- the defect was the wiring between the registry and the request. These go through the HTTP layer with a claude-cli User-Agent under default-install conditions (no passthrough opt-in, no MERIDIAN_DEFAULT_AGENT override) and assert the SDK actually receives the blocked built-ins. Verified load-bearing: removing the registry entry fails all three, alongside the contributor's registration check.
This was referenced Aug 2, 2026
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.
Lands @wayniacal's fix from #735, cherry-picked with authorship preserved, plus a server-level regression test.
The bug
ADAPTER_TRANSFORMShad no entry forclaude-code, sogetAdapterTransforms("claude-code")returned[]and the request kept thecreateRequestContextdefaults:blockedTools: [],passthrough: undefined.claudeCodeAdapter.usesPassthrough()already returnsresolvePassthrough(true)— butserver.tsresolves frompipelineCtx.*and never calls the adapter methods, so it was dead code. Same class as #546, whereopenaihad to be added to a transform'sadaptersarray for the same reason.The consequence is that the SDK subprocess gets its own Read/Write/Bash unblocked while the client also executes the returned
tool_useblocks. Side effects can happen twice — writes, commits, HTTP calls. The reporter's transcript shows two identical API posts 13 ms apart, and a second SDK session that independently edited files, committed, and raced a push to the same branch.Verified independently
The mechanism, deterministically.
claude-codeis absent from the registry;envBool("PASSTHROUGH")is the only fallback; and with no transform the SDK receives an emptydisallowedTools. Confirmed by removing the registry entry and watching the new HTTP-layer tests fail.Live, on a default install (no
MERIDIAN_PASSTHROUGH, noMERIDIAN_DEFAULT_AGENT) with the real Claude Code CLI:/healthreportsmode: internaland requests logadapter=claude-code, i.e. exactly the precondition.One correction to the PR description. It states that
MERIDIAN_PASSTHROUGH=1"does not reach this path either". It does —envBool("PASSTHROUGH")is the final fallback in the resolution chain, so setting it does turn passthrough on forclaude-code. That narrows the blast radius to default installs rather than everyone, and explains why this went unnoticed here: this machine sets bothMERIDIAN_PASSTHROUGH=1andMERIDIAN_DEFAULT_AGENT=pi, so it was insulated twice over. The fix is still correct — the default should matchclaudeCodeAdapter.usesPassthrough()rather than depending on an env var.On reproducing the doubling itself: I did not see two writes in a single live run — the model used the bridged tool that time. The doubling is possible rather than guaranteed, because with built-ins unblocked the SDK may act host-side in addition to delegating; which path the model picks varies per run. The unblocked-built-ins state is deterministic, and that is what the tests pin.
Review of the change
The transform mirrors
claudeCodeAdaptervalue-for-value, and the PascalCasecoreToolNamesreasoning is right: reusingopenCodeTransformsverbatim would hand the deferral logic OpenCode's lowercase names and defer exactly the tools it means to load eagerly.Registering under
adapter.name("claude-code", not"claudecode") is the subtle part, and the contributor called it out in a comment — worth keeping, since the file isclaudecode.tsand the mismatch is the kind of thing that gets "tidied" into a bug later.Duplicating
extractFileChangesFromToolUserather than importing it from the adapter matches howtransforms/opencode.tsalready works, so this follows the existing convention instead of inventing one. The parity tests hold the copies in sync.Added on top
A server-level test (
proxy-tool-blocking.test.ts). The contributor's parity tests assert the transform's values, and all of them would have passed while this was broken — the defect was the wiring between the registry and the request, which only an HTTP-layer assertion can see. The new cases drive aclaude-cliUser-Agent under default-install conditions and assert the SDK actually receives blocked built-ins, including one named for the exact broken state (disallowedToolsempty).Verified load-bearing: removing the registry entry fails all three, alongside the contributor's registration check.
Testing
npm test: 2303 pass, 0 failures across all invocations.npx tsc --noEmitclean.Found while verifying, filed separately
For a
claude-codeclient, the model is told the proxy host's working directory rather than the client's, so it can compose absolute paths that write to the wrong machine's tree.extractClientWorkingDirectoryis called unconditionally inserver.tsand does not branch on passthrough, so this is pre-existing and not caused by this change — it is simply easier to notice once tool execution moves client-side. Filed on its own.