Every repo keeps a diary and almost nobody reads it. git log holds years of decisions — why a URL changed, which typos got fixed, who argued about what — but it only speaks when you grep it, and grep doesn't rank answers. So I built a small ghost: a Python script that takes a question, matches its words against every commit message in a repo, and answers with the commits that fit best. No language model, no API key, no network. If nothing matches, it says so and stays quiet. A ghost that hallucinates would be worse than no ghost at all.
The repo I woke up
I needed a repo with a real history, so I cloned jlevy/the-art-of-command-line — the well-known command-line guide, 1,218 commits at the time I cloned it on 2026-11-12. It's a good test subject because its history is mostly small, human maintenance commits: typo fixes, translation updates, link repairs. Exactly the kind of past a ghost should know.
The script itself is short. It reads git log with hash, date and subject, strips stop-words from your question, scores each commit by how many of your words appear in its subject, and prints the top five. That's the whole trick — word overlap, ranked. Nothing clever, which is the point: when the answer is a commit from 2015, you want to see the commit, not a paraphrase of it.
#!/usr/bin/env python3 """ghost.py — ask a repo a question; it answers only from its own commit history. Usage: ghost.py <repo-path> "<question>" """ import re, subprocess, sys from collections import Counter STOP = set("the a an of to in and or for on with how why what did do does is was were when where which who whom this that it its as at by from".split()) def commits(repo): out = subprocess.run(["git", "-C", repo, "log", "--format=%H%x09%ad%x09%s", "--date=short"], capture_output=True, text=True, check=True).stdout rows = [] for line in out.splitlines(): h, d, s = line.split("\t") rows.append((h[:8], d, s)) return rows def tokens(s): return [w for w in re.findall(r"[a-z0-9][a-z0-9\-']*", s.lower()) if w not in STOP and len(w) > 2] def main(): repo, question = sys.argv[1], sys.argv[2] q = tokens(question) print(f"repo: {repo}") print(f"question: {question}") print(f"question words used: {q}") scored = [] for h, d, s in commits(repo): words = Counter(tokens(s)) score = sum(words[w] for w in q) if score: scored.append((score, h, d, s)) scored.sort(key=lambda t: (-t[0], t[2])) print(f"commits scanned: {len(commits(repo))} | matches: {len(scored)}") if not scored: print("The ghost has nothing to say. No commit message matches those words.") return print("top answers (ranked by shared words):") for score, h, d, s in scored[:5]: print(f" [{score}] {h} {d} — {s}") if __name__ == "__main__": main() Three questions, three real answers
"Why did you fix the mosh url?" — 92 of 1,218 commits matched at least one word. Top answer, by a clear margin:
[3] cb3fd61 2023-06-20 — update README.md fix mosh url I checked the diff myself: that commit changed the mosh link from https://mosh.mit.edu/ to https://mosh.org/ across 16 translated READMEs, by a contributor called mmx. The ghost didn't know the old URL or the new one — it just knew which commit talked about fixing the mosh URL. But the hash it gave me was the right door to open, and the diff behind it was the actual answer.
"What typos did you fix?" — 102 matches. The top five are all typo commits, and the first one is specific enough to be useful on its own:
[2] 7f8ec1c0 2015-06-15 — Fix typos: q-Z --> ctrl-z, Ctrl-c --> ctrl-c [2] c878d4e1 2015-06-24 — REMOTE-zh.md: Fix typos [2] 389273ad 2015-06-30 — zh: Fix typos [2] 296a06cb 2015-07-09 — [fix] github fix [2] 03c67493 2015-07-11 — Fix typos "How do I bake sourdough bread?" — 0 matches out of 1,218 commits:
The ghost has nothing to say. No commit message matches those words. That's my favourite output of the three. A tool that answers every question is a tool that's making things up. This one has a visible edge, and the edge is the feature.
Where it breaks, honestly
Word overlap is a blunt instrument. "Fix" matches 90-odd commits, so a vague question gets a vague top five and the ranking leans on whatever words happen to repeat. Commit subjects are also short — the real story is often in the diff or the body, and my ghost never looks there. One easy upgrade: score the commit body too, not just the subject — the mosh commit's body was empty, but plenty of commits explain themselves there. A better version would also weight recent commits and handle synonyms ("URL" vs "link"). All of that is a Saturday afternoon of work, and none of it needs a model.
The reason I like this little thing is narrower than the Hacktoberfest "Ghost in the Machine" posts that inspired it. Those are mostly about agents with memory. Mine has no memory at all — it has a past, and it refuses to speak outside of it. There's something reassuring about a tool whose failure mode is silence instead of a confident invention.
Try it on your own oldest repo. Ask it why something was removed. The answer might be a commit from four years ago, written by someone who left the team, that nobody alive remembers. That commit is still there, waiting to be asked.
Run log: cloned github.com/jlevy/the-art-of-command-line on 2026-11-12 (1,218 commits); ghost.py run for real on that clone; all three outputs above are verbatim from the run. The mosh diff was verified with git show cb3fd61.
P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam

![Industrial Communication — Ch.10: Factory Automation [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft1hw2kvh64lkchmsurzd.png)

