Summary
A claude-code client is told the proxy host's working directory, not its own. The model then composes absolute paths from it, so a tool call can write to the wrong machine's tree.
Found while verifying #743. Pre-existing and unrelated to that change — see "Not caused by #743" below.
Reproduced
Real Claude Code CLI pointed at Meridian, client running in a scratch directory:
$ cd /private/tmp/.../scratchpad/cc-cwd
$ ANTHROPIC_BASE_URL=http://127.0.0.1:PORT claude -p \
"Do not use any tools. In one line: what is your current working directory, exactly as your context states it?"
My current working directory, exactly as my context states it, is `/Users/rynfar/repos/meridian`.
/Users/rynfar/repos/meridian is where the proxy runs. The client was in .../scratchpad/cc-cwd.
The practical consequence, from an earlier run in the same setup:
TOOL_USE: Bash {"command": "echo BANANA >> /Users/rynfar/repos/meridian/log.txt"}
TOOL_RESULT is_error=False
The model was asked to append to log.txt in its own directory. It built an absolute path from the working directory it had been told, and the line landed in the proxy's repo instead. The tool reported success, and the file the user asked about was never touched.
Mechanism
claudeCodeAdapter deliberately returns undefined from extractWorkingDirectory (so the SDK subprocess chdirs somewhere valid on the server) and is supposed to surface the client's real path separately, via extractClientWorkingDirectory → buildCwdNote → a <meridian-note> in the system prompt.
That note is absent here. extractClaudeCodeClientCwd (src/proxy/adapters/claudecode.ts:38) parses the system prompt for:
systemText.match(/Primary working directory:\s*([^\n<]+)/i)
If the client's system prompt doesn't carry that exact phrasing, the parse returns undefined, buildCwdNote short-circuits to "", and the model is left with the SDK's own env block — the proxy path.
Worth confirming which it is before fixing:
- the CLI version under test doesn't emit that line at all, or
- it emits it somewhere the extractor doesn't look (e.g. a later
system block, or different wording).
Either way a silent undefined is the wrong failure mode for something this consequential.
Not caused by #743
server.ts:853 computes clientWorkingDirectory unconditionally — it does not branch on passthrough:
const clientWorkingDirectory = adapter.extractClientWorkingDirectory?.(body) || cwdResolution.claimedWorkingDirectory
So the cwd hint behaves identically before and after that change. During verification one run wrote to the client directory and another to the proxy directory; that difference is the model choosing a relative vs absolute path, not a behaviour change — both runs were given the same (wrong) directory.
It is, however, easier to hit now: with tool execution correctly moved client-side, a bad absolute path is executed by the client, on the user's real machine.
Suggested direction
- Establish which of the two cases above applies, from a captured request body rather than inference.
- Widen or fix the extractor accordingly.
- Consider making the miss observable — a one-line debug log when
extractClientWorkingDirectory returns undefined for an adapter that declares one. Today the failure is silent and presents as the model confidently using the wrong path.
Testing
src/__tests__/ has cwd-note coverage (query-cwd-note.test.ts) for the note itself. What is missing is a case asserting the extractor handles a real captured Claude Code system prompt — the same class of gap as #707, where a test asserted against a hand-made shape rather than the real one.
Summary
A
claude-codeclient is told the proxy host's working directory, not its own. The model then composes absolute paths from it, so a tool call can write to the wrong machine's tree.Found while verifying #743. Pre-existing and unrelated to that change — see "Not caused by #743" below.
Reproduced
Real Claude Code CLI pointed at Meridian, client running in a scratch directory:
/Users/rynfar/repos/meridianis where the proxy runs. The client was in.../scratchpad/cc-cwd.The practical consequence, from an earlier run in the same setup:
The model was asked to append to
log.txtin its own directory. It built an absolute path from the working directory it had been told, and the line landed in the proxy's repo instead. The tool reported success, and the file the user asked about was never touched.Mechanism
claudeCodeAdapterdeliberately returnsundefinedfromextractWorkingDirectory(so the SDK subprocess chdirs somewhere valid on the server) and is supposed to surface the client's real path separately, viaextractClientWorkingDirectory→buildCwdNote→ a<meridian-note>in the system prompt.That note is absent here.
extractClaudeCodeClientCwd(src/proxy/adapters/claudecode.ts:38) parses the system prompt for:If the client's system prompt doesn't carry that exact phrasing, the parse returns
undefined,buildCwdNoteshort-circuits to"", and the model is left with the SDK's own env block — the proxy path.Worth confirming which it is before fixing:
systemblock, or different wording).Either way a silent
undefinedis the wrong failure mode for something this consequential.Not caused by #743
server.ts:853computesclientWorkingDirectoryunconditionally — it does not branch on passthrough:So the cwd hint behaves identically before and after that change. During verification one run wrote to the client directory and another to the proxy directory; that difference is the model choosing a relative vs absolute path, not a behaviour change — both runs were given the same (wrong) directory.
It is, however, easier to hit now: with tool execution correctly moved client-side, a bad absolute path is executed by the client, on the user's real machine.
Suggested direction
extractClientWorkingDirectoryreturnsundefinedfor an adapter that declares one. Today the failure is silent and presents as the model confidently using the wrong path.Testing
src/__tests__/has cwd-note coverage (query-cwd-note.test.ts) for the note itself. What is missing is a case asserting the extractor handles a real captured Claude Code system prompt — the same class of gap as #707, where a test asserted against a hand-made shape rather than the real one.