[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

Status Open
Reported on v2.1.215
Maintainer reply None cached
Activity 0 comments · opened Jul 21, 2026

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)

View original on GitHub ↗