Skip to content

claude-code clients are told the proxy's working directory, so absolute paths write to the wrong tree #744

Description

@rynfar

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 extractClientWorkingDirectorybuildCwdNote → 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:

  1. the CLI version under test doesn't emit that line at all, or
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions