Semantic archaeology for Git. Find the history that explains the code.
TypeScript
0
163 commits
updated Sep 11, 2026
git blame tells you who changed the code. git why finds the history
that explains it.
Local-first semantic + full-text search over Git history. It returns the actual commit, its author's real words, and the relevant diff. It retrieves evidence; it does not generate an explanation of its own.
Real output from mise run demo, which rebuilds a small fixture repository
from bench/fixtures/demo/build.mjs and runs these two queries against it.
Nothing here is hand-written, and you can reproduce it in one command.
For output against real repositories — curl, zod, redis — including a
worked case where this is the wrong tool, see
docs/examples.md or the
recorded terminal sessions.
$ git why "that bizarre bug where reconnecting subscribed twice"
1. 7deab42 Stop duplicate subscriptions after reconnect
2025-11-20 · Maya Chen
Reconnecting re-ran the subscribe handler without clearing the previous
registration, so every reconnect doubled the delivered events.
src/net/socket.ts
-export function connect(url) { return new Socket(url); }
+export function connect(url) {
+ const s = new Socket(url);
+ s.on('reconnect', () => resubscribeOnce(s));
+ return s;
+}
$ git why "why do we keep the session when the refresh token is empty?"
1. f17db20 Fix infinite token-refresh loop
2025-11-03 · Maya Chen
Provider X can return an empty refresh token while the current access
token remains valid. Retrying here puts clients into an infinite loop.
src/auth/refresh.ts
export function refresh(session, refreshToken) {
- if (!refreshToken) throw new InvalidTokenError();
+ if (!refreshToken) return session;
return exchange(refreshToken);
}
curl -fsSL https://alliecatowo.github.io/git-why/install.sh | sh
npm install -g @alliecatowo/git-why
The package installs a git-why executable, which Git dispatches as the
subcommand git why. No alias setup needed. More install paths — pinned
versions, a GitHub release tarball, building from source, uninstalling —
are in docs/install.md.
Two plugins ship in plugins/:
| plugin | what you get |
|---|---|
git-why | The MCP server, a routing skill, and an index-management skill. |
git-why-full | The same, plus zg for current-code search and a history-explorer agent for questions that need several searches. |
OpenCode users get opencode/ — an opencode.json with the MCP registration
and an AGENTS.md fragment.
What the skill actually teaches is when not to reach for history. It tells
an agent to use git log -S when it can name the symbol, because that is a
case this tool measurably loses (Hit@10 0.950 against 0.350), and to use code
search rather than history for the current state of the code. A skill that
claims its own tool is always best makes an agent worse at its job.
See docs/plugin.md.
git why server on
Holds the index, the embedding model and the lineage table open between queries. On curl — 30,000 commits — that is 569 ms a query down to 286 ms. Searches use it automatically once it is running.
It can never be the reason a search fails: if no daemon is running, or it is
unreachable, or your index moved under it, the query runs directly instead.
--daemon=direct opts out, --daemon=server requires one. See
docs/daemon.md.
git log --grep only matches words you already know. The reason code
changed usually lives in a commit message, but you rarely remember its exact
wording — you remember the problem, in your own words, months later.
Look again at the first example above: the query is "reconnecting subscribed twice" and the commit is titled "Stop duplicate subscriptions after reconnect." They share almost no vocabulary. That's not a coincidence Git Why is showing off — it's the actual point of a semantic index. It carries the meaning of the change, not just its words, alongside an ordinary keyword index for when you do know the exact term.
Git Why retrieves historical evidence: the commit, the author's actual words, and the relevant diff. Historical commit messages are assertions by their authors, not infallible accounts of intent, and some reasons were never committed at all — Git Why will not manufacture those.
Every number below is written into this file by bench/report.mjs from raw
run data, and CI fails if it drifts — no figure here was typed by hand. Full
methodology, limits, and the negative results are in
docs/report.md.
174 recall questions — you remember a problem but cannot name anything in the
commit that fixed it — derived mechanically from 6 pinned real repositories
(curl, redis, requests, ripgrep, caddy, zod). Every question is verified unanswerable by
keyword search before it enters the set: if git log --grep or git log -S finds the
answer from the question's own words, the case is discarded.
| strategy | Hit@1 | Hit@5 | MRR | returned nothing |
|---|---|---|---|---|
| git why | 0.201 | 0.374 | 0.266 | 0 |
| zg (semantic code search) | 0.017 | 0.040 | 0.026 | 2 |
| git log -G | 0.006 | 0.017 | 0.011 | 15 |
| git log --grep | 0.000 | 0.011 | 0.003 | 0 |
| git log --grep --all-match | 0.000 | 0.000 | 0.000 | 109 |
| git log -S | 0.000 | 0.000 | 0.000 | 15 |
10.2x zg and 23x the best Git-native strategy — and the only approach that answers nearly every question rather than returning an empty set.
When you can name the symbol, use pickaxe search instead. On cross-file causal
questions, git log -S scores Hit@10 0.950 against git why's 0.350.
Semantic search has no advantage over a tool you can hand the exact literal.
That boundary is the honest positioning, and the shipped skill tells agents both halves:
git whygit log -SzgRetrieval quality is not the product. The question is whether an agent answering a real question does it more accurately, or in fewer turns, with the tool than without. Four arms over the same frozen tasks, paired per task, with token counts reconciled against the provider's own accounting database.
| model | paired n | accuracy W-L | median tool calls saved |
|---|---|---|---|
| gemini-3.1-flash-lite | 6 | 1-2 | 2.5 |
| claude-haiku-4-5 | 9 | 1-1 | 1 |
| claude-sonnet-5 | 8 | 0-0 | 1 |
| deepseek-v4-flash | 8 | 0-0 | 3.5 |
| gemini-2.5-flash-lite | 6 | 2-1 | 1 |
| gemini-3.1-flash-lite | 7 | 2-0 | 1 |
| gemini-3.5-flash | 6 | 0-0 | 0.5 more |
Results are mixed across models. At single-digit paired n per model this is descriptive, not significant, and it is reported that way deliberately — the direction is consistent, the magnitude is not established. Full method and per-arm figures in the benchmark report.
Method, per-arm figures and the registered hypothesis: docs/report.md.
| measurement | result |
|---|---|
| Index across 6 real repos (56,781 commits) | 5.51–7.84 KB/record |
| curl-curl (30,000 commits, 182,772 records) | 1.37 GiB, 7.84 KB/record |
Query on curl-curl (30,000 commits) with git why server on | 384 ms p50, 491 ms p95 |
| The same query with no daemon | 851 ms p50, 1371 ms p95 |
| Diff/evidence ingestion, real-repo ablation | earns its cost, ΔHit@5 +0.375 |
Eight optimisations were implemented and measured. One survived.
The one that did: lexical overlap between the question and the commit's own message, reranked over a candidate pool deeper than the result list. Weight chosen on a dev half, evaluated once on the held-out half — +26% MRR and +44% Hit@1 on cases it never saw. Shipped.
The seven that did not: a prose-tuned embedding model, pseudo-relevance feedback, a prose-commit penalty, caller-side query restatement, wider result windows, structural expansion, and phrase fusion. None improved MRR.
What made the difference was not a better idea but a different question. The
seven all asked "does this rank better". The one that worked started by asking
where the right commit actually is — and found that a quarter of the corpus is
retrieved but ranked below 5, which is a reordering problem, while 42% is never
retrieved at all, which is not. See
docs/decisions.md.
The remaining headroom is that 42%: a recall problem in the embedding itself.
That is now measured rather than guessed —
docs/embedding.md compares ten models on a fixed pool.
Transformer embedders score +41% (jina-v2-small) and +64%
(jina-v2-base) MRR over the shipped static one, at 191x and 1287x the
indexing time plus an ONNX runtime. Both are available opt-in; the default
stays fast and dependency-free, because a tool that installs in seconds and
indexes in under a minute should not quietly become one that needs fifteen
hours.
It is also wrong most of the time. Hit@5 of 0.374 means the right commit is
outside the top five on 62.6% of these questions. It beats every alternative on
them and still fails on most. Treat a result as a lead to verify with git show, never as
established fact.
Git Why is ranked retrieval over a semantic index. That's the wrong tool for some jobs, and Git already has the right one:
git log -S and git log -G are exhaustive.
Keyword mode here is ranked, not exhaustive.ripgrep over a checkout, or git grep.git merge-base, git log --ancestry-path and git show.Git Why is for the case where you remember what happened but not what it was called.
ROADMAP.md — pull requests, gh why, wikis, and what
would need measuring before any of it ships.docs/embedding.md — nine embedding models
measured, the +41% one you can opt into, and why the default did not change.docs/daemon.md — the optional daemon: what it holds
warm, why it can never break a search, and its security posture.docs/indexes.md — where indexes live, worktrees,
submodules, monorepos, disk use, and the shared model cache.docs/examples.md — real output on real
repositories, including a case where this is the wrong tool.docs/plugin.md — both plugins, the MCP tools, the
skills, and why there is no hook.docs/decisions.md — what was tried and rejected,
with the measurements. Seven optimisations that did not work.docs/install.md — every install path and
uninstall, including where the index and model cache live on disk.docs/operations.md — the full operational
contract: durability, concurrency, exit codes, history scope, coverage
limits.docs/report.md — the benchmark report this
README's numbers come from.docs/spec.md — the build specification this was
written against. Source comments cite its sections, so it is kept as
provenance rather than as user documentation.mise setup # install dependencies, then doctor
mise check # format, lint, typecheck, protocol hash, unit tests
mise test:integration # real repositories, real storage, real locks
mise test:package # pack, install into a clean prefix, invoke through Git
mise site:build # build the docs/marketing site in site/
mise tasks # everything else, including every benchmark
Numbers in README.md, site/index.md and docs/report.md are written by
bench/report.mjs from raw run data — CI fails if you edit one by hand. The
man page, the site's CLI reference and all three shell completions are checked
against git why -h, which is the only place the flag set is defined.
See docs/contributing.md for module ownership and how
to cut a release.
163 commits
TypeScript
59.2%
JavaScript
39.3%
Semantic archaeology for Git. Find the history that explains the code.
TypeScript
0
163 commits
updated Sep 11, 2026
git blame tells you who changed the code. git why finds the history
that explains it.
Local-first semantic + full-text search over Git history. It returns the actual commit, its author's real words, and the relevant diff. It retrieves evidence; it does not generate an explanation of its own.
Real output from mise run demo, which rebuilds a small fixture repository
from bench/fixtures/demo/build.mjs and runs these two queries against it.
Nothing here is hand-written, and you can reproduce it in one command.
For output against real repositories — curl, zod, redis — including a
worked case where this is the wrong tool, see
docs/examples.md or the
recorded terminal sessions.
$ git why "that bizarre bug where reconnecting subscribed twice"
1. 7deab42 Stop duplicate subscriptions after reconnect
2025-11-20 · Maya Chen
Reconnecting re-ran the subscribe handler without clearing the previous
registration, so every reconnect doubled the delivered events.
src/net/socket.ts
-export function connect(url) { return new Socket(url); }
+export function connect(url) {
+ const s = new Socket(url);
+ s.on('reconnect', () => resubscribeOnce(s));
+ return s;
+}
$ git why "why do we keep the session when the refresh token is empty?"
1. f17db20 Fix infinite token-refresh loop
2025-11-03 · Maya Chen
Provider X can return an empty refresh token while the current access
token remains valid. Retrying here puts clients into an infinite loop.
src/auth/refresh.ts
export function refresh(session, refreshToken) {
- if (!refreshToken) throw new InvalidTokenError();
+ if (!refreshToken) return session;
return exchange(refreshToken);
}
curl -fsSL https://alliecatowo.github.io/git-why/install.sh | sh
npm install -g @alliecatowo/git-why
The package installs a git-why executable, which Git dispatches as the
subcommand git why. No alias setup needed. More install paths — pinned
versions, a GitHub release tarball, building from source, uninstalling —
are in docs/install.md.
Two plugins ship in plugins/:
| plugin | what you get |
|---|---|
git-why | The MCP server, a routing skill, and an index-management skill. |
git-why-full | The same, plus zg for current-code search and a history-explorer agent for questions that need several searches. |
OpenCode users get opencode/ — an opencode.json with the MCP registration
and an AGENTS.md fragment.
What the skill actually teaches is when not to reach for history. It tells
an agent to use git log -S when it can name the symbol, because that is a
case this tool measurably loses (Hit@10 0.950 against 0.350), and to use code
search rather than history for the current state of the code. A skill that
claims its own tool is always best makes an agent worse at its job.
See docs/plugin.md.
git why server on
Holds the index, the embedding model and the lineage table open between queries. On curl — 30,000 commits — that is 569 ms a query down to 286 ms. Searches use it automatically once it is running.
It can never be the reason a search fails: if no daemon is running, or it is
unreachable, or your index moved under it, the query runs directly instead.
--daemon=direct opts out, --daemon=server requires one. See
docs/daemon.md.
git log --grep only matches words you already know. The reason code
changed usually lives in a commit message, but you rarely remember its exact
wording — you remember the problem, in your own words, months later.
Look again at the first example above: the query is "reconnecting subscribed twice" and the commit is titled "Stop duplicate subscriptions after reconnect." They share almost no vocabulary. That's not a coincidence Git Why is showing off — it's the actual point of a semantic index. It carries the meaning of the change, not just its words, alongside an ordinary keyword index for when you do know the exact term.
Git Why retrieves historical evidence: the commit, the author's actual words, and the relevant diff. Historical commit messages are assertions by their authors, not infallible accounts of intent, and some reasons were never committed at all — Git Why will not manufacture those.
Every number below is written into this file by bench/report.mjs from raw
run data, and CI fails if it drifts — no figure here was typed by hand. Full
methodology, limits, and the negative results are in
docs/report.md.
174 recall questions — you remember a problem but cannot name anything in the
commit that fixed it — derived mechanically from 6 pinned real repositories
(curl, redis, requests, ripgrep, caddy, zod). Every question is verified unanswerable by
keyword search before it enters the set: if git log --grep or git log -S finds the
answer from the question's own words, the case is discarded.
| strategy | Hit@1 | Hit@5 | MRR | returned nothing |
|---|---|---|---|---|
| git why | 0.201 | 0.374 | 0.266 | 0 |
| zg (semantic code search) | 0.017 | 0.040 | 0.026 | 2 |
| git log -G | 0.006 | 0.017 | 0.011 | 15 |
| git log --grep | 0.000 | 0.011 | 0.003 | 0 |
| git log --grep --all-match | 0.000 | 0.000 | 0.000 | 109 |
| git log -S | 0.000 | 0.000 | 0.000 | 15 |
10.2x zg and 23x the best Git-native strategy — and the only approach that answers nearly every question rather than returning an empty set.
When you can name the symbol, use pickaxe search instead. On cross-file causal
questions, git log -S scores Hit@10 0.950 against git why's 0.350.
Semantic search has no advantage over a tool you can hand the exact literal.
That boundary is the honest positioning, and the shipped skill tells agents both halves:
git whygit log -SzgRetrieval quality is not the product. The question is whether an agent answering a real question does it more accurately, or in fewer turns, with the tool than without. Four arms over the same frozen tasks, paired per task, with token counts reconciled against the provider's own accounting database.
| model | paired n | accuracy W-L | median tool calls saved |
|---|---|---|---|
| gemini-3.1-flash-lite | 6 | 1-2 | 2.5 |
| claude-haiku-4-5 | 9 | 1-1 | 1 |
| claude-sonnet-5 | 8 | 0-0 | 1 |
| deepseek-v4-flash | 8 | 0-0 | 3.5 |
| gemini-2.5-flash-lite | 6 | 2-1 | 1 |
| gemini-3.1-flash-lite | 7 | 2-0 | 1 |
| gemini-3.5-flash | 6 | 0-0 | 0.5 more |
Results are mixed across models. At single-digit paired n per model this is descriptive, not significant, and it is reported that way deliberately — the direction is consistent, the magnitude is not established. Full method and per-arm figures in the benchmark report.
Method, per-arm figures and the registered hypothesis: docs/report.md.
| measurement | result |
|---|---|
| Index across 6 real repos (56,781 commits) | 5.51–7.84 KB/record |
| curl-curl (30,000 commits, 182,772 records) | 1.37 GiB, 7.84 KB/record |
Query on curl-curl (30,000 commits) with git why server on | 384 ms p50, 491 ms p95 |
| The same query with no daemon | 851 ms p50, 1371 ms p95 |
| Diff/evidence ingestion, real-repo ablation | earns its cost, ΔHit@5 +0.375 |
Eight optimisations were implemented and measured. One survived.
The one that did: lexical overlap between the question and the commit's own message, reranked over a candidate pool deeper than the result list. Weight chosen on a dev half, evaluated once on the held-out half — +26% MRR and +44% Hit@1 on cases it never saw. Shipped.
The seven that did not: a prose-tuned embedding model, pseudo-relevance feedback, a prose-commit penalty, caller-side query restatement, wider result windows, structural expansion, and phrase fusion. None improved MRR.
What made the difference was not a better idea but a different question. The
seven all asked "does this rank better". The one that worked started by asking
where the right commit actually is — and found that a quarter of the corpus is
retrieved but ranked below 5, which is a reordering problem, while 42% is never
retrieved at all, which is not. See
docs/decisions.md.
The remaining headroom is that 42%: a recall problem in the embedding itself.
That is now measured rather than guessed —
docs/embedding.md compares ten models on a fixed pool.
Transformer embedders score +41% (jina-v2-small) and +64%
(jina-v2-base) MRR over the shipped static one, at 191x and 1287x the
indexing time plus an ONNX runtime. Both are available opt-in; the default
stays fast and dependency-free, because a tool that installs in seconds and
indexes in under a minute should not quietly become one that needs fifteen
hours.
It is also wrong most of the time. Hit@5 of 0.374 means the right commit is
outside the top five on 62.6% of these questions. It beats every alternative on
them and still fails on most. Treat a result as a lead to verify with git show, never as
established fact.
Git Why is ranked retrieval over a semantic index. That's the wrong tool for some jobs, and Git already has the right one:
git log -S and git log -G are exhaustive.
Keyword mode here is ranked, not exhaustive.ripgrep over a checkout, or git grep.git merge-base, git log --ancestry-path and git show.Git Why is for the case where you remember what happened but not what it was called.
ROADMAP.md — pull requests, gh why, wikis, and what
would need measuring before any of it ships.docs/embedding.md — nine embedding models
measured, the +41% one you can opt into, and why the default did not change.docs/daemon.md — the optional daemon: what it holds
warm, why it can never break a search, and its security posture.docs/indexes.md — where indexes live, worktrees,
submodules, monorepos, disk use, and the shared model cache.docs/examples.md — real output on real
repositories, including a case where this is the wrong tool.docs/plugin.md — both plugins, the MCP tools, the
skills, and why there is no hook.docs/decisions.md — what was tried and rejected,
with the measurements. Seven optimisations that did not work.docs/install.md — every install path and
uninstall, including where the index and model cache live on disk.docs/operations.md — the full operational
contract: durability, concurrency, exit codes, history scope, coverage
limits.docs/report.md — the benchmark report this
README's numbers come from.docs/spec.md — the build specification this was
written against. Source comments cite its sections, so it is kept as
provenance rather than as user documentation.mise setup # install dependencies, then doctor
mise check # format, lint, typecheck, protocol hash, unit tests
mise test:integration # real repositories, real storage, real locks
mise test:package # pack, install into a clean prefix, invoke through Git
mise site:build # build the docs/marketing site in site/
mise tasks # everything else, including every benchmark
Numbers in README.md, site/index.md and docs/report.md are written by
bench/report.mjs from raw run data — CI fails if you edit one by hand. The
man page, the site's CLI reference and all three shell completions are checked
against git why -h, which is the only place the flag set is defined.
See docs/contributing.md for module ownership and how
to cut a release.
163 commits
TypeScript
59.2%
JavaScript
39.3%