git blame tells you who and what. wherefore tells you why — and makes
recording the why as cheap as making the change, so it never needs recovering.
Git-native: whys live in Why: commit trailers and git notes (refs/notes/why).
No server, no database, nothing to run. The ledger is versioned with the repo
and travels with it.
One command: why.
npm link # exposes `why` on PATH (or run: node why.mjs ...)Node ≥ 18, and git on PATH. That's the whole dependency list.
# zero tooling — a trailer on the commit itself:
git commit -m "Raise webhook timeout" -m "Why: payment retries spike past 5s under load"
why add -m "Stripe counts per-minute windows, not rolling" # annotate HEAD after the fact
why add -f src/limiter.ts:40-44 -m "headroom for retries" -t perf # anchor to code
why add -c 3f2a1c9 -m "..." # annotate any commitwhy show src/limiter.ts:42 # blame the line, walk its history, print every recorded why
why show src/limiter.ts # whole-file why chain
why log # the whole ledger, newest first
why log src/limiter.ts # filtered to one path
why log --md # appendable markdown section (DECISIONS.md interop)
why show ... --json # machine-readable, for agents and tooling
why search "rolling window" # full-text search this repo's ledger, no path neededwhy show exits 2 when nothing is recorded — "not recoverable from the
ledger" is a first-class, scriptable answer. It never invents a reason.
That exit code is the point of the tool for automation: an agent or script can
be told to ground its explanation in the ledger and stay silent when the ledger
is, instead of confabulating a plausible-sounding rationale.
Anchored line ranges are drift-tracked: each entry records the commit its
coordinates refer to, and show/log map them to current HEAD via diff hunks
(lines 40-44→43-47). A range whose code was rewritten is flagged
(edited since) and always shown — hiding a why is a worse failure than
showing a stale one. Notes anchored to lines their commit never touched
(after-the-fact why add -f) are found by a full anchored-entry scan, not
just the blame chain. Renames of anchored files aren't followed — re-anchor
after a rename.
why sync # push/pull refs/notes/why with origin
why sync upstream
why hook install # pre-push hook: every push carries the ledger automaticallyNotes don't travel with plain git push; run why sync (or install the hook
once per repo) to share the ledger. Merging uses cat_sort_uniq — the ledger
is append-only, so syncs never conflict. The hook never blocks a push and
refuses to clobber an existing pre-push (husky repos: add the one-liner
manually).
If your repo keeps a human-facing DECISIONS.md, treat it as the record of
record and never overwrite it. Append the ledger as its own section instead:
why log --md >> DECISIONS.mdThe ledger is per-repo by design — git notes don't span repos, and a why answers for the repo it's asked in. When a decision in one repo is really justified by another (a shared schema repo's migration, a sibling service's incident), cite it as plain text; no special syntax exists or is needed:
why add -m "per schema-core@a1b2c3d: rate table pinned to v4" -f src/rates.ts:12why fleet then makes that citation findable without knowing which repo it
lives in — it walks every git repo directly under the current directory and
searches all of their ledgers at once:
cd ~/code
why fleet # every recorded why, across every repo, newest first
why fleet "rate table" # filtered by text, across every repo
why fleet ... --json # machine-readableSet WHEREFORE_FLEET_ROOT to search from elsewhere without cd-ing there.
Each row is tagged with its repo name. Repos with no ledger just contribute
nothing — they aren't an error.
wherefore records code intent only. Regulatory or compliance rule provenance belongs in your compliance system of record, where it can be audited as rules — don't launder it into code annotations here. Keep this ledger about why the code is the way it is.
- Trailers —
Why:lines are parsed straight off the commit message (git log --format=%(trailers:key=Why)), so the cheapest possible capture needs no tool at all. - Notes — everything added after the fact lives as JSONL in
refs/notes/why, one ref for the whole repo. Reads are batched (git cat-file --batch+git log --stdin) so the ledger costs the same handful of git spawns whether it holds ten entries or ten thousand. - Recovery —
why show <file>:<line>blames the line, walks its history withgit log -L, and also scans every file-anchored note, then prints each recorded why in order.
node test.mjsOne self-contained check: it fails if capture, recovery, chain-walking, drift
tracking, batched reads, --md export, cross-repo fleet, hook install, or
the exit codes break.
MIT