[BUG] /diff panel runs git without cwd, so it reads the launch directory instead of the session worktree (2.1.243)

Status Open
Maintainer reply None cached
Activity 5 comments · opened Aug 25, 2026

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

  1. Start claude in a git repository, for example /path/to/repo.
  2. Ask Claude to work in a worktree. The session moves to .claude/worktrees/<name>.
  3. Confirm the move. pwd in Bash shows the worktree path.
  4. Edit a file in the worktree.
  5. Run /diff.
  6. Actual: the panel shows the uncommitted changes of /path/to/repo. The worktree edit is absent.
  7. 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

View original on GitHub ↗

3 Comments

alecsammon · 1 month ago

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 cwd produces a hard failure instead:

Diff unavailable
Couldn't read the git diff — it will retry on the next change

This is common for anyone who keeps many repos under one parent directory and starts claude there, using /add-dir or 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:

async function bQt(t, n, i = q) {
  let r = await $pn();                          // git rev-parse --show-toplevel
  if (r === null) return { diff: null };        // <-- non-repo cwd lands here
  let e = await Dgt(t);
  if (e === null) return { diff: null };
  let s = await $pn();
  if (s === null || s !== r) return { diff: null };
  …

with the probe still passing an options object that omits useCwd, exactly as described above:

async function $pn(e) {
  let { stdout: t, code: r } = await Fe(lt(), ["--no-optional-locks", "rev-parse", "--show-toplevel"],
    { timeout: CX, preserveOutputOnError: !1, abortSignal: e });   // no useCwd -> cwd: undefined
  if (r !== 0 || t.trim() === "") return null;
  return t.trim();
}

{ diff: null } reaches the panel as stats === null, which renders the message:

if (!eV) { if (GP === null) Sw = "Diff unavailable", nV = "Couldn't read the git diff — it will retry on the next change" …

In a non-repo directory git rev-parse --show-toplevel exits 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 no EnterWorktree will 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

  1. mkdir -p ~/src && cd ~/src, with two or more git repos as children (~/src/repo-a, ~/src/repo-b). ~/src itself is not a repo.
  2. Start claude in ~/src.
  3. Optionally /add-dir ~/src/repo-a, or have the session enter a worktree under it. Bash pwd confirms the move.
  4. Edit a tracked file in that repo.
  5. Run /diff.
  6. Actual: Diff unavailable / Couldn't read the git diff — it will retry on the next change, for the rest of the session.
  7. Expected: the uncommitted changes of the session's current working directory.

Verification that it's purely the launch directory — the same probe the panel runs, in three places:

$ cd ~/src            && git --no-optional-locks rev-parse --show-toplevel
fatal: not a git repository (or any parent up to mount point /)
exit=128
$ cd ~/src/repo-a     && git --no-optional-locks rev-parse --show-toplevel
/home/u/src/repo-a
exit=0
$ cd ~/src/repo-a/.claude/worktrees/wt && git --no-optional-locks rev-parse --show-toplevel
/home/u/src/repo-a/.claude/worktrees/wt
exit=0

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. bQt threads the resolved root into its later git calls via an explicit cwd:

await Be(lt(), w(["--no-optional-locks", "-c", "diff.relative=false", "diff", "--numstat"]),
  { ...E(s), maxBuffer: 1e7 })

…but not into $pn(), the resolver that produced s in 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

  • Claude Code: 2.1.263 (CLI, terminal)
  • OS: macOS, Darwin 25.6.0
  • git 2.55.0

---
Posted by Claude Code on behalf of @alecsammon

reminiscience · 27 days ago

Same root cause, different trigger – still present in 2.1.276 on Windows 11.

You don't need EnterWorktree to hit this. It is enough that the session's Bash working directory differs from the launch directory:

  1. Start claude in a folder that is not a git repository (in my case a OneDrive project folder).
  2. Let Claude cd into a local git repo via the Bash tool, edit files and commit there. The status line correctly shows that repo and its branch.
  3. Run /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), /diff is 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.

Martini991 · 9 days ago

Still present in 2.1.292 (Linux), but the panel has a new implementation and a new root cause, so the useCwd fix suggested above no longer applies.

What changed

In 2.1.292 /diff comes from cc-plugin-diff, a mod built into Claude Code (docs: "The panel and the dialog both come from cc-plugin-diff, one of the mods built into Claude Code"). The mod no longer inherits process.cwd(): it passes an explicit cwd to every git command. But it captures that directory once, in its session.start handler, and keeps it for the rest of the session. After EnterWorktree, 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)

// session.start: the directory the session starts in is stored once
e("session.start", async (p, m, g) => { if (await Vn({ /* host API */ }, m.cwd), o) /* … */ })
async function Vn(p, m) { n = await qr(p) ?? await p.now(), v.cwd = m; /* registers /diff */ }

// every git command runs there
mt = (p) => ({ run: (m, g) => re(p).run(m, v.cwd === "" ? g : { cwd: v.cwd, ...g }), /* … */ })
  • v.cwd is assigned in one place only, Vn, which only the session.start handler calls.
  • The mod handles session.start, prompt.submit, tool.call, command.run (for /clear and /resume) and ui.* events. None of them updates v.cwd, and EnterWorktree does not fire session.start.
  • The repository resolved from that directory (git rev-parse --path-format=absolute --show-toplevel --git-dir --git-common-dir) is cached as well (r ??= …), and only /clear and /resume reset the cache. So a fix needs to refresh both the directory and the cached repository.

Steps to reproduce (2.1.292)

  1. Start claude in a git repository whose working tree has no changes made during this session.
  2. Create a linked worktree under .claude/worktrees/ with staged changes, and have Claude switch into it with EnterWorktree (path). pwd in Bash now prints the worktree.
  3. Run /diff.
  4. Actual: the panel reads the launch checkout and, with nothing changed there this session, shows "No changes this session".
  5. Expected: the panel shows the worktree's changes.

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 claude session with its working directory inside the worktree. v.cwd is then the worktree. The panel runs git as git --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.

Showing cached comments. Read the full discussion on GitHub ↗