Bash tool cwd forced to `/` when claude CLI is spawned from a non-shell parent in stream-json mode

Resolved 💬 2 comments Opened Apr 12, 2026 by ryou0903 Closed Apr 13, 2026

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.append and training the agent to use absolute paths or cd X && ...
  • Rewriting bash commands via a PreToolUse hook (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.spawn from Node
  • child_process.spawnSync({ input }) from Node
  • Bun.spawn({ cwd })
  • Python subprocess.run(..., cwd=target)
  • Node spawning /bin/sh -c "cd target && echo ... | claude ..." (the cd inside the shell has no effect on the final cwd)
  • detached: true spawn
  • All stdio combinations (pipe/inherit/ignore in 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=1 env var
  • CLAUDE_CODE_ENTRYPOINT=cli env var
  • CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR=1 env var
  • Shell-impersonation env vars (SHLVL, BASH_VERSION, PPID=<parent>)
  • --add-dir <target> flag
  • --bare flag
  • process.chdir(target) in Node before spawn (so parent cwd already equals target)
  • Passing cwd explicitly 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

  1. Confirm whether this is intentional sandbox behavior or an actual bug.
  2. If intentional: document it and provide an escape hatch (env var or option).
  3. If a bug: fix Bash tool spawn to use the same cwd that system/init reports.

Happy to provide additional diagnostics (strace, DEBUG_CLAUDE_AGENT_SDK=1 output, etc.) on request.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗