Skip to content

whygit

A command-line tool and MCP server that looks at a Git repository, says in plain English what state it's in, and gives the exact commands to fix it. It never changes anything, and the tests prove that on every scenario.

Case study5 min read

whygit in a repository stopped halfway through a merge. Git's own `git status` says the same thing in its own vocabulary; this says what happened and what to type.
Role
Solo project: design, code, tests and release.
When
October 2026
Stack
Python, Git, MCP (Model Context Protocol), pytest, GitHub Actions, PyPI

The problem

People new to Git hit the same few states again and again: a merge conflict, a rebase stopped halfway, a detached HEAD, a rejected push. Git's messages are accurate, and hard to act on if you don't already know Git.

What I built

One read-only engine that inspects the repository and matches pasted error messages, with two ways in: a `whygit` command for people, and an MCP server so coding agents like Claude Code get a reliable diagnosis instead of guessing.

What happened

29 repository states and 33 error messages covered, 246 tests, published on PyPI. A test fingerprints every file in 38 real scenario repositories before and after whygit runs, and nothing may change.

  • 29

    repository states it recognises

  • 33

    Git and GitHub error messages it explains

  • 246

    tests, on Linux, macOS and Windows

  • 0

    files changed, across 38 scenario repos

01

The problem with Git's messages

Git tells you exactly what happened, in Git's terms. “You have unmerged paths.” “HEAD detached at 3f2a1c9.” “Updates were rejected because the remote contains work that you do not have locally.” All true, and if you're new, none of it tells you what to type next.

So whygit does one job: look at the repository, say what's going on in plain English, and list the commands that get you out, each with a line on what it does. When a fix can lose work, like `git push --force` or `git reset --hard`, it says so and offers the safer option first.

  • Operations stopped halfway: merge conflicts (with the files), a rebase, cherry-pick or revert in progress.
  • Where you are: a detached HEAD (and whether you have commits on no branch), a branch that has diverged from GitHub, no upstream yet.
  • Things that will hurt later: a CSV over GitHub's 100 MB limit about to be committed, a staged `.env` or key file, a repository accidentally created in your home folder, a project nested inside another one.
  • Pasted errors: a rejected push, “refusing to merge unrelated histories”, password authentication no longer working on GitHub, “Please tell me who you are”, and more.

02

Two ways in, one engine

The same engine sits behind a command and an MCP server. On the command line, `whygit` diagnoses the folder you're in, and `whygit explain` takes an error you paste. Through MCP, a coding agent gets three tools: diagnose the repository, explain an error, and summarise its state.

The agent side was designed with the rules I'm testing in agent-interface-lab: few tools, a one-line description saying when to use each, typed inputs, small structured results, and errors that say how to recover. Each tool is also marked read-only and safe to repeat, so a client can treat it as harmless.

Explaining a pasted error. It also says what it can't know: the ahead/behind count is as of the last fetch, because whygit never fetches.

03

Read-only, and proven

A tool that tells people how to fix their repository must never touch it. That's easy to promise and easy to break: even `git status` can rewrite a file inside `.git` when it refreshes the index. So the promise is enforced in code and checked by tests.

There's exactly one place in the package that starts a process. It only runs Git subcommands on a short read-only list, rejects any options that write, and turns off the index refresh. It never fetches, so ahead/behind counts are reported “as of your last fetch”. Credentials in remote URLs are hidden from all output.

  • 38 scenarios build the real state with the real `git` binary in a temporary folder: a genuine conflict, a genuine detached HEAD, a clone that has diverged from a local stand-in for GitHub.
  • For each one, every file in the folder is fingerprinted (size, modification time, hash) before and after whygit runs, and the two must match.
  • A control test shows the check can see a change: running plain `git status` on the same repository does rewrite its index.
  • A code check confirms nothing else in the package can start a process, and a spy records every Git call made during a diagnosis.

04

When the test caught something

On the second CI run, the macOS job failed the read-only test. In one scenario, the modification time on the `.git/objects` folder was different after whygit ran. A broken promise would look exactly like that, so it needed explaining, not a retry.

The timing pointed elsewhere. The change landed about 50 ms after the scenario's last `git commit`. Git can run housekeeping in the background after a commit, so the folder could still have been changing when the test took its first fingerprint. I couldn't reproduce it locally in 15 runs of all 38 scenarios.

So the test now waits until the folder has stopped changing (two fingerprints 100 ms apart must match) before running whygit. If the folder is still moving on its own, the test says so instead of blaming whygit. The test repositories also turn off Git's background maintenance. After that, CI passed three runs in a row on every platform.

A broken promise would look exactly like that, so it needed explaining, not a retry.

05

Shipping it

CI runs the tests against the built package, not the source folder, on Python 3.10 to 3.14 on Linux and on macOS and Windows. The first Windows run failed twice, both in the tests rather than the tool: the fingerprint used Windows path separators, and Python quietly wrote the line-ending test data with Windows line endings, so there was nothing to warn about.

Releases publish themselves: pushing a version tag builds the package and uploads it to PyPI through trusted publishing, so no password or token is stored anywhere. The first name, gitwhy, was refused because PyPI treats it as the same name as an existing “git-why”, so it became whygit. It's also listed in the official MCP registry, so MCP clients can find it there.

Running 0.1.0 on my own projects folder turned up one more problem. Outside a repository, it lists the repositories just below to help you find the right one, but it cut the list at eight without saying so. 0.1.1 says “… and 5 more”.

Use itCommand
Run once, no installuvx whygit
Explain an errorwhygit explain "<paste the error>"
Add to Claude Codeclaude mcp add --scope user whygit -- uvx whygit mcp

What I'd carry forward

  1. 1If a tool promises not to change something, test the promise directly: fingerprint everything before and after.
  2. 2Give a check a control that proves it can fail, or a pass means nothing.
  3. 3When a safety test fails once, explain it before you rerun it.
  4. 4Test the built package on every platform you claim to support. Windows found two problems on its first run.
  5. 5Use your own tool on your own machine before calling it done.

Get in touch

Questions about my work, a role, or a project idea. Send a message here or email me directly.

Srikarmks@gmail.com