utils/git/ — Git Operations Utilities

Git integration utilities for repository state, diffs, and filesystem-level operations

A comprehensive git integration layer spanning 6 files and ~2,500 lines. Provides repository detection, branch management, diff generation (PR-style and working-tree), config parsing, gitignore management, and a high-performance filesystem-based caching system that avoids spawning git subprocesses for common queries.

40+
Exports
6
Files
~2.5 k
Lines
50
LRU Cache Max

📚 What is this & why does it exist?

The git utilities module is how Claude Code understands your project's version control state. It detects whether you're in a git repository, reads the current branch and commit, generates diffs to show what you've changed, and manages gitignore rules. Instead of running git commands for every query (slow!), it reads .git/ files directly and caches results using a file watcher—making it fast enough for real-time status updates in the terminal UI.

💡 Real-world analogy

Imagine a building's security control room with monitors and sensors. Instead of sending a guard to physically check every door and window (spawning git subprocesses), the control room has live feeds from sensors on the doors themselves (filesystem watchers on .git/HEAD, .git/refs/). It caches the "all clear" status and only re-checks when a sensor triggers (file changed). For detailed inspections (diffs), it does dispatch a guard—but with strict time limits and area restrictions to prevent delays.

Contents

Architecture

The git utilities are organized in a layered architecture. The high-level git.ts API delegates to the filesystem-based gitFilesystem.ts for read-heavy operations and spawns git subprocesses only for complex tasks like diff generation and gitignore checking.

flowchart TD
  GIT["git.ts\n(High-level API)"] --> GFS["gitFilesystem.ts\n(No-subprocess reads)"]
  GIT --> DIFF["gitDiff.ts\n(Diff generation)"]
  GIT --> IGN["gitignore.ts\n(Ignore management)"]
  GFS --> GCP["gitConfigParser.ts\n(Config parsing)"]
  GFS --> FW["GitFileWatcher\n(Cache + fs.watchFile)"]
  DIFF --> GIT
  IGN --> GIT
  GS["gitSettings.ts\n(Settings bridge)"] -.-> GIT
  FW --> HEAD[".git/HEAD"]
  FW --> REFS[".git/refs/heads/*"]
  FW --> CFG[".git/config"]

Key Classes & Functions

The git utilities export over 40 functions organized across 6 files, ranging from low-level filesystem readers to high-level repository state aggregators.

Class / FunctionFilePurpose
findGitRootgit.tsWalk up directory tree to find git root; memoized with LRU(50)
getGitStategit.tsReturns comprehensive GitRepoState: commitHash, branch, remoteUrl, isClean, worktreeCount
normalizeGitRemoteUrlgit.tsNormalizes SSH/HTTPS URLs to canonical host/owner/repo; handles CCR proxy URLs
GitFileWatchergitFilesystem.tsPrivate class watching .git/HEAD, config, refs; caches derived values with dirty-flag invalidation
resolveGitDirgitFilesystem.tsResolves actual .git directory; handles worktrees where .git is a file with gitdir: pointer
readGitHeadgitFilesystem.tsParses .git/HEAD: returns { type: 'branch', name } or { type: 'detached', sha }
resolveRefgitFilesystem.tsResolves git refs to commit SHAs; checks loose files then packed-refs
fetchGitDiffgitDiff.tsTwo-phase diff: quick --shortstat probe, then per-file numstat with memory limits
fetchSingleFileGitDiffgitDiff.tsPR-style diff for one file against merge-base with default branch
parseGitConfigValuegitConfigParser.tsParses .git/config with full git spec compliance (quoting, escapes, sections)
isPathGitignoredgitignore.tsChecks if a path is ignored via git check-ignore
isSafeRefNamegitFilesystem.tsValidates ref names against path traversal and argument injection
shouldIncludeGitInstructionsgitSettings.tsSettings bridge: checks env var and settings (isolated to avoid circular deps)

Data Structures

The git module defines several key types for representing repository state, diff results, and file status. These types are consumed by the UI layer, sandbox, and conversation engine.

TypeNameDescription
typeGitRepoState{ commitHash, branchName, remoteUrl, isHeadOnRemote, isClean, worktreeCount }
typeGitFileStatus{ tracked: string[], untracked: string[] }
typeGitDiffResult{ stats: GitDiffStats, perFileStats: Map, hunks: Map }
typeToolUseDiff{ filename, status, additions, deletions, changes, patch, repository }
typeNumstatResult{ stats: GitDiffStats, perFileStats: Map<string, PerFileStats> }
typeCacheEntry<T>Internal cache entry with value, dirty flag, and compute function

Interactions

The git utilities serve as a foundational layer consumed by many other modules including the sandbox, permissions, conversation engine, and UI components.

DirectionPartnerInterfaceDescription
←.git/ filesystemfs.readFile / fs.watchFileDirect filesystem reads for HEAD, refs, config without spawning git
→git executableexecFileNoThrowSpawns git for complex operations (diff, check-ignore, log)
→SandboxgetGitState()Provides repo state for sandbox worktree path resolution
→ConversationfetchGitDiff()Generates diff stats shown in the UI status bar
→Permissionsgitignore.tsAdds patterns to global gitignore for sensitive files
←SettingsgitSettings.tsReads CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS env/settings

Caching Strategy

The GitFileWatcher class implements a dirty-flag caching pattern that watches .git/ files with fs.watchFile(). When a watched file changes, the corresponding cache entry is marked dirty rather than immediately recomputed. The next read triggers recomputation, avoiding wasted work when files change multiple times between reads.

sequenceDiagram
  participant C as Caller
  participant GFW as GitFileWatcher
  participant FS as .git/ Files
  participant Cache as LRU Cache (50)

  C->>GFW: getCachedBranch()
  GFW->>Cache: Check dirty flag
  alt Cache clean
    Cache-->>GFW: Cached value
    GFW-->>C: Branch name
  else Cache dirty (file changed)
    GFW->>FS: readFile(.git/HEAD)
    FS-->>GFW: ref: refs/heads/main
    GFW->>Cache: Update value, clear dirty
    GFW-->>C: "main"
  end
  Note over GFW,FS: fs.watchFile(1000ms) sets dirty flag on change

Configuration

The git utilities use timeouts, cache sizes, and diff limits to balance performance against completeness. Most of these are internal constants, but CLAUDE_CODE_BASE_REF is user-configurable via environment variable.

Knob / ParameterDefaultDescription
LRU_CACHE_MAX50Maximum entries in findGitRoot memoization cache
WATCH_INTERVAL1000 msfs.watchFile polling interval (10 ms in tests)
GIT_TIMEOUT_MS5000Timeout for git subprocess commands
SINGLE_FILE_DIFF_TIMEOUT_MS3000Timeout for single-file PR-style diff
MAX_FILES50Maximum files included in diff output
MAX_DIFF_SIZE_BYTES1,000,000Skip files larger than 1 MB in diff
MAX_LINES_PER_FILE400GitHub-style auto-load limit per file
CLAUDE_CODE_BASE_REFautoEnv var overriding merge-base for PR-style diffs

Directory Map

The git source files are split between the utils/ root and a utils/git/ subdirectory. The split isolates filesystem-level operations from high-level utilities and prevents circular dependencies with the settings module.

Source tree for git
utils/
├── git.ts                      ← High-level API: findGitRoot, getGitState, normalizeGitRemoteUrl
├── gitDiff.ts                  ← Diff utilities: fetchGitDiff, fetchSingleFileGitDiff, parseGitNumstat
├── gitSettings.ts              ← Settings bridge: shouldIncludeGitInstructions (avoids circular dep)
└── git/
    ├── gitConfigParser.ts      ← .git/config parser with full git spec compliance
    ├── gitFilesystem.ts        ← Filesystem-based reads: HEAD, refs, worktrees, GitFileWatcher cache
    └── gitignore.ts            ← Gitignore check and management: isPathGitignored, addFileGlobRule

Deep-Dive Highlights

Core mechanisms from this sub-module have been extracted into dedicated focus pages for in-depth exploration. Each page covers one algorithm, protocol, or subsystem with diagrams, code walks, and configuration details.

Deep-Dive Pages

Filesystem-Based Git Caching Focus

GitFileWatcher, dirty-flag invalidation, LRU bounds, and how reading .git/ files directly avoids subprocess overhead.

Extracted topic page

Two-Phase Diff Engine Focus

Quick --shortstat probe, per-file numstat with memory limits, PR-style merge-base diffs, and synthetic untracked diffs.

Extracted topic page

Worktree & Submodule Support Focus

How gitdir: pointers, commondir resolution, and shared ref lookup enable multi-worktree repositories.

Extracted topic page

Security Considerations

The git utilities implement multiple security measures to prevent injection attacks and protect repository integrity when processing untrusted data from .git/ files. All external inputs (ref names, SHAs, URLs) are validated before use in filesystem operations or subprocess commands.

Ref Name Validation

All ref and branch names read from .git/ are validated by isSafeRefName() before use in any filesystem or subprocess operations. This prevents path traversal attacks (e.g., ../../etc/passwd) and argument injection (e.g., --upload-pack=evil). Only alphanumeric characters, /, ., _, +, -, and @ are allowed.

SHA Validation

isValidGitSha() accepts only 40-character hex strings (SHA-1) or 64-character hex strings (SHA-256). This strict validation prevents injection of arbitrary content into git commands that accept SHAs as arguments, ensuring that only legitimate commit identifiers can flow through the system.

URL Normalization

normalizeGitRemoteUrl() handles CCR (Code Collaboration Relay) proxy URLs in addition to standard SSH and HTTPS formats. The normalized form (host/owner/repo) is used for hashing to create anonymous repository identifiers that don't leak the actual URL. This normalization is critical for telemetry and analytics where repository identity must be consistent regardless of the access protocol used.

Worktree Support

The git utilities fully support git worktrees, which create multiple working trees from a single repository. This is critical for Claude Code users who work on multiple branches simultaneously. The worktree detection system correctly handles the .git file (not directory) that worktrees use, and resolves shared refs and config through the commondir mechanism.

FunctionWorktree Behavior
resolveGitDir()Reads .git file (not directory) containing gitdir: path pointer
getCommonDir()Reads commondir file to find shared git directory for refs/objects
findCanonicalGitRoot()Follows worktree chain back to the main repository root
getWorktreeCountFromFs()Counts .git/worktrees/ entries without spawning git
readWorktreeHeadSha()Reads HEAD for a specific worktree directory

Diff Engine Limits

The diff engine applies multiple limits to prevent memory exhaustion and timeouts when dealing with large repositories or massive changesets. These limits use a V8-aware strategy: '' + line is used instead of line.slice() to break sliced string references and allow garbage collection of the underlying large buffer.

LimitValuePurpose
Max files50Truncate diff to first 50 files
Max file size1 MBSkip files larger than 1 MB
Max lines per file400Truncate hunks beyond GitHub's auto-load limit
Max files for details500Skip per-file stats if too many files changed
Git timeout5 secondsKill git subprocess if it hangs
Single file timeout3 secondsShorter timeout for individual file diffs

Code Examples

Example: Reading HEAD Without Subprocess

// Filesystem-based HEAD reading — no git subprocess needed
export async function readGitHead(
  gitDir: string,
): Promise<{ type: 'branch'; name: string } | { type: 'detached'; sha: string } | null> {
  try {
    const content = await readFile(join(gitDir, 'HEAD'), 'utf8');
    const trimmed = content.trim();

    // Symbolic ref: "ref: refs/heads/main"
    if (trimmed.startsWith('ref: ')) {
      const ref = trimmed.slice(5);
      const branchPrefix = 'refs/heads/';
      if (ref.startsWith(branchPrefix)) {
        return { type: 'branch', name: ref.slice(branchPrefix.length) };
      }
    }

    // Detached HEAD: raw SHA
    if (isValidGitSha(trimmed)) {
      return { type: 'detached', sha: trimmed };
    }

    return null;
  } catch {
    return null;
  }
}

Example: Normalizing Remote URLs

// Normalizes SSH and HTTPS remote URLs to canonical form
export function normalizeGitRemoteUrl(url: string): string | null {
  // SSH: git@github.com:owner/repo.git
  const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
  if (sshMatch) {
    return `${sshMatch[1]}/${sshMatch[2]}`;
  }

  // HTTPS: https://github.com/owner/repo.git
  try {
    const parsed = new URL(url);
    const path = parsed.pathname.replace(/\.git$/, '').replace(/^\//, '');
    return `${parsed.host}/${path}`;
  } catch {
    return null;
  }
}

// Result: "github.com/anthropics/claude-code"

Example: Two-Phase Diff with Quick Probe

// fetchGitDiff uses a two-phase approach for efficiency:
// Phase 1: Quick --shortstat probe (O(1) memory)
// Phase 2: Per-file --numstat only if probe passes limits

export async function fetchGitDiff(): Promise<GitDiffResult | null> {
  if (!await getIsGit()) return null;
  if (await isInTransientGitState()) return null;

  // Phase 1: Quick probe
  const shortstat = await execFileNoThrow(gitExe(), [
    'diff', 'HEAD', '--shortstat',
  ], { timeout: GIT_TIMEOUT_MS });

  const quickStats = parseShortstat(shortstat.stdout);
  if (quickStats && quickStats.filesCount > MAX_FILES_FOR_DETAILS) {
    return { stats: quickStats, perFileStats: new Map(), hunks: new Map() };
  }

  // Phase 2: Detailed numstat
  const numstat = await execFileNoThrow(gitExe(), [
    'diff', 'HEAD', '--numstat', '--no-renames',
  ], { timeout: GIT_TIMEOUT_MS });

  return parseGitNumstat(numstat.stdout);
}