[BUG] Bash tool: `cd` performed by a command that ends with a non-zero exit code is silently discarded — next call runs in the old cwd, no notice
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The Bash tool's cwd tracking only adopts a directory change if the final exit code of the call is zero. If a command performs a successful cd but the call as a whole exits non-zero, the cd is silently discarded: the next tool call starts in the previous tracked directory, contradicting both shell semantics (cd A && cmd leaves PWD=A even when cmd fails) and the documented contract given to the model ("Working directory persists between calls").
Deterministic 2-call reproduction (v2.1.215, Linux):
call 1: cd /tmp/somedir && false → "Exit code 1"
call 2: pwd → prints the OLD directory (not /tmp/somedir)
Control: replace false with true (or append ; echo done so the call ends with exit 0) and call 2 prints /tmp/somedir. The discard is 100% reproducible and depends only on the call's final exit status.
No notice of any kind is emitted on this path. This is inconsistent with the other cwd-loss path: when a foreground command is moved to the background on timeout, v2.1.215 now explicitly appends "Session cwd remains \<dir\>; directory changes made by the backgrounded command do not apply to subsequent commands." The error-exit path has the same information available but says nothing.
Why this matters in practice
The most common victim shape is cd <worktree-or-subdir> && <test/build command> — exactly the calls most likely to exit non-zero (failing tests, lint errors). The agent then reasonably issues follow-up commands without a cd prefix (the contract says cwd persists), and they run against the wrong directory.
Auditing our transcripts for a 5-day window (session JSONL cwd fields vs. the cd target of each Bash call): 35 discard events matched the error-exit signature across ~1,270 cd-containing calls, affecting every model we run in this harness (Claude models and a third-party model behind an Anthropic-compatible endpoint alike — this is harness-level, not model-level). Observed downstream failures include uv run pytest dying with "Failed to spawn: pytest" because the follow-up call ran one worktree over, and git operations targeting a sibling worktree.
A subtle amplifier: because the discard is silent and the stated contract is "cwd persists", agents mis-attribute the resulting failures ("cwd randomly volatilized"), and weaker models can spiral on that wrong theory instead of re-issuing cd.
What Should Happen?
Preferred: adopt the directory change regardless of the call's exit status, matching real shell semantics — track the shell's actual end-of-call PWD, not "PWD if the call succeeded".
At minimum: emit the same explicit notice already used on the timeout/background path (e.g. "Shell cwd was reset to \<dir\>" / "Session cwd remains \<dir\>") so the agent can recover immediately.
Related issues (distinct, but adjacent cwd-tracking reports)
- #74741 — cwd silently lost when a foreground command is auto-backgrounded / timeout-killed (notice missing). Distinct trigger: here the command completes in the foreground and the discard is decided by its exit code. (The background path gained an explicit notice by v2.1.215; this path still has none.)
- #67725 / #72339 — "cwd resets between tool calls" reports with unidentified trigger conditions; the error-exit rule reported here may explain a subset of those observations (a
cd X && <failing command>earlier in the session).
Environment
- Claude Code version: 2.1.215
- Platform: Anthropic API
- Operating System: Ubuntu/Debian Linux (Docker container)
- Terminal: iTerm2
✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)