Skip to content

[BUG] git status polling causes tsc --watch infinite recompilation loops on macOS via FSEvents #25750

@ofcRS

Description

@ofcRS

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

  1. Open a TypeScript project with tsc --watch running (or any FSEvents-based file watcher)
  2. Have unstaged changes in the working directory
  3. Start Claude Code in the same project directory
  4. Observe tsc --watch recompiling 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 --short
  • git log --oneline -n 5
  • git 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 lstat64 on tracked files)
  • Generating FSEvents that confuse file watchers

Implementation options (in order of preference):

  1. Best: Set GIT_OPTIONAL_LOCKS=0 in the env when spawning git subprocesses for status/polling
  2. Alternative: Add --no-optional-locks flag to all git status and git log invocations used for the status bar
  3. 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 useFsEvents watch strategy, which is the default on macOS)

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplatform:macosIssue specifically occurs on macOS

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions