# File History System Deep Dive

The file history system is Claude Code's "undo button"—when the AI modifies code and the result isn't right, a single `/rewind` command precisely rolls back the file state to any point after a given conversation turn. The system's most distinctive design decision is this: **it organizes snapshots by conversation turn (not by individual file edit)**, allowing users to rewind at the granularity of "my Nth exchange with the AI" rather than hunting through scattered file edits. The underlying implementation follows industry-standard engineering practices—SHA256 path hashing, mtime deduplication, hard-link reuse, and FIFO eviction—but the choice of snapshot granularity represents an architectural decision truly worth examining. This chapter parses the complete snapshot lifecycle, storage naming strategy, cleanup mechanics under the 100-snapshot limit, and the deeper trade-offs between this independent snapshot approach and a git-integrated alternative.

> **Source locations**: `src/utils/fileHistory/`, `src/utils/fsOperations/`

> 💡 **Plain English**: The file history system is like an autosave in a video game—before each AI code change, it automatically saves a snapshot (checkpoint), and you can "load game" to return to a previous version at any time. The key difference is that checkpoints aren't created for every file change, but for every "you ask, AI answers" conversation turn—like an RPG saving at story beats instead of after every step. It keeps up to 100 saves, automatically overwriting the oldest, and it's smart enough not to re-save files that haven't changed.

### 🌍 Industry Context: Undo and Rewind Mechanisms in AI Coding Tools

How do you roll back when AI-modified code goes wrong? This is a trust problem every AI coding tool must solve, yet different products take radically different approaches:

- **Cursor**: Relies on VS Code's built-in undo/redo stack; AI edits and manual edits share the same history. Composer mode has an independent "Accept/Reject Changes" mechanism, but the granularity is per-edit, with no support for rewinding by conversation turn.
- **Aider**: Automatically `git commit`s after each AI modification; undo is just `git revert` or the `/undo` command. This approach has real advantages: git history is unlimited (not capped at 100 snapshots), searchable, and visible to the team—other members can see what the AI changed via `git log`. The cost is that git history fills with automated commits; some users find this harms readability, while others see it as the finest-grained audit trail possible.
- **CodeX (OpenAI)**: Each agent runs in an OS-level isolated environment; modifications are presented as patches, and only take effect when the user explicitly applies them. This represents a fundamentally different design philosophy—"preventive" rather than "curative." **OpenCode** chose an undo mechanism based on underlying Git restoration to ensure safe and controllable operations—representing the "version-control-first" route to file safety.
- **GitHub Copilot Workspace**: All changes happen in a cloud-based draft; the user manually selects which modifications to merge. Similar to a code review workflow.
- **JetBrains AI**: Leverages the IDE's Local History feature, recording a snapshot on every save, including changes unrelated to the AI.
- **Windsurf**: Provides Cascade's checkpoint mechanism, saving state by AI operation turn.

From a design philosophy perspective, these solutions fall into three categories:

1. **Preventive** (CodeX sandbox preview, Copilot Workspace cloud drafts, Google Antigravity Artifacts pre-review)—changes don't take effect until user approval, so no rewind is needed.
2. **Integrated** (Aider's git auto-commit)—leverages existing version-control infrastructure; rewind history is unlimited and team-visible, but coupled with git history.
3. **Independent** (Claude Code's snapshot system, Windsurf's checkpoint)—a lightweight mechanism independent of git; doesn't pollute history but has capacity limits.

Claude Code chose the independent route. The core design decision is **organizing snapshots by conversation turn**—supporting precise rewinds to the file state "after the Nth conversation turn." This is more precise than Cursor's undo stack (can roll back across multiple edits at once), but compared to Aider's git-integrated approach, it suffers from a snapshot ceiling (100) and lack of team visibility.

---

## Overview

When the AI modifies your code and you want to revert, the file history system lets you do so without `git stash` or `git checkout`—a single `/rewind` command precisely restores the file state to any point after a given conversation turn. The system's core design organizes snapshots by conversation turn: it automatically tracks the original file content before each Edit/Write operation, then bundles that turn's modifications into a single snapshot after every assistant message. The underlying layer avoids redundant backups via SHA256 path hashing and mtime deduplication, uses hard-links for cross-session reuse, and caps the total at 100 snapshots with a FIFO policy.

---

> **[Chart placeholder 3.11-A]**: Snapshot lifecycle diagram — the full chain from trackEdit (before) → makeSnapshot (after) → getDiffStats (inspect) → rewind (restore)

---

## Core Design Decision: Organizing Snapshots by Conversation Turn

This is the most noteworthy architectural decision in the entire file history system—**the snapshot granularity is the conversation turn, not the file edit.**

In a single AI coding conversation, the user might say "help me refactor this module," and the AI could modify 5–10 files in succession. If snapshots were created per file edit (like Cursor), the user would face a scattered list of file modifications on rewind—"main.ts was changed," "utils.ts was changed"—and would have to manually figure out which edits belonged to the same turn. By creating snapshots per conversation turn, Claude Code presents coherent semantic units like "Turn 3 (AI modified 5 files)," and a single rewind rolls back all changes from that turn.

**Why are turns more suitable than edits for AI coding?**

The user's mental model is "conversation," not "edit." When users want to undo, they remember "the thing I asked the AI to do in the third turn was wrong," not "the seventh file write needs to be reverted." Organizing by turn aligns the rewind operation with the user's cognitive granularity.

**Technical implementation**: `fileHistoryMakeSnapshot()` is called at three entry points (REPL.tsx, QueryEngine.ts, handlePromptSubmit.ts) after every assistant message, bundling all tracked file modifications from that turn into a single snapshot keyed by `messageId`. This means a snapshot may contain anywhere from zero (a turn where the AI didn't modify files) to dozens (a large-scale refactor) of file backups.

**Edge case**: When a single turn modifies a large number of files (say, 50), the snapshot contains backups for all of them. The rewind is all-or-nothing—you can't revert only 3 of those files. This is reasonable in most scenarios (users usually want to undo the entire "request"), but may lack flexibility in rare cases.

**Competitor comparison**: Windsurf's checkpoint uses a similar turn-level granularity, but Aider's per-edit commit offers finer-grained control—users can precisely `git revert` a single file edit. This is the classic trade-off between "semantic intuition" and "precise control."

> 💡 **Plain English**: Imagine hiring a contractor to renovate a room. Saving per edit is like taking a photo every time a brick is moved—when you look back, you have no idea which photos belong to "demolishing the wall" and which to "installing the lights." Saving per turn is like taking a photo after each request you made—"wall demolished," "lights installed"—so rewinding is crystal clear.

---

## 1. Snapshot Lifecycle

### 1.1 Creation (Pre-edit Tracking)

`fileHistoryTrackEdit()` (`fileHistory.ts:86-193`) is called **before** an Edit/Write tool modifies a file. It uses a three-phase design:

1. **Deduplication short-circuit** (lines 99-118): If the file is already tracked in the most recent snapshot and its content hasn't changed, return immediately—a standard dedup short-circuit pattern that skips subsequent work when conditions aren't met.
2. **Asynchronous backup** (lines 120-128): Captures the pre-edit content via `createBackup()`.
3. **Race-condition recheck** (lines 131-192): Phase 2 is async, so concurrent modifications may occur (multiple tool calls running in parallel or the user editing manually at the same time). Before committing, the system re-verifies whether the file state is still consistent. If inconsistency is detected, it recreates the backup based on the latest state.

**Idempotency guarantee**: Backups are keyed by content hash, so multiple `trackEdit()` calls for the same file reuse the existing backup.

### 1.2 Storage (Periodic Snapshots)

`fileHistoryMakeSnapshot()` (`fileHistory.ts:195-342`) is called after every assistant message (at the three entry points in REPL.tsx, QueryEngine.ts, and handlePromptSubmit.ts).

Snapshot structure (lines 299-303):
```typescript
{
  messageId: UUID,           // The message ID this snapshot corresponds to
  trackedFileBackups: {...}, // Tracked path → FileHistoryBackup mapping
  timestamp: Date            // Snapshot timestamp
}
```

Three layers of optimization prevent redundant backups:

1. **stat-before-content** (lines 233-239): `stat()` the file first; ENOENT means it was deleted, avoiding reads of nonexistent files.
2. **mtime deduplication** (lines 257-269): If the file's mtime is earlier than the latest backup's mtime, the file wasn't modified—reuse the latest version without creating a new backup.
3. **Version increment** (lines 272-275): Each tracked file has an independent v1, v2, v3... counter that only increments when the content actually changes.

### 1.3 Query (Pre-rewind Preview)

`fileHistoryGetDiffStats()` (`fileHistory.ts:413-483`) is called by `MessageSelector.tsx` to show the user a change summary before confirming a rewind:

- Uses `diffLines()` from the npm `diff` library to compute line-level diffs.
- Returns `DiffStats`: list of changed files + inserted/deleted line counts.

`fileHistoryHasAnyChanges()` (`fileHistory.ts:494-531`) is a lightweight variant—it returns only a boolean and early-exits at the first changed file. Used for UI decisions like "should we show the rewind option?"

### 1.4 Restore (Rewind Execution)

`fileHistoryRewind()` (`fileHistory.ts:347-397`):

```
Find target snapshot by messageId (findLast)
  → applySnapshot()
    → For each tracked file:
       ├── Backup is null (file didn't exist originally) → unlink() to delete
       └── Backup exists → restoreBackup() (only executes if content changed)
  → Record tengu_file_history_rewind_success analytics event
```

## 2. Storage and Naming

### 2.1 Directory Structure

```
~/.claude/file-history/
├── {sessionId1}/
│   ├── a1b2c3d4e5f6g7h8@v1    ← First version of /Users/.../main.ts
│   ├── a1b2c3d4e5f6g7h8@v2    ← Second version of the same file
│   ├── 9z8y7x6w5v4u3t2s@v1    ← Another file
│   └── ...
└── {sessionId2}/
    └── ...
```

### 2.2 Backup File Naming

> **Note**: Path hash + version suffix is a standard file naming strategy—hashing collapses long paths into short filenames for O(1) lookup. Git object storage (SHA1-named), Docker layer caching, and npm cache all use similar strategies. The version suffix `@v1, @v2...` lets multiple versions of the same file coexist. No need to over-analogize this to database concepts—it's simply a common hash-mapping-plus-versioning scheme, clean and effective in practice.

`getBackupFileName()` (`fileHistory.ts:725-730`):

```typescript
const fileNameHash = createHash('sha256')
  .update(filePath)              // Hash the full absolute path
  .digest('hex')
  .slice(0, 16)                  // Take first 16 hex characters (64 bits)
return `${fileNameHash}@v${version}`
```

**Why hash the path instead of the content?**
- Path-to-backup-filename is an O(1) computation—given a path, you immediately know where to look for backups.
- No runtime content comparison is needed to generate the filename.
- 16 characters (64 bits) means you'd need about 2^32 (4.3 billion) distinct paths before a 50% collision probability under the birthday paradox, which is astronomically low for a single session. However, note that **the consequence of a collision is severe**: if two different paths produce the same hash prefix, their backup files would silently overwrite each other—causing a rewind to restore the wrong file's content, with no warning to the user. The source code contains no collision detection mechanism (e.g., checking whether an existing backup file belongs to the same path before writing). Although the probability is vanishingly low, for a safety-critical feature like code rollback, this risk of silent data corruption is worth understanding.

## 3. MAX_SNAPSHOTS and Cleanup

### 3.1 The Limit

`const MAX_SNAPSHOTS = 100` (`fileHistory.ts:54`)

Cleanup strategy (lines 305-311):
```typescript
allSnapshots.length > MAX_SNAPSHOTS
  ? allSnapshots.slice(-MAX_SNAPSHOTS)  // Keep the latest 100
  : allSnapshots
```

**FIFO cleanup**: When snapshots exceed 100, the oldest are automatically removed.

### 3.2 Sequence Counter

`snapshotSequence` (line 312) continues to increment even after old snapshots are cleared—used as an activity signal for `useGitDiffStats`. This is a "logical clock" design: the sequence monotonically increases and never rolls back.

## 4. Hard-Link Cross-Session Reuse

`fileHistory.ts:978-1010`: When resuming a session, the system uses `link()` to create a hard-link rather than `copyFile()`:

- **Zero additional disk overhead**: hard-links share the same inode.
- **Graceful fallback**: If `link()` fails (e.g., cross-filesystem), it falls back to `copyFile()`.
- **Applicable scenario**: User closes the terminal and later reopens Claude Code to continue the previous session.

## 5. Crash Recovery and Metadata Persistence

A "deep dive" can't cover only the happy path—how the system behaves on abnormal exit is equally important.

**Backup files vs. snapshot metadata**: The file history system has two data layers. The first is the backup files on disk (`~/.claude/file-history/{sessionId}/{hash}@v{n}`), which are persistent. The second is snapshot metadata—the mapping of "which messageId corresponds to which versions of which files"—stored in the in-memory `FileHistoryState` object.

**Consequences of a crash**: If the Claude Code process exits abnormally (crash, terminal force-closed, power loss), the backup files remain on disk, but the in-memory snapshot metadata is lost. This means:

1. Backup files on disk are intact, but the system doesn't know which backup belongs to which conversation turn.
2. On session resume, `restoreFileHistory()` (`fileHistory.ts:978-1010`) reuses backup files from the old session via hard-link, but if metadata is lost, the snapshot list is empty—users cannot `/rewind` to a pre-crash conversation state.
3. There is no independent metadata persistence mechanism (no JSON/SQLite written to disk).

**Mid-rewind failure**: `fileHistoryRewind()` restores each tracked file one by one. If an error occurs mid-restore (disk full, permission denied), already-restored files are not rolled back—the file state ends up in a partially restored, inconsistent condition. The system provides no transactional guarantee or rollback mechanism.

This is a structural weakness of the independent snapshot approach compared to git integration: git's reflog and object storage provide natural persistence and crash recovery, whereas in-memory snapshot metadata is fragile in the face of process crashes.

---

## 6. Integration Points

### 6.1 Tool Integration

- **Edit tool** (`FileEditTool.ts:435-438`): Calls `fileHistoryTrackEdit()` before modifying.
- **Write tool** (`FileWriteTool.ts:259-261`): Calls `fileHistoryTrackEdit()` before writing.
- **NotebookEdit**: Similar pattern.

### 6.2 Message Loop Integration

- **REPL.tsx** (lines 3092-3099): Calls `fileHistoryMakeSnapshot()` after every assistant message.
- **QueryEngine.ts** (lines 645-650): Same as above.
- **handlePromptSubmit.ts** (lines 528-533): Same as above.

### 6.3 UI Integration

- **MessageSelector.tsx** (lines 77, 173): Calls `fileHistoryGetDiffStats()` to display rewind preview.
- **`/rewind` command**: Calls `fileHistoryRewind()`.

## 7. Design Trade-offs and Assessment

### Genuine Highlights

1. **Organizing snapshots by conversation turn**—this is the system's most important design decision (see the Core Design Decision section above). It aligns rewind granularity with the user's mental model.
2. **`snapshotSequence` as a logical clock**—even when snapshots are FIFO-evicted, the sequence never rolls back, preventing UI components (the `useGitDiffStats` hook) from triggering unnecessary re-renders due to sequence regression. This is a refined consideration for React state management.
3. **Query separation via `fileHistoryHasAnyChanges()`**—when you only need to know "are there any changes?" to decide whether to show a rewind button, you don't need to compute the full diff. Replacing expensive full-diff computation with a cheap early-exit boolean query is a valuable "separate query strategy" for AI Agent UI responsiveness optimization.

### Standard Engineering Practices (Solid Implementation, but Not Over-Praised)

The following are correct engineering choices, but they are industry-standard practices:

- **stat-before-content optimization**: `stat()` before read is standard in almost any program that conditionally reads files (rsync, make, build tools).
- **O(1) path-hash lookup**: SHA256 path hashing is a common file naming strategy.
- **Hard-link cross-session reuse**: This is a standard Unix filesystem technique. Worth noting the fallback to `copyFile()` for fault tolerance.
- **mtime deduplication**: Avoids creating redundant backups for unmodified files.
- **FIFO eviction**: Simple cleanup policy for the 100-snapshot cap.

### Costs and Risks

1. **Silent consequences of path-hash collisions**: Although 64-bit hash collision probability is vanishingly low, a collision silently overwrites backups with no detection mechanism (see Section 2.2 discussion).
2. **Snapshot metadata is not persisted**: After a process crash, snapshot mappings are lost; backup files remain but cannot be associated with conversation turns (see Section 5).
3. **Rewind lacks transactional guarantee**: If restoration fails midway, file state becomes inconsistent.
4. **Path hashing means file renames are treated as "new files"**—history before the rename is not associated with the new path.
5. **mtime deduplication relies on filesystem timestamp precision**—not only can network filesystems like NFS be unreliable, but local filesystems vary in precision (ext4 is nanosecond, HFS+ is second, FAT32 is two-second). If two modifications occur within the same mtime window (user manual edit + nearly simultaneous AI edit), mtime deduplication may incorrectly conclude the file is unmodified, causing snapshot loss.
6. **100-snapshot cap may be insufficient for long sessions**—early edits become unreachable, and there's no tiered storage strategy (e.g., keeping full detail for the latest 50 and only key snapshots at exponentially decaying intervals for older ones).
7. **Coupling risk across three entry points**—`makeSnapshot()` must be manually invoked in REPL.tsx, QueryEngine.ts, and handlePromptSubmit.ts. If a new entry point is added in the future (e.g., API mode), developers must remember to insert the snapshot call; otherwise, modifications from that entry become non-rewindable.
8. **Hard-links don't work across filesystems**—if `~/.claude` and the project are on different partitions, the fallback to `copyFile()` increases disk usage.

### Why Not Git Integration?

This is the most critical architectural question in the entire design, and it deserves serious discussion.

Aider has already proven that a git auto-commit scheme works; git worktrees can preserve history without polluting the main branch, and git stash can hold temporary state. So why did Claude Code choose to implement its own independent system?

**The real reasons for the independent approach** (not just "doesn't pollute history"):

1. **Git-less scenarios**: Users may run Claude Code in a directory without `git init`; the independent scheme doesn't depend on git being present.
2. **Performance overhead of git operations**: Running `git add` + `git commit` after every AI modification may introduce perceptible latency in large repositories, especially monorepos with many files.
3. **Git lock contention**: If the user is simultaneously running git operations in another terminal (rebase, merge), an automatic commit may fail due to `.git/index.lock`.
4. **Implementation complexity**: Edge cases like `.gitignore` rules, submodules, and shallow clones would need to be handled.

**The costs of the independent approach** (which must be honestly acknowledged):

1. **Team invisibility**: `~/.claude/file-history/` is purely local; team members cannot see what the AI changed via `git log`. Aider's approach has a natural advantage in collaborative scenarios.
2. **Limited rewind depth**: 100 snapshots vs. git's unlimited history.
3. **Crash fragility**: Snapshot metadata is not persisted (see Section 5), whereas git's reflog and object storage are naturally crash-recoverable.
4. **Not searchable**: You can't search historical modifications like `git log -S "function_name"`.

This is a reasonable engineering trade-off—broader applicability (no git dependency) and lower implementation risk (no conflict with user git operations) are purchased at the cost of persistence and visibility.

---

*Quality self-check:*
- [x] Coverage: Complete lifecycle (create → store → query → restore) + storage format + cleanup + hard-links + crash recovery + integration points
- [x] Fidelity: All line numbers come from deep agent reads of fileHistory.ts
- [x] Depth: In-depth analysis of turn-level granularity design decision, crash recovery and metadata persistence discussion, and serious argumentation of git integration vs. independent approach
- [x] Criticality: Silent consequences of hash collisions, multi-filesystem mtime precision discussion, three-entry-point coupling risk, and fair comparison with Aider's approach
- [x] Calibration: Standard engineering practices (SHA256/FIFO/hard-link/stat-before-content) are labeled as standard practice rather than innovation
