[BUG] /diff panel runs git without cwd, so it reads the launch directory instead of the session worktree (2.1.243)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.243)
What's Wrong?
The /diff panel runs every git command without a cwd. The git child process therefore inherits process.cwd() of the claude process, which is the directory where the CLI started.
EnterWorktree changes the session working directory that Bash, Read, and Edit use. It does not change process.cwd() of the CLI process. After EnterWorktree, the panel reads the base checkout and reports its changes. Work done in the worktree stays invisible.
Root Cause
The shared git runner sets cwd only when the caller asks for it (deobfuscated from the 2.1.243 bundle):
async function Ee(gitPath, args, opts = { timeout: …, preserveOutputOnError: !0, useCwd: !0 }) {
return z(gitPath, args, {
abortSignal: opts.abortSignal,
timeout: opts.timeout,
preserveOutputOnError: opts.preserveOutputOnError,
cwd: opts.useCwd ? c() : undefined, // <-- undefined when useCwd is absent
…
})
}
useCwd: !0 is a default parameter value. It applies only when the caller omits the options object completely. Any caller that passes its own options object and forgets useCwd silently gets cwd: undefined.
Every diff-panel call passes an options object, and not one of them sets useCwd:
b(v(), ["--no-optional-locks","rev-parse","--verify","--quiet","HEAD"], {timeout:P, preserveOutputOnError:!1, abortSignal:e})
b(v(), ["--no-optional-locks","rev-parse","--show-toplevel"], {timeout:P, preserveOutputOnError:!1, abortSignal:e})
b(v(), ["--no-optional-locks","-c","diff.relative=false","diff","--numstat"], {timeout:P, preserveOutputOnError:!1, abortSignal:e})
b(v(), ["--no-optional-locks","-c","diff.relative=false","diff",e,"--shortstat"], {timeout:P, preserveOutputOnError:!1, abortSignal:t})
b(v(), ["--no-optional-locks","ls-files","--others","--exclude-standard","--full-name"], {timeout:P, preserveOutputOnError:!1, abortSignal:t})
The string useCwd does not occur once in the whole diff-panel module. Other git call sites in the same bundle do pass it, for example:
_(b(), [...w, "diff", ...we, "HEAD"], { useCwd: !0, maxBuffer: e })
The rev-parse --show-toplevel call is the worst one. It anchors the whole panel to the repository root of the wrong directory.
Steps to Reproduce
- Start
claudein a git repository, for example/path/to/repo. - Ask Claude to work in a worktree. The session moves to
.claude/worktrees/<name>. - Confirm the move.
pwdin Bash shows the worktree path. - Edit a file in the worktree.
- Run
/diff. - Actual: the panel shows the uncommitted changes of
/path/to/repo. The worktree edit is absent. - Expected: the panel shows the uncommitted changes of
.claude/worktrees/<name>.
Starting a new claude session inside the worktree works correctly, because process.cwd() and the worktree then agree.
Suggested Fix
Add useCwd: !0 to every git call in the diff-panel module. A safer change is to invert the flag, so that cwd is applied unless a caller opts out. The current default-parameter form fails open, and any new caller repeats the bug.
Related Issues
- #65767 (closed) — same symptom in the UI diff view; this report adds the root cause.
- #78733 — file links resolve against the original project root after
EnterWorktree. - #45202 (closed) — "Open in VS Code" opens the original directory after
EnterWorktree. - #78212 — desktop diff panel shows the base repo for a hook-created worktree.
Is this a regression?
I do not know.
Environment
- Claude Code: 2.1.243 (CLI, terminal)
- OS: macOS, Darwin 25.5.0
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Independently hit and confirmed this on 2.1.263 — still unfixed, and there's a second, harder failure mode worth adding to the report.
Additional symptom: a non-repo launch directory fails permanently instead of showing the wrong diff
This issue documents the case where the launch directory is a repo, so the panel silently shows the base checkout instead of the worktree. If the launch directory is not a git repository at all, the same missing
cwdproduces a hard failure instead:This is common for anyone who keeps many repos under one parent directory and starts
claudethere, using/add-diror a worktree to pick the repo to work in. The parent directory has no.git, so the panel is dead for the entire session no matter which repo or worktree the session moves into.Mechanism (2.1.263)
Same root cause, one step earlier. The diff worker's first act is the toplevel probe, and it bails to a null diff if the probe fails:
with the probe still passing an options object that omits
useCwd, exactly as described above:{ diff: null }reaches the panel asstats === null, which renders the message:In a non-repo directory
git rev-parse --show-toplevelexits 128 (fatal: not a git repository …), so$pn()returns null on the very first call and no part of the diff pipeline ever runs.Why this variant is worse than the wrong-diff one
The retry promise cannot be kept.
process.cwd()of the CLI process never changes for the life of the session, so every retry re-runs the identical failing probe. No edit, no/add-dir, and noEnterWorktreewill ever make the panel work — it just re-fails quietly on each change. A user has no way to tell this apart from a transient glitch.Repro
mkdir -p ~/src && cd ~/src, with two or more git repos as children (~/src/repo-a,~/src/repo-b).~/srcitself is not a repo.claudein~/src./add-dir ~/src/repo-a, or have the session enter a worktree under it. Bashpwdconfirms the move./diff.Diff unavailable/Couldn't read the git diff — it will retry on the next change, for the rest of the session.Verification that it's purely the launch directory — the same probe the panel runs, in three places:
Every worktree and repo is healthy; only the launch directory fails.
One extra note on the suggested fix
The inverted-flag suggestion above is the right call, because the diff module is already internally inconsistent.
bQtthreads the resolved root into its later git calls via an explicit cwd:…but not into
$pn(), the resolver that producedsin the first place. So the module knows which directory it means; it just doesn't tell git when asking where the repo is. Fixing only the call sites listed above leaves that asymmetry in place for the next caller to trip over.Environment
---
Posted by Claude Code on behalf of @alecsammon
Same root cause, different trigger – still present in 2.1.276 on Windows 11.
You don't need
EnterWorktreeto hit this. It is enough that the session's Bash working directory differs from the launch directory:claudein a folder that is not a git repository (in my case a OneDrive project folder).cdinto a local git repo via the Bash tool, edit files and commit there. The status line correctly shows that repo and its branch./diff→ "The diff panel shows git changes – the current directory isn't in a git repository".So the panel is not just reading the wrong checkout after
EnterWorktree; whenever the launch directory has no.git, it reports "not a git repository" regardless of where the session is actually working. In a workflow where sessions routinely start in non-git folders but commit to a separate repo (e.g. a notes/knowledge repo updated at the end of each session),/diffis never usable.Expected: the panel should use the same persisted working directory the Bash/Read/Edit tools use (the one shown in the status line), or fall back to the repo of the most recent commit in the session.
Environment: Claude Code 2.1.276, Windows 11 Pro 10.0.26200, PowerShell 7 as primary shell, Git Bash available.
Still present in 2.1.292 (Linux), but the panel has a new implementation and a new root cause, so the
useCwdfix suggested above no longer applies.What changed
In 2.1.292
/diffcomes fromcc-plugin-diff, a mod built into Claude Code (docs: "The panel and the dialog both come fromcc-plugin-diff, one of the mods built into Claude Code"). The mod no longer inheritsprocess.cwd(): it passes an explicitcwdto every git command. But it captures that directory once, in itssession.starthandler, and keeps it for the rest of the session. AfterEnterWorktree, the panel still reads the checkout the session was launched in.Root cause (2.1.292, minified names from the mod source embedded in the binary)
v.cwdis assigned in one place only,Vn, which only thesession.starthandler calls.session.start,prompt.submit,tool.call,command.run(for/clearand/resume) andui.*events. None of them updatesv.cwd, andEnterWorktreedoes not firesession.start.git rev-parse --path-format=absolute --show-toplevel --git-dir --git-common-dir) is cached as well (r ??= …), and only/clearand/resumereset the cache. So a fix needs to refresh both the directory and the cached repository.Steps to reproduce (2.1.292)
claudein a git repository whose working tree has no changes made during this session..claude/worktrees/with staged changes, and have Claude switch into it withEnterWorktree(path).pwdin Bash now prints the worktree./diff.Suggested fix
Resolve the repository from the session's current working directory (the one Bash, Read and Edit use) each time the panel fetches the diff, instead of the directory captured at
session.start. When that directory changes, drop the cached repository and the pre-session baseline that was built for the old one.Workaround
Start a new
claudesession with its working directory inside the worktree.v.cwdis then the worktree. The panel runs git asgit --git-dir=<gitDir> --work-tree=<toplevel>, and I ran its probe and diff commands by hand with those arguments in a linked worktree: they succeed.Is this a regression?
I don't know. The comments above report the bug on 2.1.243 through 2.1.276 under the earlier implementation.