Session cwd silently resets to original launch directory, discarding legitimate navigation (affects PreToolUse hook cwd for all tool types)
Summary
A session's tracked working directory can silently drift back to its original launch directory, discarding all subsequent legitimate cd navigation — and this same stale/drifted cwd is what gets passed to PreToolUse hooks for any tool type (not just Bash), causing hooks that gate on "what repo/branch am I in" to enforce against the wrong repository entirely.
Impact
We run a PreToolUse hook (a small Python script invoked via settings.json) that blocks Write/Edit/Bash calls on Jira-ticket-keyed git branches unless a claim receipt exists. It resolves "what branch is this operation happening against" from the cwd field in the hook's stdin JSON. When session cwd has silently drifted to an unrelated sibling repo, the hook enforces against that repo's checked-out branch instead of the branch the user believes they're on — producing a confusing, byte-identical-looking false-positive block that cites a completely unrelated ticket/branch.
More generally: any tooling (hooks, or an agent's own directory bookkeeping) that trusts the harness's reported cwd as authoritative for "where is this session working" can be misled by this drift.
Evidence #1 — original incident (transcript)
Session transcript (local project dir -Users-kicksaw-Claude-Code-sf-architect--claude-worktrees-git-local-sync-dc02d2), working in a git worktree for repo A (sf-architect, branch claude/git-local-sync-dc02d2):
- A sequence of
Bashtool calls each rancd /path/to/repo-B/...; <read-only command>to inspect reference docs living in a different, unrelated sibling repo (internal-plugins). Eachcdwas to a deeper subdirectory of repo B (e.g.internal-plugins/plugins/sf-architect/skills/sf-architect/references/pipeline/qa-fix). - No command ever
cd'd back to repo A. - The next tool call — a
Write, notBash— was blocked by our PreToolUse hook citing repo B's currently-checked-out branch (a keyed branch belonging to a completely different, unrelated ticket). - The blocked call's own result metadata contained both:
"cwd": ".../internal-plugins/plugins/sf-architect/skills/sf-architect/references/pipeline/qa-fix"(repo B, several directories deep)"gitBranch": "claude/git-local-sync-dc02d2"(repo A's actual branch — correct!)
in the same result object — i.e. the harness's own gitBranch metadata still knew the true branch, while the cwd value handed to the hook (and presumably to the Write tool's own resolution logic) had drifted to the unrelated repo.
Evidence #2 — live reproduction (this session)
In a fresh session, after cd-ing from the session's original launch directory into an unrelated repo and doing ~40 tool calls of legitimate work there (cwd persisted correctly across all of them, as expected for a Bash-tool-like persistent shell):
cd /tmp && pwd
printed /tmp as expected, but immediately after, the harness appended:
Shell cwd was reset to /Users/<user>/<original-session-launch-directory>
A follow-up pwd confirmed the cwd was now back at the original launch directory — not the repo I had legitimately been working in for the prior ~40 tool calls. All of that intermediate, legitimate navigation was silently discarded.
Evidence #3 — recurrence without an out-of-bounds cd
Later in the same session, after cd-ing back into the legitimate working repo and continuing normal work (including one aborted tool call and a user-message turn boundary), a plain pwd check showed the cwd had drifted back to the original launch directory again — with no explicit out-of-bounds cd performed in between. This suggests the reset/drift is not only triggered by a cd outside some allowed-directory set; it can also recur across tool-call-error or turn boundaries.
Expected behavior
- If cwd tracking enforces an allowed-directory boundary, a
cdthat legitimately moved the session's working directory to a new location (as evidence #2 shows normally works fine) should update the "authoritative" cwd the harness resets to — not silently reset all the way back to the session's original launch directory. - Whatever cwd value is fed to
PreToolUsehooks (forWrite/Edit/Bash/etc.) should match whatevergitBranch/other session metadata reports, so hook authors don't have to guard against these two signals disagreeing within the same tool call's metadata.
Environment
- Claude Code version: 2.1.219 (from transcript metadata; evidence #2/#3 gathered on a more recent version)
- macOS (Darwin)
- Entry point: claude-desktop / CLI (both evidenced)
Reproduction steps (evidence #2/#3 class)
- Start a Claude Code session in directory A.
- Ask it to
cdinto an unrelated directory B and do several tool calls there (confirm cwd persists correctly). - Ask it to run a command that
cds outside whatever the harness's allowed-directory set is (e.g.cd /tmp && pwd). - Observe: cwd is reset, but to directory A (the original launch dir), not directory B (the last legitimate working directory).