@nmdlabs/whyline
v0.4.0
Published
git blame for AI agents: which prompt wrote this line, what the agent was thinking, and what it cost
Readme
whyline
Review what an AI coding agent did before you commit it.
whyline reads the session transcripts that Claude Code and Codex already write to disk, matches every changed hunk in your diff back to the prompt and edit that produced it, and tells you which hunks to look at first.
$ whyline
3 hunks across 3 files, 2 need a look: 1 untraced, 1 unrequested
no turn found
TODO.md @@ -0,0 +1 @@
! untraced no structured edit covers these lines
file differs from what the recorded edits produce; some lines were changed another way
+scratch notes
turn 1 · claude-code · main agent · 2026-10-16 04:23 · 2.3k tokens · 72.0k from cache
you asked: add a TRANSCRIPTION member to the enum in src/modes.ts (#31)
agent said: Adding TRANSCRIPTION after TEXT_TO_SPEECH. While here, the retry count in config looks low for speech, so I'll raise it.
src/config.ts @@ -1 +1 @@
! unrequested prompt named src/modes.ts
-export const retries = 3;
+export const retries = 5;
unflagged:
src/modes.ts 1 hunk 1 turn 2.3k tokens · 72.0k from cache
The agent was asked for one enum member. It also changed a retry count in a file nobody mentioned, and explained why in a sentence you would have scrolled past. TODO.md was written by hand, so no turn accounts for it. The hunk you actually asked for is one line in the summary at the bottom.
No install, no hooks, no account. It works on sessions you ran last week.
What it flags
| flag | meaning |
|---|---|
| untraced | no structured edit in any session accounts for these lines. Hand edits, shell edits (sed, heredocs), or a session whyline could not find. |
| unrequested | your prompt named specific files and this is not one of them, nor imported by one. Only raised when the prompt named a file. |
| after-correction | written in a turn whose prompt opens with "no", "stop" or "undo", or says "don't", "revert", "wrong" or "instead" in its first 80 characters, or that came right after you interrupted the agent. |
| unspecified | nothing in the turn, an earlier turn of the same session, or the commit message points at an issue, a URL, or a spec-like document. The "did anyone write down what this is for" signal. |
| drifted | an earlier edit in this file did not match when replayed, so attribution after it is best-effort. |
| churned | the same lines were rewritten across three or more turns. |
| subagent | written by a spawned agent whose reasoning you never saw directly. |
Every flag is a plain rule over the transcript and the diff. No model is called. When whyline cannot prove where a line came from, it says untraced instead of guessing.
Install
npm i -g @nmdlabs/whylineOr run it without installing: npx @nmdlabs/whyline.
Usage
whyline # working tree vs HEAD, flagged hunks first
whyline --all # every hunk
whyline main..HEAD # any git range
whyline --html out.html # self-contained report
whyline --json # everything, for scripts
whyline --brief # headers and flags only; cheap for an agent to read
whyline --format md # Markdown for a PR description
whyline --prices default # add dollar estimates at list prices
whyline sessions # which sessions it found for this repo
whyline doctor # what it can and cannot see on this machine
whyline attach [range] # keep the record with the code (see below)
whyline verify [range] # are the records present and unaltered?
whyline export [range] --format agent-trace
whyline check [range] --policy .whyline.json # exit 4 on a violation (see below)More demos
Brief mode and cost. --brief prints the verdict, each turn's ask, and the flagged hunks with no diff bodies: a few lines an agent or a CI job can read cheaply. --prices default adds a dollar estimate per turn at the vendors' list prices; it is off unless asked for, because subscriptions are not billed per token.

Keeping the record. attach writes the report for a commit into git notes, with a hash. The note is plain JSON you can read with git notes --ref whyline show. verify checks that every commit in a range has a record and that none has been altered, on any machine with the notes fetched, with no transcript present.

Policy in CI. check evaluates a small JSON policy against the review and exits 4 on a violation. --format md prints the same result as Markdown for a pull request comment.

Tokens and cost
Every turn shows the tokens the model processed fresh and, separately, how many it read from cache. Cache reads are cheap per token but tell you how much context the turn was carrying.
Dollar figures are off by default, because Claude Code and Codex subscriptions are not billed per token. If you pay per token, --prices default prices each turn at the vendors' list prices (table dated in src/pricing.ts), and --prices rates.json lets you supply your own in USD per million tokens:
{ "claude-opus-5": { "input": 5, "output": 25, "cacheRead": 0.5, "cacheWrite": 6.25 } }A model with no price is reported as unpriced, never costed at zero.
whyline itself calls no model and spends no tokens. If an agent reads whyline's output, --brief gives it the verdict, each turn's ask, and the flagged hunks without the diff bodies.
Sessions are matched to the repo by their working directory and to the diff by time. --since 7d and --session <id> override that.
How it works
- Adapters read
~/.claude/projects/**and~/.codex/sessions/**into one event stream: prompts, reasoning, edits, shell commands, token usage, subagent spawns, interrupts. - A ledger replays the structured edits (Claude's
Edit/Write, Codex'sapply_patch) in order and remembers which edit last wrote each line. - The resolver runs
git diff --unified=0, looks each added line up in the ledger, attaches the prompt, reasoning, agent and turn cost, and applies the flag rules. - Terminal, HTML and JSON renderers print the same result.
Edits made through shell commands have no structured record in either harness, so those lines come out untraced. That is a gap in the data, and whyline reports it rather than papering over it.
Privacy
Transcripts are your private conversations. whyline makes no network calls of any kind and has zero runtime dependencies. It writes only what you ask for: a report with --html, notes under whyline attach, a hook under whyline install. attach --push is git's own push, run only when you pass the flag, and it carries the record, never the transcript. CI fails if any network module appears in src/.
Keeping the record
Transcripts are deleted by the harness after a while (Claude Code: 30 days). To keep the answer to "why is this line here" with the code:
whyline attach # write an intent record for HEAD into git notes (refs/notes/whyline)
whyline attach main..HEAD # one record per commit
whyline attach --push # and push the notes ref to origin
whyline verify main..HEAD # every commit attached, unaltered?
whyline install # post-commit hook: attach a record to every commit from now on
whyline install --sign # and sign it with your git ssh signing keyA record is exactly what --json prints for that commit, plus a hash. It holds the 120-character prompt excerpts and one line of agent reasoning per hunk, never the transcript. --no-prompts leaves even those out, for public repos.
On any machine with the notes fetched (git fetch origin refs/notes/whyline:refs/notes/whyline), whyline <range> renders from the records when no transcript is present, and says so in its second line.
whyline export main..HEAD --format agent-trace prints the same records in the Agent Trace format, one JSON line per commit.
Signing uses ssh-keygen -Y with the key in git config user.signingkey (set gpg.format ssh); verify checks against gpg.ssh.allowedSignersFile, the same files git itself uses for signed commits.
Records are produced on the author's machine from the author's transcripts: self-reported. The hash proves a record was not altered after it was written; it does not prove the transcript was honest.
In a pull request
On the author's machine, whyline install means every commit carries a record. Push the notes with the branch:
git push origin main refs/notes/whylineThen a CI job can review the branch without any transcript:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: git fetch origin refs/notes/whyline:refs/notes/whyline
- run: npx @nmdlabs/whyline check origin/main...HEAD --policy .whyline.json --format md > whyline.md || true
- uses: marocchino/sticky-pull-request-comment@v2
with: { path: whyline.md }.whyline.json:
{ "requireRecords": true, "maxUntracedRatio": 0.3, "forbid": ["unspecified"], "subagentNeedsBrief": true }check exits 4 on a violation, so drop the || true to fail the job.
What this is and is not: the record is produced on the author's machine from the author's transcripts. The hash proves it was not altered afterwards and a signature proves which key wrote it; neither proves the transcript was honest. Use check as a review aid and a nudge toward writing down what each change is for. It is not an authorization control.
Related tools
- Agit and Entire answer "which prompt wrote this line" with
blameandwhycommands, and go further on session sharing, forking and team capture. whyline builds the same provenance layer but puts the review question first: which hunks should I worry about? - Agent Blame marks lines as AI- or human-written via editor hooks.
Requirements
Node 20 or newer, git, and at least one Claude Code or Codex session run inside the repo.
License
MIT
