Summary
On Windows, wt step prune can fail with unable to access '.git/config': Permission denied because prune runs branch-integration checks concurrently with background worktree removals, and Windows has no shared-read locking on .git/config.
Observed on PR #2788 CI (test (windows)): integration_tests::step_prune::test_prune_locally_merged_when_upstream_diverged panicked prune should succeed, with stderr:
✗ checking branch integration
warning: unable to access '.git/config': Permission denied
fatal: unknown error occurred while reading the configuration files
◎ Removing feature-prune-local worktree & branch in background
Linux/macOS pass the same test; main's Windows CI is green.
Root cause
src/commands/step/prune.rs (~line 512) spawns a thread running integration_reason over a rayon into_par_iter() while the main loop concurrently spawns background worktree removals (handlers.rs → execute_instant_removal_or_fallback: rename + git worktree prune + detached rm -rf). The integration thread reads .git/config; the background removal writes git metadata/config. On Windows this read/write overlap intermittently yields Permission denied.
This race is inherent to prune's parallel design and predates PR #2788. PR #2788's dbcc73ade (revalidate_worktree_before_removal) added two more .git/config-opening subprocesses (git status --porcelain, git rev-parse HEAD) into the main-thread removal path, widening the contention window enough to surface it.
Why it wasn't "fixed" in #2788
Both checks in revalidate_worktree_before_removal are load-bearing data-safety guards and must not be dropped to dodge the race:
ensure_clean is the only dirty-work guard on the fast removal path — execute_instant_removal_or_fallback does rename + prune + rm -rf, which (unlike non-force git worktree remove) never refuses a dirty worktree.
- The HEAD-moved check is the only guard against a post-snapshot commit being destroyed by the
branch -D that runs off the stale pre_computed_integration.
So the fix belongs at the source.
Proposed fix
Make prune not run integration checks concurrently with background removals — e.g. join/collect the integration-check thread before starting any removal, or serialize the two phases. (Code path: src/commands/step/prune.rs around the std::thread::spawn + into_par_iter at ~512 and the rx consumption loop that drives removals.)
A TODO(prune-windows-git-race) is left on revalidate_worktree_before_removal in src/output/handlers.rs pointing here.
Ref PR #2788.
This was written by Claude Code on behalf of Maximilian Roos
Summary
On Windows,
wt step prunecan fail withunable to access '.git/config': Permission deniedbecause prune runs branch-integration checks concurrently with background worktree removals, and Windows has no shared-read locking on.git/config.Observed on PR #2788 CI (
test (windows)):integration_tests::step_prune::test_prune_locally_merged_when_upstream_divergedpanickedprune should succeed, with stderr:Linux/macOS pass the same test;
main's Windows CI is green.Root cause
src/commands/step/prune.rs(~line 512) spawns a thread runningintegration_reasonover arayoninto_par_iter()while the main loop concurrently spawns background worktree removals (handlers.rs→execute_instant_removal_or_fallback: rename +git worktree prune+ detachedrm -rf). The integration thread reads.git/config; the background removal writes git metadata/config. On Windows this read/write overlap intermittently yieldsPermission denied.This race is inherent to prune's parallel design and predates PR #2788. PR #2788's
dbcc73ade(revalidate_worktree_before_removal) added two more.git/config-opening subprocesses (git status --porcelain,git rev-parse HEAD) into the main-thread removal path, widening the contention window enough to surface it.Why it wasn't "fixed" in #2788
Both checks in
revalidate_worktree_before_removalare load-bearing data-safety guards and must not be dropped to dodge the race:ensure_cleanis the only dirty-work guard on the fast removal path —execute_instant_removal_or_fallbackdoes rename + prune +rm -rf, which (unlike non-forcegit worktree remove) never refuses a dirty worktree.branch -Dthat runs off the stalepre_computed_integration.So the fix belongs at the source.
Proposed fix
Make prune not run integration checks concurrently with background removals — e.g. join/collect the integration-check thread before starting any removal, or serialize the two phases. (Code path:
src/commands/step/prune.rsaround thestd::thread::spawn+into_par_iterat ~512 and therxconsumption loop that drives removals.)A
TODO(prune-windows-git-race)is left onrevalidate_worktree_before_removalinsrc/output/handlers.rspointing here.Ref PR #2788.