[BUG] git status polling causes tsc --watch infinite recompilation loops on macOS via FSEvents
Bug Description
Claude Code periodically runs git status --short and git log --oneline -n 5 for its status bar/system context. On macOS, git's lstat64 calls on dirty (unstaged) files generate FSEvents notifications. TypeScript's tsc --watch (using the default useFsEvents strategy) picks these up as file changes, triggering infinite recompilation loops approximately every 20 seconds.
Steps to Reproduce
- Open a TypeScript project with
tsc --watchrunning (or any FSEvents-based file watcher) - Have unstaged changes in the working directory
- Start Claude Code in the same project directory
- Observe
tsc --watchrecompiling every ~20 seconds despite no actual file changes
Root Cause
Git acquires advisory locks and updates file metadata (.git/index.lock, stat calls on dirty files) during git status. These operations generate FSEvents on macOS, which tsc --watch (and other watchers like nodemon, webpack-dev-server, etc.) interpret as file modifications.
Evidence from sudo fs_usage -w -f filesys -e git:
Every ~20 seconds, paired git PIDs spawned by Claude Code run lstat64 on files with unstaged changes. Zero write events — files are only stat'd, never modified. The FSEvents subsystem still emits notifications for these stat operations, causing watchers to trigger.
Specific git commands observed:
git status --shortgit log --oneline -n 5git status --porcelain
Proposed Fix
Add --no-optional-locks flag to all background/polling git commands, OR set GIT_OPTIONAL_LOCKS=0 in the environment of spawned git processes.
From git-status(1):
--no-optional-locks: Do not try to update the index or other files that can be updated as a side effect, if the only purpose is to optimize later git operations.
This is the standard solution for automated/scripted git status polling. It prevents git from:
- Creating
.git/index.lock - Refreshing the index (which triggers
lstat64on tracked files) - Generating FSEvents that confuse file watchers
Implementation options (in order of preference):
- Best: Set
GIT_OPTIONAL_LOCKS=0in the env when spawning git subprocesses for status/polling - Alternative: Add
--no-optional-locksflag to allgit statusandgit loginvocations used for the status bar - Minimal: Document the workaround for users
Workaround
Users can add to their shell config:
# ~/.zshrc or ~/.bashrc
export GIT_OPTIONAL_LOCKS=0
# ~/.config/fish/config.fish
set -gx GIT_OPTIONAL_LOCKS 0
This env var is inherited by all child processes (including git spawned by Claude Code) and is safe for all git operations — it only skips opportunistic index refresh, not required locks.
Impact
- Affects all macOS users running file watchers (tsc --watch, nodemon, webpack, vite, etc.) alongside Claude Code
- Causes unnecessary CPU usage from repeated compilations
- Can mask real compilation errors in the noise of phantom rebuilds
- Particularly disruptive for large TypeScript projects where compilation takes several seconds
Environment
- OS: macOS (any version using FSEvents)
- Claude Code: 2.1.42
- tsc: 5.x (any version using
useFsEventswatch strategy, which is the default on macOS)
Related Issues
- #11005 (stale
.git/index.lock— same root cause: git lock files from status polling) - #9096
- #10256
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗