[BUG] Desktop: unrequested background `git fetch` to origin on diff/commit refresh — untraceable by design, and no setting disables it
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 desktop app — not the CLI — runs git fetch against origin with no user request, no permission prompt, and no record in any session transcript. It is triggered by ordinary UI work (computing a diff, listing commits, the diff prefetcher), so it needs no worktree, no PR, and no git-related prompt from the user.
This is related to #21108, but that report is about the CLI; the code path below exists only in the desktop app, so a CLI fix will not cover it. It is also the network-side analogue of #79713 (harness mutating the user's repo at session spawn with nothing in the transcript).
Where it comes from
Grepping the literal string --no-write-fetch-head:
| Binary | Hits |
|---|---|
| %APPDATA%\Claude\claude-code\2.1.221\claude.exe (desktop-managed CLI) | 0 |
| VS Code extension anthropic.claude-code-2.1.207 native binary | 0 |
| C:\Program Files\WindowsApps\Claude_1.25927.0.0_x64__pzs8sxrjxfjjc\app\resources\app.asar | 1 |
It is emitted by the desktop Electron main process, from the worktree manager's refreshSourceRef():
execFetchCapped(
['fetch', '--no-write-fetch-head', 'origin', `+refs/heads/${n}:refs/remotes/origin/${n}`],
baseRepo, kt, signal
)
and the -c prefix matches the observed command exactly:
var M = ['-c','safe.directory=*'],
N = ['-c','core.fsmonitor=false'],
Ce = ['-c','core.hooksPath=/dev/null'];
var we = [...Ce, ...M, ...N];
There are two trigger paths, and the second one is the problem:
- Worktree acquisition/creation — on a cold repo it first runs a full
fetch --prune originin the background (telemetry reasoncold_full_fetch), then narrows torefreshSourceRef. GitStatusService—fetchGitDiffUncached,fetchGitDiffStatsUncached,getGitCommits, and the diff prefetcher each dothis.config.refreshBaseRef && await this.config.refreshBaseRef(cwd, baseRef), which funnels into the samerefreshSourceRef. No worktree required.
Why this is a security issue, not a UX annoyance
- Unrequested outbound connection using the user's credentials. On SSH remotes it consumes the SSH agent / key — exactly the symptom in #21108. On VPN-gated or air-gapped remotes it is an unrequested connection attempt to a host the user did not ask to contact at that moment.
- **The CLI itself classifies
git fetchas not read-only.** The CLI bundle excludesgit fetch/git pullfrom its read-only git set, on the stated grounds that despite looking read-only they amount to arbitrary code execution. The desktop app runs, unattended and unprompted, the very command the CLI refuses to auto-approve. --no-write-fetch-headmeans it leaves no trace.FETCH_HEADis never updated, so the user cannot tell afterwards that it happened. I initially concluded my repo had never been auto-fetched becauseFETCH_HEADwas stale — that inference is wrong by construction.- It silently advances
refs/remotes/origin/<branch>, breaking baregit push --force-with-lease. The lease is taken against the remote-tracking ref, and a background fetch renews it behind the user's back. Same class of harm as #77660 ("refs/remotes/*must remain truthful"), but here it is a concrete data-loss vector.
There is currently no way to disable it
I grepped the desktop bundle for autoFetch, disableFetch, fetchDisabled, skipFetch, offlineMode, disableGit — zero hits. The only related key, useWorktree, is a per-session/scheduled-task parameter and does not affect path 2, so opting out of worktrees does not stop this.
What Should Happen?
- Add a setting that disables background remote git access, e.g.
git.autoFetch: "never" | "worktree-only" | "always", honored by both trigger paths and readable fromsettings.jsonso it can also be applied as a managed/enterprise policy. - Make it opt-in, or at minimum prompt once per repository. Contacting a remote with the user's credentials should not be a silent default — this is the same consent boundary the CLI already enforces by excluding
git fetchfrom its read-only set. - Until (1) ships, document the behavior, including that
--no-write-fetch-headmakes it untraceable and how it interacts withgit push --force-with-lease.
Error Messages/Logs
Observed process (Windows 11, desktop app is the parent process):
git.exe -c core.hooksPath=/dev/null -c safe.directory=* -c core.fsmonitor=false fetch --no-write-fetch-head origin +refs/heads/master:refs/remotes/origin/master
No corresponding entry in any session transcript. .git/FETCH_HEAD is NOT updated by this command, so the repository retains no evidence that it ran.
String search for --no-write-fetch-head:
%APPDATA%\Claude\claude-code\2.1.221\claude.exe 0 hits
.vscode\extensions\anthropic.claude-code-2.1.207-win32-x64\...\claude.exe 0 hits
C:\Program Files\WindowsApps\Claude_1.25927.0.0_x64__pzs8sxrjxfjjc\app\resources\app.asar 1 hit
Guards observed in app.asar (constant names are minifier output for build 1.25927.0.0):
Z = 1800*1e3 30-minute TTL + in-flight dedup on the worktree path
kt = 1e4 10s cap on the narrow fetch
Ot = 3e4 30s cap on the full "fetch --prune origin"
separate TTL on the diff path (telemetry reason: diff_base_ref_timeout)
failures are non-fatal: logged as "continuing with on-disk refs"
Telemetry events already emitted for this code path:
desktop_ccd_worktree_source_ref_fetch
cold_full_fetch
desktop_ccd_worktree_fetch_degraded
Steps to Reproduce
- Open a repository that has an
originremote in the desktop app's Code tab. No worktree, no PR, no git-related prompt to Claude. - Let the app render the changes/diff view or the commit list. (The diff prefetcher will also do this on its own, without interaction.)
- Observe the spawned processes. On Windows:
``powershell``
Get-CimInstance Win32_Process -Filter "Name = 'git.exe'" |
Select-Object ProcessId, ParentProcessId, CommandLine
The fetch --no-write-fetch-head origin +refs/heads/<branch>:refs/remotes/origin/<branch> command appears, parented to the desktop app.
- Check
.git/FETCH_HEAD— it is not updated, so nothing in the repo records that the fetch happened.
With an SSH remote whose key carries a passphrase, this surfaces as an unexplained passphrase/agent prompt with no user action behind it — the same symptom as #21108, but a different code path.
Note on timing: a 30-minute TTL applies, so if you have used the repo recently you may need to wait it out before the fetch fires again.
Claude Model
Other
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.221 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
- Surface: Claude Desktop Code tab, not a terminal — the "Terminal/Shell" dropdown has no matching option, hence "Other".
- Desktop app version:
1.25927.0.0(Windows 11 x64, MSIX install). - Bundled CLI:
2.1.221. VS Code extension present at2.1.207(also unaffected). - Remote: self-hosted GitLab over SSH (
git@<internal-git-host>:...). Repo has no worktrees (git worktree listshows only the main working tree). - All findings above come from grepping
app.asarof build1.25927.0.0. The minified identifiers (Z,kt,Ot,we) will change between builds; the stable anchors are the method namerefreshSourceRef, the literal flag--no-write-fetch-head, and the telemetry event names. - Related: #21108 (same symptom, CLI code path), #79713 (harness mutating the repo at session spawn with no transcript record), #77660 (truthfulness of
refs/remotes/*).
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗