Projects
The Tripwire
A control you have to remember to run isn’t a control. It’s a good intention.
The problem
I run my own infrastructure — an automation server, an LLM gateway, an agent host, and the repository that records every change made to any of it. One person operates the whole thing. That’s the constraint that actually matters. There is no security team, no Monday review, nobody whose job is to notice that an .env file slipped into a commit three weeks ago and has been sitting in version control ever since.
Security posture doesn’t fail loudly. It drifts. A .gitignore that covered everything in March stops covering the new thing you added in May. A certificate that had ninety days on it has nine. Nothing announces any of this. You find out during an audit, or you find out the bad way.
The obvious fix is discipline: check it regularly. But discipline is the component that fails. I’m the single point of failure in my own review process, and I’d be reviewing my own work, which is the least reliable inspection there is.
So the job wasn’t to check more carefully. It was to remove myself from the loop.
The system
177 lines of bash on a timer. Every five days it wakes up, scans, writes a dated report, and files it.
It checks for the things that actually go wrong: sensitive files tracked in version control, gaps in .gitignore coverage, credential-shaped strings in tracked text, credential files sitting on disk readable by the wrong users, and certificate expiry. Every finding lands in one of three tiers: green, yellow, red. The report takes the worst one. Red means act now. Yellow means scope a fix. Green means nothing needed.
Three decisions did most of the work.
The scanner can’t keep a secret. A tool that hunts for exposed credentials must never itself become the place a credential gets written down. Findings cite a file path, a line number, and the name of the rule that fired. Never the matched text. That isn’t a policy I follow: it’s enforced mechanically, in the pipe. The scanner’s output is cut down to file:line before anything is stored, and everything after that is discarded. There’s no path where the secret survives long enough to be written anywhere.
It also has to skip itself. The script contains every pattern it’s searching for, so scanning its own source would flag it on every run. The tool that looks for problems is the one thing it can’t look at.
Dependency-free on purpose. There are better scanners. gitleaks does entropy analysis and carries 150+ rules, and it would catch things mine won’t. It’s also another binary to install and keep updated on the box I’m trying to keep clean. Plain regex catches the formats that actually leak: provider key prefixes, private key blocks. Nothing added to the supply chain. That’s a real tradeoff, not a free win. The report states it in its own output every time, including a warning: the loose pattern matches produce false positives and need verifying before anyone acts on them. A tool that overstates its own coverage is worse than one that doesn’t run, because you trust it.
Delivery was the easy part, once I stopped trying to build it. No dashboard, no alerting service, no new account. The script commits the report and pushes it. GitHub emails me the digest. I didn’t build a notification system — I borrowed one I already read every day.
What changed
The first run, on June 3, came back yellow. One finding: .gitignore was missing coverage for *.local. Small, boring, and exactly the kind of thing that stays invisible right up until it doesn’t. Fixed the same day.
Every run since has been green. Thirteen of them, across two months. Each one committed to the repository with its date and its verdict, which means the history isn’t a claim I’m making. It’s a record anyone can read start to finish.
That distinction is the whole point. Before, if someone had asked how I knew my infrastructure was clean, the honest answer was that I’d looked recently and it seemed fine. Now the answer is a directory of dated reports.
There’s a failure mode in this kind of thing worth naming: a long wall of green is its own hazard. You stop reading. So each report diffs itself against the previous one and prints a status-change banner when the verdict flips. The steady state is designed to be boring. The exception is designed to be loud. If I ever stop reading the green ones, the system still works.
The detail I like most wasn’t designed. On July 16, the certificate countdown went from 33 days remaining to 87. Renewal had fired on its own, exactly as it was supposed to. Nobody told the scanner to watch for that. It just records the number every five days. But sitting in the audit trail is proof that a different piece of automation did its job, on a day I wasn’t watching either of them.
That’s the version of this I’d want on any system I’m responsible for. Not a tool that makes me more careful. A tool that notices on a schedule I don’t control, reports into a channel I already read, and leaves a record I can hand to someone who asks.