> ## Content Index
> Fetch the complete content index at: https://varops.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Git is the wrong granularity for agents. Here's what we built around it.
- URL: https://varops.com/git-is-the-wrong-granularity-for-agents-heres-what-we-built-around-it/
- Published: 2026-10-19T17:49:00.000Z
- Updated: 2026-10-19T19:01:51.000Z
- Description: Two layers Git doesn't have are checkpoints below the commit and provenance above it. ghf captures every agent edit as restorable worktree state, and GitStory binds cited rationale to the commit that landed. Neither writes anything into Git.
- Author: Ran Aroussi
- Tags: Founder Mode

**TL;DR:** Coding agents make hundreds of edits between two commits, and the reasons behind the final code mostly live in chat sessions nobody rereads. I built two small tools that sit beside Git without touching it:

- [**ghf**](https://ghf.sh/?ref=varops.com) checkpoints the worktree after every agent edit, so any agent on any machine can restore, diff**,** or fork that state.
- [**GitStory**](https://gitstory.sh/?ref=varops.com) turns the evidence of the work into a short, cited story attached to the commit that landed, so the next agent can ask *why* before it changes anything.

> **Git preserves what changed. ghf captures how the work evolved. GitStory preserves why it landed.**

Both are early previews. ghf is open source (Apache 2.0) and at 0.1.0-rc.2\. GitStory is in private preview.

---

## The thesis

Git is one of the best pieces of software ever written, and nothing here argues otherwise. But Git encodes an assumption: **a commit is a human-sized decision.** A person works for a while, decides the result is worth keeping, writes a message and commits. Everything between two commits is the developer's own business, held in their editor, their head, maybe a stash.

Agents break that assumption in two directions at once.

**Below the commit, there's too much.** An agent working on one task passes through dozens of complete, meaningful states: the last green build before a bad refactor, an approach that almost worked, an attempt another model was making in parallel. Then the commit lands and every one of them is gone. Harness undo helps a little, but it's tied to one session on one machine.

**Above the commit, there's too little.** The commit says *what* changed. The diff says *how*. Neither says *why this approach*, *what was tried first*, or *which constraint ruled out the obvious fix*. Humans paper over this with tribal memory. Agents don't have any. A fresh agent sees code that looks odd, "fixes" it, and walks straight back into the bug the odd code was preventing.

So the thesis is simple: **agent-driven development needs two layers Git doesn't have, and both should live beside Git, never inside it.**

```mermaid
flowchart TB
    subgraph why["GitStory · why it landed"]
        direction LR
        S1["Story for commit B: cited decisions, rejected options, unknowns"]
    end
    subgraph what["Git · what changed"]
        direction LR
        C1((commit A)) --> C2((commit B))
    end
    subgraph how["ghf · how it evolved between A and B"]
        direction LR
        K1[ck1] --> K2[ck2] --> K3[ck3] --> K4["ck4 · tests=pass"] --> K5[ck5] --> K6[ck6]
        K3 -.fork.-> F1[ck7] --> F2[ck8]
    end
    how -. "lands as" .-> what
    what -. "explained by" .-> why

```

Three layers, three lifecycles:

| Layer        | Question it answers                                   | Granularity                    | Lifetime                           |
| ------------ | ----------------------------------------------------- | ------------------------------ | ---------------------------------- |
| **Git**      | What changed?                                         | A decision worth keeping       | Forever                            |
| **ghf**      | What state was the code in, and can I get back to it? | Every agent edit               | Until the work lands, plus 30 days |
| **GitStory** | Why is the code this way?                             | A few claims per landed change | As long as the code matters        |

That last column is the real reason these are separate tools. Working history is bulky, short-lived and mostly uninteresting once the work lands. Engineering memory is tiny, precious and should outlive everything. Put both in one system and one of them ends up with the wrong lifecycle.

---

## Why not just put it in Git?

It's the first question everyone asks, and it deserves a straight answer.

You *could* commit after every edit, push checkpoints to a hidden ref, or stuff reasons into commit trailers or `git notes`. I deliberately didn't, for three reasons:

1. **Accepted history should stay accepted.** WIP commits mix "what we tried" with "what we decided." Hidden refs get pushed, mirrored, cloned and garbage-collected by tools that don't know they're special. The moment working history lives in Git, every Git tool has to understand it.
2. **They need different rules.** Checkpoints need their own retention (prune after landing), their own permissions (a read-only token for a reviewer) and their own sync (background, batched, lazy). Stories need their own store, export and search. Git's model fits neither.
3. **Adoption.** A teammate who has never heard of ghf should clone the repo and see *exactly* what they'd see without it: same commits, same messages, same refs. That's what makes it safe to try on a real codebase.

So both tools follow the same hard rule: **nothing is ever written into Git.** No commits, trailers, notes, refs or objects. ghf keeps its store under `.git/ghf` and GitStory under `.git/story`, and both only *read* Git. The acceptance tests check this directly: Git's refs, log and notes are identical before and after a full run.

---

## ghf: a working history for every edit

ghf (Git High Frequency) saves the full state of the worktree every time an agent finishes an edit. Claude Code and Cursor hooks call it automatically. A checkpoint is a complete, independently restorable snapshot, stored as content-addressed objects (BLAKE3, zstd) in a local SQLite store and synced in the background to a small self-hosted server.

```mermaid
sequenceDiagram
    participant A as Agent (Claude Code)
    participant G as git hf
    participant L as .git/ghf (SQLite)
    participant S as ghf-server
    participant M as Another machine
    A->>G: hook after each edit
    G->>G: scan + hash (stat cache skips unchanged files)
    G->>L: objects + checkpoint, one transaction
    G-->>S: background sync (only missing objects)
    M->>S: git hf sync
    M->>M: git hf trace restore ck_… (new worktree, verified)

```

Checkpoints are grouped into **traces**, one per unit of work. Any authorized agent can list, diff, restore, apply or fork a trace. That makes a few things easy:

- **Back to the last green state.** Tests passed, then a later step broke them. Run `git hf trace log --annotation tests=pass`, restore, done. No WIP commit, and the restore is itself recorded.
- **Hand off between machines.** Another worker picks up from the exact file state, uncommitted changes included.
- **Fork an attempt.** Two models try different approaches from the same checkpoint, each in its own worktree and trace.
- **Show a human what was tried.** When a run escalates, there's a timeline to look at, not just a final diff.

It has to be fast enough that nobody notices it. On a 2,000-file repository (release build, Apple Silicon), a checkpoint after an edit takes **about 34 ms at p95**, a checkpoint that creates a file stays **under 50 ms**, restoring on another machine takes **0.6 s**, and syncing 250 checkpoints takes about **2.3 s**.

Safety was the other non-negotiable. Restore goes into a *new* worktree by default. In-place restore and apply refuse to run if anything uncaptured would be lost. Apply is all or nothing. Before the first release candidate was tagged, an independent review found nine issues, including a crafted manifest that could make a teammate's restore write outside the worktree. All nine were fixed first.

The second release candidate closed the gaps I'd listed as known limitations: a file watcher for edits made outside the agent's tools, staged-index capture, submodules, Windows support, and a standalone `ghf` command alongside `git hf`.

---

## GitStory: memory for why

ghf answers "how did the work evolve?" It doesn't answer "why is the code like this?", and it shouldn't try. Traces are pruned weeks after landing, and nobody wants to read four hundred checkpoints to find one reason.

GitStory takes the evidence produced while the work happens and distills it into a **story**: a short record bound to the commit that actually landed.

```mermaid
flowchart LR
    subgraph local["On your machine"]
        N[git story note] --> E[(evidence events)]
        T[test results] --> E
        CC[Claude Code sessions] --> E
        GT[Git commits] --> E
        GH[ghf traces] -.optional.-> E
        E --> SC[scrub secrets<br/>cap excerpts]
    end
    SC --> GATE{{egress gate<br/>policy + trusted endpoint}}
    GATE --> SY[synthesize<br/>model]
    SY --> VA[validate<br/>separate pass]
    VA --> LD[land<br/>reconcile with landed diff]
    LD --> ST[(story store<br/>local or S3)]
    ST --> Q[blame · path · history · search]

```

A story is small on purpose. Here's a real one, lightly trimmed, produced end to end by GitStory with `gpt-6-luna` from a test repository:

> **The charge retry loop reuses one UUID idempotency key across attempts, so a retry after a timeout cannot charge twice.** 
>  
> **Decisions**Generate one idempotency key before the retry loop and pass that same key to each charge attempt. *(observed · diff)*Reuse the key across retries because the provider deduplicates requests with the same key. *(stated · note)*  
>  
> **Rejected**A Redis lock around `charge()` was tried and dropped: it can't help if the provider already processed the first request. *(stated)*  
>  
> **Unknowns**What idempotency guarantees and retention period does the provider enforce?

Six weeks later:

```text
$ git story blame billing/retry.py:5-10 --brief
st_01m5aa3y  0e841b8f  Reuse one idempotency key across charge retries

```

That one lookup is the whole point. Here's the difference it makes for an agent asked to "simplify the retry loop":

| Without GitStory                                                                              | With GitStory                                           |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Reads the code and git log. Nothing says why the key must be shared.                          | Runs git story blame before editing.                    |
| Moves key creation into the loop "so each attempt starts clean", or adds the Redis lock back. | Sees the decision and the reason behind it.             |
| Tests pass, because none covers a timeout after the provider already charged.                 | Sees the rejected lock and why it fails, and skips it.  |
| Double charges come back in production.                                                       | Takes the open question to a human instead of guessing. |

### The hard part is trust, not summarization

Any model can write a plausible paragraph about a diff. That's exactly the problem: a confident, invented reason is worse than no reason, because the next agent will treat it as a constraint. So most of GitStory is machinery for **not making things up**:

- **Every claim cites evidence.** A claim that cites nothing, or cites an event that doesn't exist, is dropped before anything is stored.
- **Claims are typed.** *Observed* means the evidence proves it (a diff, a test result). *Stated* means someone said it (a note, a commit message, the agent in-session). *Inferred* is the model's own reading. Inferred claims can only live in `hypotheses`, and the agent view (`--brief`) leaves them out entirely.
- **A separate validation pass.** A second call checks every claim against the evidence it cites and the landed diff. Claims that don't hold are dropped or demoted to hypotheses. Test claims are checked mechanically: "50/50 passed" has to match the recorded pass and fail counts.
- **Missing reasons become unknowns.** If nobody stated why, the story says so. It never fills the gap.
- **Durable on its own.** Each claim rests on the *stored* part of its evidence. Delete the session log, lose the CI link, let ghf prune the trace, and `git story validate` still passes.

### Landing, not drafting, is what counts

Drafts are written in the background while the work is in progress (a Claude Code hook or a Git post-commit hook triggers them), so nothing ever waits on a model. But drafts describe intentions, and intentions change in review.

So the story that matters is written at **landing**. After the merge, squash merge or run of commits pushed to main, `git story land` reconciles every draft claim against the code that actually landed. An approach that was in a draft but removed in review ends up in `rejected` or `discarded`, never in `decisions`.

### Memory that stays current

Decisions expire. A lock gets removed, then a later bug brings it back. If memory can't express that, it rots into a pile of contradictory reasons. GitStory links stories with `supersedes`:

```mermaid
flowchart LR
    A["A · Add a Redis lock<br/><i>retries double-charged</i>"] -->|superseded by| B["B · Remove the lock<br/><i>row versioning replaces it</i>"]
    B -->|superseded by| C["C · Bring the lock back<br/><i>versioning missed cross-row writes</i>"]
    style C stroke-width:3px

```

When an agent knowingly reverses a decision, it says so (`git story note "…" --supersedes st_…`). When landing changes lines an older story covers, GitStory uses blame to *propose* a link, and someone accepts it. Proposals never apply themselves. `blame` then follows links forward, so a line last touched by B returns both B *and* C, with C marked current.

### Privacy is a feature, not a setting

Agent transcripts are the richest evidence there is, and also the worst thing to ship anywhere: huge, full of pasted secrets, and nobody else's business. So GitStory never sends the raw log. Locally, it extracts typed events (a goal, an edit, a test run with its counts, a stated reason), scrubs keys, tokens, `.env` values and high-entropy strings, and caps each excerpt. Only then can anything leave, and only through one gate:

- `.gitstory` sets the egress policy: what may be sent (events, diffs, excerpts) and where.
- Each endpoint must be trusted once per machine.
- With `inference = local`, no request ever leaves the machine.
- Every call is recorded in an **egress manifest** stored with the story: the endpoint, the model, which events and which files' diffs were sent, and how many bytes.

---

## Better together, independent by design

The two tools answer different questions, and either one works without the other. But they're designed to meet:

```mermaid
timeline
    title One unit of work
    During the work : ghf checkpoints every edit : GitStory collects notes, tests, sessions : GitStory holds the ghf trace
    At landing : GitStory reconciles drafts with the landed diff : Cites ghf attempts and tests=pass checkpoints : Releases the hold
    Weeks later : ghf prunes the trace : The story still validates
    Months later : git story blame answers why

```

- **Checkpoints become evidence.** GitStory reads ghf traces through ghf's public CLI and cites attempts, along with each checkpoint's `tests=pass` or `tests=fail` annotation.
- **Holds keep evidence alive.** GitStory places a hold on each trace it reads so ghf won't prune it before the story lands, then releases it.
- **Stories outlive traces.** A story stores the durable part of every event it cites, so it doesn't depend on ghf keeping anything.
- **Neither needs the other.** ghf doesn't know GitStory exists, and GitStory works with plain Git.

---

## Built for agents first

The main users of both tools aren't people at a terminal. They're agents in a loop, and that shaped everything:

- Every command takes `--json` and returns one versioned envelope (`ghf/v1`, `gitstory.cli/v1`) with stable error and exit codes.
- Nothing ever prompts. Automation can't answer a prompt.
- Each tool ships an **agent skill** that teaches the habit, not just the commands. For GitStory, that means blame before changing existing code, note real decisions as you make them, and supersede on purpose instead of editing old stories.
- Humans still get a view. ghf-server has a read-only web viewer for traces, and GitStory exports Markdown.

---

## Eating my own dog food

The first real story GitStory wrote was about ghf itself. I pointed it at ghf's repository and the Claude Code sessions where ghf's second release candidate was built, then landed that work as one story. It correctly identified the original request as the goal and listed the six decisions that shipped. It recorded one rejected approach: a `#[path]` wrapper for the `ghf` alias, which broke the crate's module paths and was replaced by a library with two binaries. Its unknowns were honest too. It noted that the evidence never showed CI passing on a real Windows host, which was fair, because GitStory doesn't collect CI results yet.

That run also turned up several problems in GitStory, which is exactly why you dogfood. The most instructive one: the session window picked up an unrelated task from earlier that day and briefly made it the goal. Evidence is now anchored to the request that led to the first edit of a changed file.

---

## Where this goes

Both tools are being tested in Cloop and Automaze Works, and anything can change before 1.0\. Next up:

- **ghf:** a month-long pilot inside a real agent pipeline, measuring rescues, time saved and overhead per task, then 0.1.0.
- **GitStory:** a quality study on real agent sessions, with people rating stories for accuracy and usefulness, before it opens up.

The bet underneath both is the same. Agents won't replace Git; they'll need more *around* it: a place to keep every step and a place to keep every reason, each with its own rules, and neither getting in Git's way.

If you run coding agents and either of these problems sounds familiar, I'd love to hear from you.

- ghf: [ghf.sh](https://ghf.sh/?ref=varops.com) · [GitHub](https://github.com/varops/ghf?ref=varops.com)
- GitStory: [gitstory.sh](https://gitstory.sh/?ref=varops.com) · [GitHub](https://github.com/varops/gitstory?ref=varops.com)