# haunt: the ghost in your git history — a local Gemma 2 whisper for your repo's graveyard

> Source: <https://dev.to/abhinav412/haunt-the-ghost-in-your-git-history-a-local-gemma-2-whisper-for-your-repos-graveyard-3ndg>
> Published: 2026-10-31 09:13:13+00:00

This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26).

Every repository is quietly becoming a graveyard. Files nobody remembers writing. Tests nobody dares delete. TODOs aging like gravestones.

**haunt** is a ghost for that graveyard — and it lives in git, not in a terminal pasted on top. Two small rituals, entirely offline:

```
pip install .          # then, inside any git repository:
haunt install          # the whisper joins your post-commit hook
git commit -m "tomb, again"
#   🕯️ Ah, the echoes of lost brilliance in those dusty files, a symphony of
#      potential forever unplayed. The phantom hand of progress reaches out
#      to erase... but will it leave behind anything but shadows?

haunt scan             # which files are the most haunted, and why
haunt report --html    # a narrated HAUNTINGS.md of the worst hauntings
haunt persona ancestor # swap the voice: victorian, poltergeist, ancestor
```

Three design choices make it a *ghost in the machine* rather than an AI plugin:

`git log` alone — no model, fully explainable:

```
   dormant  = min(age_days/365, 1) × min(size/500, 1)        (w 0.35)
   restless = min(churn_90d/20, 1)                            (w 0.25)
   orphaned = 1 if every author inactive 180d+                (w 0.25)
   debt     = min((TODO FIXME HACK XXX)/10, 1)               (w 0.15)
```

Scores name their ghosts: 🫥 **Orphan spirit** (all authors gone), 🕸️ **Sleeper** (old, quiet), ⚡ **Poltergeist** (high churn, many hands, no owner), 🕯️ **Revenant** (dormant for months, then suddenly touched).

**The model only narrates; it never reads your code.** Gemma 2 2B (open weights, via Ollama) receives *metrics and commit subjects* — never source files (`send_snippets` is off by default). A 2B model can't review code, so haunt never asks it to. It asks for what a 2B model is actually good at: voice.

**The whispers never block a commit.** One generate attempt inside a time budget; if the model is cold, busy, or absent, a deterministic template keyed to the commit hash takes over — same commit, same line, forever. No retries, no queues. The hook always exits 0.

`haunt scan` on [pallets/click](https://github.com/pallets/click) — a 14-year-old, very-much-alive open-source CLI library — finds **175 haunted files**. The top haunting:

```
score   type             age    churn90  authors  debt  loc  file
0.59    🫥 Orphan spirit  354d   0        0/13     0     782  tests/test_context.py
```

Thirteen authors. **All of them inactive.** 354 days without a commit. The file has a funeral attended by no one. The full tally: **84 orphan spirits, 81 sleepers, 10 poltergeists** — a repo at rest that nobody would call haunted until you read the blame.

And with the ancestor persona — built for Día de los Muertos, the week's truth that this Halloween stretch is also about honoring the past:

🪔 *"Their efforts bring warmth and joy to these old halls. May their spirits find solace in the stories shared through this code."*

The poltergeist handles the noisy places:

⚡ *"More flickering, more ghosts. Just don't break a fingernail."*

Graceful degradation, demonstrated by literally killing Ollama mid-session and committing:

```
🕯️ A chill passes through the repository. The commit is buried without ceremony.
```

Same line twice, deterministically keyed to the commit hash — and the hook cost the commit nothing.

And when the report narrates with Gemma — here is haunt's HAUNTINGS.md on that same repo, Victorians mourning real files:

`age 354d | churn90 0 | authors 0/13 | TODO/FIXME 0 | 782 loc`

*"The pages flutter, whispering tales of what could have been. An echo of promises left unfulfilled, a spectral symphony in the grand ballroom of code. Time slips away like dust, leaving only emptiness where brilliance once resided. Give this ghost a purpose: delete, add a test, document it, or assign an owner to finally give it meaning, however fleeting."*

The model honestly ends with what the *metrics* justify — every narration closes with a grounded directive, not vibes. On a lonely orphaned doc it got almost tender:

*"Dust motes dance in the flickering light, each one a memory of a lost chapter, a discarded dream. So many stories whispered between lines of code, now gathering dust like forgotten souls. A quiet voice asks: 'Shall I erase its echo or preserve its fragile remnants?' Name an owner for this forlorn document; at least grant it some solace."*

Python 3.13, `typer`, `httpx`, and raw `git` subprocesses. No GitPython, no LLM framework. [The repo is here](https://github.com/Abhinav412/haunt); MIT licensed; 24 tests pass, including a test that runs a *real* `git commit` end-to-end through the installed hook and asserts the whisper appears.

`git log --format=<sep>%H<sep>%at<sep>%an<sep>%s<sep> --name-only` feeds the entire scanner — records joined on `\x1e/\x1f` unit separators. Small saga: Python's `str.splitlines()` `\x1e` as a line boundary and silently shred every record; `split("\n")` keeps the ghosts intact.`num_ctx` pinned at 4096 — Ollama's silent default of 2048 clips the persona+stats prompt without telling you. `keep_alive=30m` and an install-time warm-up so the first whisper fits its budget.`haunt install` writes a marker block (`>>> HAUNT BEGIN … <<< HAUNT END`) with an absolute interpreter path and a `PYTHONPATH` fallback to `python -m haunt` — the hook works in any repo without `pip install` — and backs up any existing hook it finds.
Three posts from the community shaped these calls: [@enjoy_kumawat](https://dev.to/enjoy_kumawat)'s [install-and-timeout postmortems](https://dev.to/enjoy_kumawat/my-ai-commit-hook-got-a-timeout-fix-and-an-install-fix-it-had-never-once-actually-run-45pa) (hooks that "ran" but never actually run), [@lowspecai](https://dev.to/lowspecai/tokens-per-second-is-a-memory-bandwidth-number-measure-it-in-2-seconds-1omf) and [@studiomeyer_io](https://dev.to/studiomeyer_io/local-llms-in-2026-what-actually-works-on-consumer-hardware-50me) on what local inference really costs on CPU, and [@bartrozycki](https://dev.to/bartrozycki/when-retries-timeouts-and-fallbacks-start-interacting-23gc) on how retries and fallbacks compound into their own failure mode — which is why haunt has exactly one attempt and one fallback, no interaction between them.

Then reality kept the honesty flowing:

`UnicodeEncodeError` inside the Haunt's premise is that an AI reads your *private diffs* to comment on them. Closed models make that premise toxic: your repo's history would live in someone else's logs forever. With **Gemma's open weights on-device**, the only witness is a ghost, and it never leaves your machine.

`ollama pull gemma2:2b` — and any other Ollama model slots in via one config line. Personas are TOML files; localize the ancestor voice for your team's history. And the fallback templates are auditable, first-party Python — even "no model" is an inspectable code path here.
That last point is the open story in miniature: when the proprietary link dies, dignity dies with it. When the open link dies, you still get a whisper, just a quieter one.

The repository is never truly empty. It is merely quiet. 🪦
