{"slug": "turns-out-i-m-the-one-making-the-ghosts", "title": "Turns out I'm the one making the ghosts", "summary": "A developer built Ghosts in the Machine, an open-source tool that walks a public repository's git history to surface silent failures that never produced an error, such as files committed empty and filled months later. Deterministic rules and heuristics flag zero-byte blobs, credential-shaped lines, size collapses and mismatched diffs, and an open-weight Gemma model then groups, ranks and explains the findings alongside the raw git record for verification. Running it on the developer's own repos surfaced a seed.sql committed empty and filled 78 days later, plus empty requirements.txt and readme.md files.", "body_md": "*This is a submission for the [Hacktoberfest Open-Source AI Challenge: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26)*\n\nOn 8 August I committed a file called `seed.sql`. On 25 October I filled it in.\n\nFor 78 days it was zero bytes. Nothing ever told me. The file existed. The\n\nimport ran. The app worked. Only the data was missing — and an empty file\n\ndoesn't raise an error. It just sits there being nothing, very quietly, for two\n\nand a half months.\n\nI found out while deploying, a few days ago. So I built something to go looking\n\nfor whatever else was in there.\n\n**Ghosts in the Machine** reads a public repository's git history and finds\n\nfailures that never produced an error — the ones nobody noticed, because there\n\nwas nothing to notice.\n\nPoint it at a repo. It walks the commits, finds what broke silently, and an\n\nopen-weight model groups it, ranks it, and writes up what happened.\n\n```\n   a public repo\n        │\n        ▼\n ┌─────────────────┐\n │  walk history   │   every commit, every diff\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │  rules          │   no AI. zero-byte blobs,\n │  (deterministic)│   credential-shaped lines\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │  oddness        │   no AI. size collapses, resurrected\n │  (deterministic)│   paths, diffs that don't match\n └────────┬────────┘   their own message\n          ▼  findings, with commit hashes\n ┌─────────────────┐\n │  Gemma          │   did this break? which of these\n │  (open weights) │   matter? how do I explain it?\n └────────┬────────┘\n          ▼\n ┌─────────────────────────────────────┐\n │  what the model made of it          │\n ├─────────────────────────────────────┤\n │  what git actually recorded         │  ← both shown, so you can\n └─────────────────────────────────────┘    check one against the other\n```\n\nWhen I ran it on my own repositories:\n\n`seed.sql` — committed empty, filled 78 days later`requirements.txt` — committed empty in the initial commit, filled 79 days later`readme.md` — committed empty, by a commit whose message was `manifest.json`, empty, which I later deleted without ever noticing it held nothing\nEvery one of those is mine. None of them ever failed.\n\nThe organisers suggested a haunted terminal and an offline oral history tool. I\n\nwent elsewhere. I kept coming back to what a ghost actually is: something\n\npresent, that nobody can see, that everyone has stopped looking for.\n\nThat describes a file committed empty exactly. It's in the tree. It's in every\n\nclone. It's in the history permanently. And because it never errors, nobody is\n\nlooking.\n\nThe repository is the haunted house. The commits are the record of who lived\n\nthere. I just read the record.\n\nThe case I kept thinking about isn't mine. It's inheriting a repo.\n\nYou join a team, or pick up something abandoned, and you want to know: what's\n\nwrong with this that nobody is going to tell me? The README is aspirational.\n\nThe code is current. The history holds the real answers, and nobody reads 500\n\ncommits to find them.\n\nNobody hands over a project with a list of everything quietly broken in it.\n\nPartly because it would be awkward. Mostly because they don't know either.\n\nThat's the whole point — these failures produced no error, so the person who\n\nmade them is as unaware as the person inheriting them.\n\nThe same goes for open-sourcing something, or handing a repo to a client.\n\nWhat's in the history that shouldn't be? Secrets survive deletion, and people\n\nusually find that out afterwards.\n\n→ [ghosts-in-the-machine.onrender.com](https://ghosts-in-the-machine.onrender.com/)\n\nTwo exhibits load on arrival, from my own repositories. Paste in any public\n\nrepo and it will read that one instead.\n\nIt's a free instance, so the first request after a quiet spell takes a moment\n\nto wake up. Small and medium repos come back in under a minute. Large ones take\n\nlonger, because cloning dominates.\n\n→ [github.com/VidhiDixit2000/ghosts-in-the-machine](https://github.com/VidhiDixit2000/ghosts-in-the-machine)\n\nTwo layers, deliberately separated.\n\n**The deterministic layer** walks the history and emits findings. No AI. It\n\nchecks whether a blob is zero bytes when the file extension implies content,\n\nand whether added lines look like credentials. Facts, with commit hashes\n\nattached.\n\n**The model layer** answers a different question: which of these matter, which\n\nare the same story, and how do I explain them to a person? That's Gemma, via\n\nOpenRouter — open weights, hosted inference, so the demo runs without a GPU.\n\nThe split isn't tidiness. Each layer does what the other is bad at.\n\nA regular expression can't tell you an empty file mattered. It has no idea\n\nwhether 78 days is a long time. Ask a model to scan a whole history instead and\n\nit invents findings, misses real ones, and burns tokens doing it.\n\nHere's the split earning its keep. One repo produced three separate findings: a\n\nconnection string in `database.py`, a `DATABASE_URL` in `docker-compose.yml`,\n\nand a `POSTGRES_PASSWORD` in the same file. Three true findings. One story —\n\nthe same password, three places, one commit. The rules found them. Only the\n\nmodel saw they were one ghost.\n\nOn the page, both are shown. **What the model made of it** sits above **what git actually recorded**, so you read the narration and then check it against\n\nThat mattered more than I expected.\n\nWhile I was tuning the narration prompt, Gemma 3 12B wrote this about the\n\ncredentials:\n\nThese values... were removed from the working tree **before the commit**.\n\nHowever, they remain present in the repository's history.\n\nThat's impossible. Git history contains what was committed. If something was\n\nremoved before a commit, that commit never recorded it — it wouldn't be in the\n\nhistory at all. The sentence contradicts itself.\n\nThis was `google/gemma-3-12b-it` at temperature 0.3.\n\nWhat actually happened: committed 30 July, removed 25 October. Nearly three\n\nmonths apart. The findings said so correctly. The model garbled them on the way\n\nto prose.\n\nI only noticed because the evidence was sitting right underneath. If the\n\nnarration had replaced the findings instead of sitting beside them, I'd have\n\nshipped a well-written falsehood about my own repository.\n\nMoving to Gemma 3 27B and lowering the temperature fixed that particular error.\n\nBut the architecture caught it, not the model choice.\n\nIf you're building on a model: test across versions *and* temperatures. A wrong\n\nanswer at 0.3 looks completely reasonable until you check it against something\n\nthat can't be wrong.\n\nI started by writing rules for failures I'd personally hit. Empty files,\n\nbecause of `seed.sql`. Leaked credentials, because of a `.dockerignore` I\n\nforgot and a password I thought I'd deleted by commenting it out.\n\nThen I saw what I'd built: a tool that finds failures someone has already met.\n\nFor a project about *failures nobody noticed*, that's close to self-defeating.\n\nA rule per failure type, forever, and only ever the ones already on the list.\n\nThe way out isn't more rules. There are infinitely many. It's changing what the\n\nrules are *for*.\n\nGit's vocabulary is tiny. A file appeared, vanished, changed, or moved. Its\n\nbytes grew or shrank. That's nearly all of it. So however many ways software\n\ncan break, a failure has to express itself in that vocabulary to be visible at\n\nall — and it lands in one of a few shapes:\n\nTruncated JSON and a gutted `requirements.txt` are different failures with the\n\nsame shape. A case-only rename and a resurrected file are different failures\n\nwith the same shape.\n\nSo I added a second deterministic layer that names nothing. Six measurements:\n\nsize collapse, a file too small for its extension, a resurrected path, two\n\npaths differing only in case, a large diff under a short message, a change\n\nthat's almost all deletion. Each scores how *unusual* a commit is. The dozen\n\noddest go to the model in one call, and the model says what — if anything —\n\nwent wrong.\n\nLike a metal detector. It doesn't know what a coin is. It knows there's metal\n\nhere. Someone else digs.\n\n**The signal set is bounded by what git can record. What the model can name isn't.**\n\nHere it is working. On a test repo, the oddness layer flagged a `.json` file\n\ndeleted inside a 14,000-line commit. Neither rule would ever have caught that —\n\nit isn't empty and it isn't a credential. It surfaced because it was *strange*.\n\nAnd then the model explained it as an attempt to hide evidence.\n\nBoth halves of that sentence are the point.\n\nTo be honest about what this is: those six measurements are still rules. I\n\nwrote them by hand and picked every number in them. What changed is the ratio.\n\nOne rule in the first layer finds one failure type, forever. One measurement in\n\nthe second finds a truncated JSON, a gutted manifest, a half-written migration,\n\na botched merge — failures I've never thought of and don't need to.\n\nThe rules didn't disappear. They stopped growing with the problem.\n\n**This tool reads your repository.** Every commit, every diff, every line that\n\never held a credential. If it's going to do that, you should be able to read it\n\nback: what it looks for, what it sends to a model, what it redacts before\n\nsending. The code is public for the same reason the evidence sits beside the\n\nnarration. A tool making claims about your history should be checkable.\n\n**Open weights meant I could reason about failure.** When Gemma produced that\n\nimpossible sentence, I could see which model, which version, what temperature,\n\nwhat prompt. I changed two of them and watched the failure go away. That's a\n\ndebugging loop. Behind an opaque API it would have been a shrug.\n\n**Two named rules, six measurements.** Most well-maintained repositories come\n\nback empty, correctly. The page says so rather than pretending otherwise.\n\n**It doesn't read a whole repository.** Commit depth is capped, and the page\n\ntells you when a history was truncated.\n\n**Large repos are slow.** Cloning dominates, not scanning.\n\n**The model reads intent into odd commits.** On a repo called `buggy-python`,\n\nwith a commit message reading \"write buggy code\", it described a deleted JSON\n\nfile as an attempt to hide evidence. The measurement was right that something\n\nunusual had happened. Everything after that was the model writing a story.\n\n**The narration isn't stable.** Same repo, same prompt, same temperature: one\n\nrun called a deletion \"possibly intentional experimentation\", the next called\n\nit \"a deliberate attempt to hide evidence\". No code changed in between.\n\n**It invents chronology inside a single commit.** A commit is a snapshot —\n\nthe changes in it have no order. I told the model that in plain words. It\n\nrepeated it back correctly (\"both changes were recorded in the same commit\")\n\nand then wrote, one sentence later, that one happened immediately after the\n\nother. It isn't ignoring the rule. It states the rule and breaks it in the\n\nsame paragraph.\n\n**Secret detection is shallow** next to dedicated tools. The quiet-failure\n\nhalf is where the gap is.\n\nEvery one of those is visible on the page, because the evidence sits under the\n\nnarration. That isn't a coincidence. It's why the page is built that way.\n\nSome named rules will always be worth keeping. A dependency manifest losing\n\nmost of its contents is worth catching directly rather than inferring. But they\n\nshouldn't live in one growing file of conditionals.\n\nThe shape that scales is a small registry. Every rule implements the same\n\ninterface — does this apply to this change, and if so what does it find — and\n\nevery rule returns findings in one format. Adding a rule then costs nothing\n\nstructurally. Rules that don't apply to a file never run against it. Each\n\ncommit is parsed once and shared. Duplicate findings collapse by fingerprint.\n\nOne rule failing doesn't take the scan down.\n\nA registry to make named rules cheap, and oddness scoring to make most of them\n\nunnecessary. The first bounds the maintenance cost. The second bounds how many\n\nyou need at all.\n\nI'm the kind of person who commits functionality in pieces. Somewhere in all\n\nthat pushing, something ends up in a file that shouldn't be there. There are\n\nmistakes in every repo I've shipped that I'd only see if I stepped away, came\n\nback, looked properly, and went *wait, what?*\n\nSo I built something to look. It spent most of its time looking at mine.\n\nThe one that got me wasn't the empty files. It was the password. I'd found it\n\nweeks earlier — a connection string in `database.py` — and I commented it out,\n\nfelt responsible, moved on. My own tool later showed me that same line, in a\n\ncommit from July, with the `#` still on the front. The comment character meant\n\nnothing. Git had kept it the whole time, and so had every clone anyone had ever\n\nmade.\n\nThen the same password twice more, in a file I hadn't thought about. Three\n\ncopies of one mistake, and I'd only ever found one.\n\nSomewhere in there it stopped feeling like I was finding ghosts and started\n\nfeeling like I'd been making them.\n\nThe 78-day empty file is the one I tell people about, but it isn't really a\n\nmistake. Nobody did anything wrong. A file was committed before it had content,\n\nand there was no moment at which anything would have said so. It sat there, in\n\nevery clone, for two and a half months, being nothing.\n\nThat's what I find unsettling about git. Your repository remembers everything\n\nyou did and tells you none of it unless you go and ask. Every project I've\n\nshipped has things in it I don't know about yet.\n\nWhich means the next person to open one of my repos wouldn't know either. That\n\npart I hadn't thought about until I was looking at the output.\n\nBuilding a ghost detector and finding five of your own is a strange way to\n\nspend a couple of days. I commit differently now.", "url": "https://wpnews.pro/news/turns-out-i-m-the-one-making-the-ghosts", "canonical_source": "https://dev.to/vidhidixit2000/turns-out-im-the-one-making-the-ghosts-3oij", "published_at": "2026-10-31 22:43:09+00:00", "updated_at": "2026-10-31 22:51:54.936539+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents", "large-language-models"], "entities": ["Ghosts in the Machine", "Gemma", "GitHub", "Hacktoberfest", "VidhiDixit2000"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/turns-out-i-m-the-one-making-the-ghosts", "markdown": "https://wpnews.pro/news/turns-out-i-m-the-one-making-the-ghosts.md", "text": "https://wpnews.pro/news/turns-out-i-m-the-one-making-the-ghosts.txt", "jsonld": "https://wpnews.pro/news/turns-out-i-m-the-one-making-the-ghosts.jsonld"}}