git why
Put the why in version control.
git why is a Git subcommand that records why each commit exists — as native
git trailers on the commit message, so
the reasoning travels with git clone, git fetch and git push and shows up in plain
git log. No database, no service.
When you build software by directing an AI, that reasoning otherwise lives in a chat window
that closes. Six months later the code is still there and the why is gone. git why
keeps the rationale, the prompt, and the rejected alternatives attached to the commit that
they explain.
Install
pipx install git+https://github.com/maledadams/git-why
or drop the single file on your PATH — no build, no dependencies:
curl -fsSL https://raw.githubusercontent.com/maledadams/git-why/main/git_why.py -o ~/.local/bin/git-why
chmod +x ~/.local/bin/git-why
Git picks it up as a subcommand automatically: git why.
Quickstart
git why init # install the hook (once per repo; tip-only, never blocks)
git why commit -m "add breed filter" \
-b "let users narrow cat results by breed" \
--rationale "client-side filter on the already-fetched list" \
--agent claude-sonnet-5
git why HEAD # reasoning for a commit
git why src/filter.ts # reasoning for the last change to a file
git why log --since "7 days ago" # the reasoning trail
git why log --agent claude-sonnet-5
git why export > reasons.ndjson # every recorded reason, one JSON object per line
git why check origin/main..HEAD # optional CI gate (see "How much it enforces")
You never have to use git why commit — a plain git commit --trailer "Why: ..." works
the same. The wrapper is just shorter.
What gets stored
Reasoning is stored as trailers on the commit message. Why: is the one that matters; the
rest are optional.
| Trailer | Meaning |
|---|---|
Why: |
why this change exists |
Why-Prompt: |
the instruction that produced it |
Why-Rationale: |
why this approach |
Why-Alternatives: |
what was considered and rejected |
Why-Spec: |
requirement / issue it satisfies |
Why-Agent: |
model + tool that wrote it |
Why-Session: |
id grouping commits from one work session |
Why-Confidence: |
low / medium / high / human-reviewed |
Why-Skip: |
this commit deliberately has no reason, and why |
How it works
- Trailers, not a database. Everything lives in the commit message’s last paragraph,
the same place as
Co-Authored-By:. Nothing to host, nothing to sync. git why exportturns the whole history into NDJSON — one object per commit, everyWhy-*field as a key. That’s the point of a schema: you can filter and analyse it (--agent,--session,--specongit why log) instead of grepping prose.git why checkapplies the same rule in CI.
How much it enforces
By default, nothing is blocked. git why init installs a commit-msg hook that stays
out of your way. git config why.strict sets how loud it is:
| Level | On a substantial change with no reason… |
|---|---|
nudge (default) |
commit goes through; one line on stderr saying how to add a reason |
substantial |
commit is blocked until you add a reason (git why init --enforce) |
all |
as above, on every non-merge commit (git why init --all) |
off |
completely silent (git why init --silent) |
“Substantial” = more than ~15 non-generated lines, and the subject isn’t
chore/build/ci/revert/bump. Lockfiles, *.min.* and snapshots don’t count toward the
size. Trivial changes are never touched, at any level. Tune it:
git config why.threshold 30, git config why.skip-paths "docs/*,*.md".
At the substantial / all levels a weak reason is rejected too — a Why: that just
repeats the subject, or filler like update / wip / fix.
Any commit can opt out on purpose, and the reason for skipping is recorded:
git commit --trailer "Why-Skip: vendored, not our code".
For AI agents
git why agent-setup prints a block to paste into CLAUDE.md, .cursorrules, or wherever
your agent reads instructions. The gist:
Commit with
git why commit -m "<subject>" -b "<why this change exists>" --prompt "<the request>" --agent "<model>". Write the reason yourself from the conversation — it’s intent the user never has to type. Trivial changes can use a baregit commit.
The default hook never blocks the agent — it just leaves a tip if a real change went in
with no reason. Turn on --enforce once you trust the workflow.
Why not just keep the chat logs
Chat logs aren’t attached to the code, aren’t in the repo, don’t survive a clone, and
nobody greps them. A trailer is one line, lives on the commit, and git log already
shows it.
How git why compares
| Approach | In the repo | Survives clone / push | Structured & queryable | Per-commit | Enforceable |
|---|---|---|---|---|---|
| git why (trailers) | yes | yes | yes — export, log filters |
yes | yes (opt-in) |
git notes |
yes | no (not pushed by default) | no | yes | no |
| Conventional Commits | yes | yes | commit type only, not the reason | yes | via commitlint |
| ADRs (markdown files) | yes | yes | yes | no — project-level | no |
| Chat / agent session logs | no | no | no | loosely | no |
| Prose in the commit body | yes | yes | git log \| grep only |
yes | no |
FAQ
What is git why?
git why is a small Git subcommand that records why each commit exists. The reason,
the prompt that produced the change, the approach taken and the alternatives rejected are
stored as git trailers on the commit
message — so they live in the commit, survive clone/push, and show in git log.
How is git why different from git blame?
git blame attributes each line to the commit and author that last changed it — not the
intent behind the change. git why <file> picks up where that stops: it shows the recorded
reasoning for the last commit that touched the file.
Where does git why store the reasoning?
In the commit message itself, as Key: value trailers (Why:, Why-Rationale:,
Why-Agent:, …), the same mechanism as Co-Authored-By: and Signed-off-by:. There is
no database, no server, and nothing to keep in sync.
How is this different from git notes?
git notes attach data to a commit but are not pushed or fetched by default, have no
schema, and no tooling around them. git why uses trailers, which are part of the commit
and travel automatically, and adds a reader, filters, an NDJSON export, and an optional
enforcement hook.
Does git why work with any AI coding agent?
Yes. git why agent-setup prints an instruction block to paste into Claude Code, Cursor,
Aider, or any agent’s config. Any tool that runs git commit can record a reason.
Does git why send my data anywhere?
No. It shells out to your local git and nothing else — no network calls, no telemetry.
Your commit messages never leave your machine.
Does it change my commit workflow?
No. Plain git commit --trailer "Why: …" works; git why commit is just a shorter way to
type it. The hook installed by git why init only prints a one-line tip by default —
--enforce makes it block.
Can I enforce a reason in CI?
Yes: git why check <range> --level substantial exits non-zero if a non-trivial commit in
the range has no reason (or a lazy one). Trivial diffs — lockfile bumps, typos — are
ignored.
What repos and languages does it support?
Any Git repository, any language — it only reads commit metadata. It needs git 2.32+ and
Python 3.8+, and is a single file with zero dependencies.
Roadmap
Kept deliberately small. Planned next, roughly in order:
- git notes fallback for prompts too long to sit in a message
- an editor-time staging file so an agent can record intent before it commits
- editor integrations (VS Code, Neovim) surfacing
git whyon the current line git why blame <file>— blame, but the last column is the reason
Contributing
Issues and PRs are welcome, including from first-timers — see
CONTRIBUTING.md and the
good first issue label.
License
MIT © Lucia Adams