cd /news/ai-tools/haunt-the-ghost-in-your-git-history-… · home › topics › ai-tools › article
[ARTICLE · art-161440] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

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

A developer built haunt, an open-source tool that scores a git repository's files for neglect using only git log metrics — dormant, restless, orphaned and debt factors — and labels them as orphan spirits, sleepers, poltergeists or revenants. A local Gemma 2 2B model via Ollama narrates the results in a chosen persona, receiving only metrics and commit subjects and never source code, with a deterministic hash-keyed template fallback so the post-commit hook always exits 0. Run against pallets/click, haunt flagged 175 haunted files, including 84 orphan spirits and a top-scoring test file with 13 authors all inactive for 180 days or more.

by read5 min views1 publishedOct 31, 2026

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine.

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"

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 — 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; 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's install-and-timeout postmortems (hooks that "ran" but never actually run), @lowspecai and @studiomeyer_io on what local inference really costs on CPU, and @bartrozycki 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. 🪦

── more in #ai-tools 4 stories · sorted by recency
── more on @haunt 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/haunt-the-ghost-in-y…] indexed:0 read:5min 2026-10-31 · —