git-ai: Who Actually Wrote This Line?
Table of Contents

I keep the ThoughtWorks Tech Radar open as a periodic sanity check — not to adopt anything from it, just to see what’s moving. This time the honest takeaway was the volume itself. The distance between “new technique” and “thing three teams around me already use” has collapsed to something like a quarter.
A few entries were genuinely interesting. The one I want to write up is git-ai,1 partly because it solves a problem I actually have, and partly because how it solves it is better than the feature.
The Question I Currently Can’t Answer
On my team we use Cursor, Claude Code, and Antigravity, across a rotating set of models, alongside plenty of code still written by hand. The output all lands in the same place, through the same mechanism: a commit, with a human name and email on it.
So the commit tells me who committed the code. It tells me nothing about what produced it.
That gap is fine right up until you want to ask an operational question, and then it’s the only thing in the way:
- What is AI actually costing us, per project? Not the seat licence — the token spend attributable to work that shipped.
- Which model is worth its price? Cost per accepted change is a real number, and right now nobody can compute it.
- Which model’s code survives review best? If one model’s output consistently draws fewer review comments and fewer follow-up fixes, that’s more decision-relevant than any benchmark score.
Every one of those questions needs the same missing primitive: line-level attribution that says which agent, which model, and which session produced this code. Not a guess from a heuristic — a record written at the time it happened.
That’s exactly what git-ai stores.
What it Does, from the outside
git-ai attaches attribution metadata to your commits, and the design constraint it holds is that nothing about your workflow changes. You commit the way you always did. The repo’s contents, history, and commit objects are untouched.
It installs as an ordinary git subcommand — a git-ai binary on $PATH, which git surfaces as git ai ... — and it writes its output into git notes, the underrated corner of git built precisely for “attach data to a commit that already exists, without changing the commit.” You read it back with:
git log --show-notes="ai"
That’s the whole user-facing surface. Which is the point: a tool that asks you to change how you commit will not survive contact with a team.
The Interesting part is how
Here’s where I went down the rabbit hole. There are two separate problems, and the tool answers them in two completely different ways.
Problem One: how Does it Know an Agent Wrote Something?
It doesn’t infer. The agents report themselves.
Every serious coding agent now has an internal hook system — pre- and post-tool-call hooks that fire when the agent uses a tool. git-ai registers into those, so whenever an agent writes to a file, the agent shells out to:
git ai checkpoint
Each checkpoint records which lines came from which agent, model, and session, into a pending working log. Note that this is entirely inside the agent — it has nothing to do with git’s hooks. It’s the agent narrating its own edits, before any commit exists.
That much I’d have guessed. The next part I wouldn’t have.
Problem Two: when Does Any of it Reach Git?
The checkpoints are pending state. A git note can only be written against a commit’s SHA, so nothing can be attributed until a commit actually exists. Which means git-ai needs to know, reliably, the instant a commit succeeds.
The obvious answer is a post-commit hook. And the obvious answer has two problems that are hard to design around:
- Local hooks (
.git/hooks/) need per-repo installation. Every clone, every new repo, every machine. Not a mechanism you can build “just works” on top of. core.hooksPathis a global override.2 Point it at git-ai’s directory and git stops looking in.git/hooks/— so your repo’s own pre-commit checks, your lint hooks, your team’s shared setup silently stop running. There is one hook slot per event, and every tool wants it.
Earlier versions of git-ai did use a managed post-commit hook, and retired it for exactly these reasons.
The replacement is the part that made me sit up. git-ai doesn’t use git hooks at all. It listens to git’s own telemetry.
Trace2: Git Narrating Itself
Git has a built-in structured tracing facility called Trace2.3 Every git process can emit a live JSON event stream describing what it’s doing — which subcommand ran, the full argv, timing regions, exit codes. It exists for git’s own performance diagnostics, and it’s configured with a single setting, trace2.eventTarget (or the GIT_TRACE2_EVENT environment variable).
The detail that makes it a platform rather than a debugging aid: the target doesn’t have to be a log file. It can be a Unix domain socket.
So git-ai’s install sets trace2.eventTarget globally to a socket, and runs a background daemon listening on the other end. From that moment, every git invocation on the machine voluntarily narrates itself to that daemon in real time. The daemon filters the stream for one thing: a successful commit event. When it sees one, the checkpoints are no longer pending — it diffs the new commit against its parent, maps the recorded checkpoints onto the resulting line ranges, and writes the result as a note on that commit’s SHA.
Sit with the difference for a second, because it’s a genuine architectural distinction and not just an implementation detail:
| Git hooks | Trace2 | |
|---|---|---|
| Direction | git calls out to your script | git reports on itself to a listener |
| Installation | per repo, or a global override | one config setting, machine-wide |
| Contention | one slot per event — tools compete | any number of listeners, no conflict |
| Coverage | the events git defines hooks for | every git invocation |
A hook is an invasive integration point you have to win a fight over. Trace2 is an observation point nobody has to share. For a tool whose entire value proposition is “this changes nothing about your setup,” that’s not a clever trick — it’s the only mechanism that satisfies the requirement.
It’s written in Rust, which is the right call for something that sits in the path of every git command you run and has to cost nothing.
The part I Didn’t Expect: They Published the Format
The thing that moved this from “neat tool” to “worth writing about” is that the note format isn’t proprietary. git-ai defines and publishes an open standard for how AI attribution is stored in git notes — currently v3.0.0, versioned in the repo.4
That reframes what’s being built. If the format is a spec, then git-ai is one implementation of a substrate rather than a silo: your CI can read the notes, your review tooling can read the notes, a dashboard can read the notes, and a different attribution tool can write them. The attribution data outlives any one vendor’s decision to pivot, get acquired, or go quiet.
Given how much of the current AI-tooling wave is racing to own the metadata rather than share it, choosing to standardize the format is the most encouraging thing in the whole project. 🧭
Where it Gets Messy
Two caveats worth knowing before you get excited, both inherited from git notes rather than from git-ai:
Notes are attached to a SHA, and rewriting history changes the SHA. Rebase, cherry-pick, squash, amend — each produces a new commit, and the note doesn’t come along. git-ai has dedicated logic to re-derive and migrate attribution across those operations, but the system is explicitly eventually consistent for anything more involved than a straight commit. If your team squash-merges everything, that path matters more to you than the happy path does.
Notes don’t travel by default. refs/notes/* isn’t in git’s default push refspec, so attribution stays local until someone configures it to be pushed and fetched. Any team-level analysis — the cost-per-project and review-comment questions I opened with — needs that configured deliberately, or you’re collecting data on one laptop.
Why This One Stuck with Me
I went in wanting the feature, and came out more interested in the shape of the solution.
The lesson I’m keeping isn’t about AI attribution at all. It’s that when a tool needs to observe a system without perturbing it, the instinct is to reach for the extension point the system advertises — the hook, the plugin slot, the middleware chain — and those are all invasive, single-tenant, and contended. Meanwhile the telemetry the system already emits about itself is uncontended, complete, and costs the host nothing to have observed. git-ai found the second kind of integration point in a thirty-year-old tool everyone thought they knew.
That’s a question worth carrying into other systems: what does this thing already tell me about itself, that I’ve been trying to make it tell me on purpose? 🔍
comments powered by Disqus