[BUG] Desktop: unrequested background `git fetch` to origin on diff/commit refresh — untraceable by design, and no setting disables it

Status Open
Reported on v2.1.221
Maintainer reply None cached
Activity 2 comments · opened Aug 7, 2026

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:

  1. Worktree acquisition/creation — on a cold repo it first runs a full fetch --prune origin in the background (telemetry reason cold_full_fetch), then narrows to refreshSourceRef.
  2. GitStatusServicefetchGitDiffUncached, fetchGitDiffStatsUncached, getGitCommits, and the diff prefetcher each do this.config.refreshBaseRef && await this.config.refreshBaseRef(cwd, baseRef), which funnels into the same refreshSourceRef. No worktree required.

Why this is a security issue, not a UX annoyance

  1. 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.
  2. **The CLI itself classifies git fetch as not read-only.** The CLI bundle excludes git fetch / git pull from 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.
  3. --no-write-fetch-head means it leaves no trace. FETCH_HEAD is never updated, so the user cannot tell afterwards that it happened. I initially concluded my repo had never been auto-fetched because FETCH_HEAD was stale — that inference is wrong by construction.
  4. It silently advances refs/remotes/origin/<branch>, breaking bare git 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?

  1. Add a setting that disables background remote git access, e.g. git.autoFetch: "never" | "worktree-only" | "always", honored by both trigger paths and readable from settings.json so it can also be applied as a managed/enterprise policy.
  2. 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 fetch from its read-only set.
  3. Until (1) ships, document the behavior, including that --no-write-fetch-head makes it untraceable and how it interacts with git 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

  1. Open a repository that has an origin remote in the desktop app's Code tab. No worktree, no PR, no git-related prompt to Claude.
  2. Let the app render the changes/diff view or the commit list. (The diff prefetcher will also do this on its own, without interaction.)
  3. 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.

  1. 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 at 2.1.207 (also unaffected).
  • Remote: self-hosted GitLab over SSH (git@<internal-git-host>:...). Repo has no worktrees (git worktree list shows only the main working tree).
  • All findings above come from grepping app.asar of build 1.25927.0.0. The minified identifiers (Z, kt, Ot, we) will change between builds; the stable anchors are the method name refreshSourceRef, 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/*).

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗