Skip to content

fix: register claude-code transforms so tool calls pass through - #743

Merged
rynfar merged 2 commits into
mainfrom
fix/claudecode-transforms-merged
Aug 2, 2026
Merged

fix: register claude-code transforms so tool calls pass through#743
rynfar merged 2 commits into
mainfrom
fix/claudecode-transforms-merged

Conversation

@rynfar

@rynfar rynfar commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Lands @wayniacal's fix from #735, cherry-picked with authorship preserved, plus a server-level regression test.

The bug

ADAPTER_TRANSFORMS had no entry for claude-code, so getAdapterTransforms("claude-code") returned [] and the request kept the createRequestContext defaults: blockedTools: [], passthrough: undefined.

claudeCodeAdapter.usesPassthrough() already returns resolvePassthrough(true) — but server.ts resolves from pipelineCtx.* and never calls the adapter methods, so it was dead code. Same class as #546, where openai had to be added to a transform's adapters array 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_use blocks. 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-code is absent from the registry; envBool("PASSTHROUGH") is the only fallback; and with no transform the SDK receives an empty disallowedTools. Confirmed by removing the registry entry and watching the new HTTP-layer tests fail.

Live, on a default install (no MERIDIAN_PASSTHROUGH, no MERIDIAN_DEFAULT_AGENT) with the real Claude Code CLI: /health reports mode: internal and requests log adapter=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 for claude-code. That narrows the blast radius to default installs rather than everyone, and explains why this went unnoticed here: this machine sets both MERIDIAN_PASSTHROUGH=1 and MERIDIAN_DEFAULT_AGENT=pi, so it was insulated twice over. The fix is still correct — the default should match claudeCodeAdapter.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 claudeCodeAdapter value-for-value, and the PascalCase coreToolNames reasoning is right: reusing openCodeTransforms verbatim 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 is claudecode.ts and the mismatch is the kind of thing that gets "tidied" into a bug later.

Duplicating extractFileChangesFromToolUse rather than importing it from the adapter matches how transforms/opencode.ts already 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 a claude-cli User-Agent under default-install conditions and assert the SDK actually receives blocked built-ins, including one named for the exact broken state (disallowedTools empty).

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 --noEmit clean.

Found while verifying, filed separately

For a claude-code client, 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. extractClientWorkingDirectory is called unconditionally in server.ts and 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.

wayniacal and others added 2 commits August 2, 2026 16:55
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.
@rynfar
rynfar merged commit 94ba52f into main Aug 2, 2026
3 checks passed
@rynfar
rynfar deleted the fix/claudecode-transforms-merged branch August 2, 2026 23:08
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.

2 participants