Bash tool drops a leading `cd <dir> &&` based on a stale session cwd after entering a git worktree

Status Open
Reported on v2.1.284
Maintainer reply None cached
Activity 0 comments · opened Oct 1, 2026

Summary

The Bash tool silently drops a leading cd <dir> && from a command when <dir> equals the session's recorded working directory. That recorded directory can lag behind the shell's actual working directory right after the shell cds into a git worktree. The rest of the command then runs in the worktree, even though the command explicitly changed to the repository root first.

Environment

  • Claude Code 2.1.284 (agent SDK 0.3.284), launched by Claude Desktop 2.16120.0
  • Windows 11, Git Bash as the Bash tool shell
  • Repository with linked git worktrees under <repo>/.worktrees/

What happens

In one assistant turn:

  1. cd <repo>/.worktrees/task && pwd prints the worktree path, as expected.
  2. cd <repo> && pwd && python -c "import os; print(os.getcwd())" prints the worktree path twice. The cd <repo> never ran.

The command line of the spawned shell confirmed it: the eval '…' argument no longer contained the cd <repo> && prefix.

Afterwards Claude Desktop reported Primary working directory: <repo>/.worktrees/task (was <repo>), together with its worktree notice ("Do NOT cd to the original repository root"). So the session's recorded directory moved to the worktree only after the second command had already been normalized against the old one.

It is intermittent and seems to depend on timing. In our sessions it happened 5 times when an agent ran cd <repo> && python scripts/worktree.py merge <task> right after working in the worktree. The merge script then ran from inside the worktree, which also locked the worktree directory on Windows and broke its cleanup. Other attempts with the same sequence worked.

Cause (from the bundled code)

Bash input normalization (normalizeToolInput, case for the Bash tool) does roughly:

const cwd = oe(); // AsyncLocalStorage cwd ?? session state cwd
command = oQ(command, isWindows ? [cwd, toPosixPath(cwd)] : [cwd]).command;
// oQ strips a leading `cd ${dir} && ` for the first dir that matches

oe() is the session's recorded cwd, not the cwd the shell will actually start in. When the two differ, stripping changes what the command does instead of removing a no-op.

Expected

A leading cd X && is only removed when the shell will really start in X. Alternatively, it is never removed when the recorded cwd and the shell cwd differ.

Workaround

Spell the path in a form that is not on the strip list, for example cd C:/repo && … (drive letter with forward slashes) on Windows, or use absolute paths and git -C.

View original on GitHub ↗