Bash tool cwd forced to `/` when claude CLI is spawned from a non-shell parent in stream-json mode
Summary
When the claude CLI is spawned from a non-shell parent process (Node, Bun, Python, etc.) using --output-format stream-json --input-format stream-json, the Bash tool's subprocess always sees its working directory as /, regardless of the cwd passed to the spawner. The CLI's own init-time cwd is correct, so the divergence happens only at the Bash tool's subprocess spawn.
This breaks Agent SDK hosts that expect per-session working directories, and it also breaks automatic CLAUDE.md discovery from the session's nominal cwd.
Environment
- OS: macOS 15 (Darwin 25.3.0), Apple Silicon
- claude CLI: 2.1.104
- Node: 22.22.0
@anthropic-ai/claude-agent-sdk: latest as of 2026-04-12
Reproduction
Failing case (Node spawn)
// /tmp/sdk-cwd-test.mjs
import { spawn } from 'child_process';
const target = '/Users/me/project/subdir';
const proc = spawn('/Users/me/.local/bin/claude', [
'-p',
'--output-format', 'stream-json',
'--verbose',
'--input-format', 'stream-json',
'--dangerously-skip-permissions',
], {
cwd: target,
stdio: ['pipe', 'pipe', 'inherit'],
env: {
...process.env,
ANTHROPIC_API_KEY: 'sk-ant-...',
},
});
proc.stdin.write(JSON.stringify({
type: 'user',
message: { role: 'user', content: 'Run `pwd` and output only the path.' }
}) + '\n');
proc.stdin.end();
let out = '';
proc.stdout.on('data', c => out += c);
proc.on('close', () => {
for (const line of out.split('\n')) {
try {
const d = JSON.parse(line);
if (d.type === 'system' && d.subtype === 'init') console.log('INIT cwd:', d.cwd);
if (d.type === 'result') console.log('Bash pwd:', d.result);
} catch {}
}
});
Run:
node /tmp/sdk-cwd-test.mjs
Output:
INIT cwd: /Users/me/project/subdir ← correct
Bash pwd: / ← WRONG
Working case (shell pipe)
cd /Users/me/project/subdir && \
echo '{"type":"user","message":{"role":"user","content":"Run pwd and output only path"}}' | \
/Users/me/.local/bin/claude -p --output-format stream-json --verbose --input-format stream-json --dangerously-skip-permissions
Output contains:
{"type":"result","result":"/Users/me/project/subdir", ...}
Both invocations use identical CLI flags, identical stdin content, identical env. The only difference is the parent process type (Node vs shell).
Expected behavior
The Bash tool's subprocess should be spawned with the same cwd as the one passed to the host's spawn() call (which is the same cwd that the system/init message correctly reports).
Actual behavior
The Bash tool's subprocess is spawned with cwd=/. Any pwd, relative ls, or cat ./file behaves as if the agent's workspace is the filesystem root.
Impact
Agent SDK hosts that run the CLI as a programmatic subprocess (typical for chatbots, automations, Claude Agent SDK integrations) can't rely on cwd for their bash tool operations. Workarounds require either:
- Injecting a "your logical working directory is X" notice into
systemPrompt.appendand training the agent to use absolute paths orcd X && ... - Rewriting bash commands via a
PreToolUsehook (fragile, has side effects)
Neither is a proper fix; both are user-space patches for what looks like an internal chdir inside the CLI.
Reproduced with multiple spawners
The same failure reproduces with:
child_process.spawnfrom Nodechild_process.spawnSync({ input })from NodeBun.spawn({ cwd })- Python
subprocess.run(..., cwd=target) - Node spawning
/bin/sh -c "cd target && echo ... | claude ..."(thecdinside the shell has no effect on the final cwd) detached: truespawn- All
stdiocombinations (pipe/inherit/ignorein any slot)
The failure does NOT reproduce when the shell itself is the direct parent of claude (interactive shell, nohup bash -c, etc.).
Things that did not fix it
Tested but ineffective from the host side:
CLAUDECODE=1env varCLAUDE_CODE_ENTRYPOINT=clienv varCLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1env var- Shell-impersonation env vars (
SHLVL,BASH_VERSION,PPID=<parent>) --add-dir <target>flag--bareflagprocess.chdir(target)in Node before spawn (so parent cwd already equals target)- Passing
cwdexplicitly vs inheriting from parent
Key observation
The system/init message (stream-json output) always contains the correct cwd, so the CLI itself knows where it is. The failure is specifically in the Bash tool's subprocess spawn path. From binary string inspection, the tool uses cwd: YY_() where YY_() resolves to asyncLocalStorage.cwd ?? R_.cwd. Something between init and the first Bash call sets that to / when the parent is not a shell.
Asks
- Confirm whether this is intentional sandbox behavior or an actual bug.
- If intentional: document it and provide an escape hatch (env var or option).
- If a bug: fix
Bashtool spawn to use the same cwd thatsystem/initreports.
Happy to provide additional diagnostics (strace, DEBUG_CLAUDE_AGENT_SDK=1 output, etc.) on request.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗