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.
📚 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 / Function | File | Purpose |
|---|---|---|
findGitRoot | git.ts | Walk up directory tree to find git root; memoized with LRU(50) |
getGitState | git.ts | Returns comprehensive GitRepoState: commitHash, branch, remoteUrl, isClean, worktreeCount |
normalizeGitRemoteUrl | git.ts | Normalizes SSH/HTTPS URLs to canonical host/owner/repo; handles CCR proxy URLs |
GitFileWatcher | gitFilesystem.ts | Private class watching .git/HEAD, config, refs; caches derived values with dirty-flag invalidation |
resolveGitDir | gitFilesystem.ts | Resolves actual .git directory; handles worktrees where .git is a file with gitdir: pointer |
readGitHead | gitFilesystem.ts | Parses .git/HEAD: returns { type: 'branch', name } or { type: 'detached', sha } |
resolveRef | gitFilesystem.ts | Resolves git refs to commit SHAs; checks loose files then packed-refs |
fetchGitDiff | gitDiff.ts | Two-phase diff: quick --shortstat probe, then per-file numstat with memory limits |
fetchSingleFileGitDiff | gitDiff.ts | PR-style diff for one file against merge-base with default branch |
parseGitConfigValue | gitConfigParser.ts | Parses .git/config with full git spec compliance (quoting, escapes, sections) |
isPathGitignored | gitignore.ts | Checks if a path is ignored via git check-ignore |
isSafeRefName | gitFilesystem.ts | Validates ref names against path traversal and argument injection |
shouldIncludeGitInstructions | gitSettings.ts | Settings 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.
| Type | Name | Description |
|---|---|---|
| type | GitRepoState | { commitHash, branchName, remoteUrl, isHeadOnRemote, isClean, worktreeCount } |
| type | GitFileStatus | { tracked: string[], untracked: string[] } |
| type | GitDiffResult | { stats: GitDiffStats, perFileStats: Map, hunks: Map } |
| type | ToolUseDiff | { filename, status, additions, deletions, changes, patch, repository } |
| type | NumstatResult | { stats: GitDiffStats, perFileStats: Map<string, PerFileStats> } |
| type | CacheEntry<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.
| Direction | Partner | Interface | Description |
|---|---|---|---|
| ← | .git/ filesystem | fs.readFile / fs.watchFile | Direct filesystem reads for HEAD, refs, config without spawning git |
| → | git executable | execFileNoThrow | Spawns git for complex operations (diff, check-ignore, log) |
| → | Sandbox | getGitState() | Provides repo state for sandbox worktree path resolution |
| → | Conversation | fetchGitDiff() | Generates diff stats shown in the UI status bar |
| → | Permissions | gitignore.ts | Adds patterns to global gitignore for sensitive files |
| ← | Settings | gitSettings.ts | Reads 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 / Parameter | Default | Description |
|---|---|---|
LRU_CACHE_MAX | 50 | Maximum entries in findGitRoot memoization cache |
WATCH_INTERVAL | 1000 ms | fs.watchFile polling interval (10 ms in tests) |
GIT_TIMEOUT_MS | 5000 | Timeout for git subprocess commands |
SINGLE_FILE_DIFF_TIMEOUT_MS | 3000 | Timeout for single-file PR-style diff |
MAX_FILES | 50 | Maximum files included in diff output |
MAX_DIFF_SIZE_BYTES | 1,000,000 | Skip files larger than 1 MB in diff |
MAX_LINES_PER_FILE | 400 | GitHub-style auto-load limit per file |
CLAUDE_CODE_BASE_REF | auto | Env 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.
Two-Phase Diff Engine Focus
Quick --shortstat probe, per-file numstat with memory limits, PR-style merge-base diffs, and synthetic untracked diffs.
Worktree & Submodule Support Focus
How gitdir: pointers, commondir resolution, and shared ref lookup enable multi-worktree repositories.
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.
| Function | Worktree 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.
| Limit | Value | Purpose |
|---|---|---|
| Max files | 50 | Truncate diff to first 50 files |
| Max file size | 1 MB | Skip files larger than 1 MB |
| Max lines per file | 400 | Truncate hunks beyond GitHub's auto-load limit |
| Max files for details | 500 | Skip per-file stats if too many files changed |
| Git timeout | 5 seconds | Kill git subprocess if it hangs |
| Single file timeout | 3 seconds | Shorter 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);
}