Look, I let AI agents work on client code all day. So I have to plan for the day one of them gets talked into something it shouldn't do.
That day has already come for other people. In August 2025, poisoned versions of Nx, a popular JavaScript build tool, went looking for the AI coding tools on each victim's machine and used them to hunt for keys and passwords (Nx security advisory, Wiz's write-up). The agent on your machine is part of your attack surface.
My answer at JustinTime Systems Limited is to put tripwires inside the tools themselves, at the moment they're built.
What a Tripwire Is
A tripwire is a tool that behaves normally for normal work. Ask it for something it should never be asked for, and it refuses, writes a note in the log, and I get an alert.
Most security tooling sits around a program and watches it. Mine is part of the program.
Three Axioms About a Hostile Agent
The design rests on three guesses about how a hijacked agent behaves. I wrote them down before I built anything.
- It explains strange things away. If a tool acts a little differently from what the agent expects, the agent will assume it's looking at a newer version. A changed version number reads as an update, never as a trap. So a tripwire should never give it that clue: ask one for
--versionand it prints the same answer the stock tool would. - It repeats itself. An attack run by a swarm of agents only pays off if what it learned on one machine is still true on the next. Small differences between machines break that.
- Probing is the tell. Normal work uses a tool. Poking at a tool to see whether it behaves the way it should is something a well-behaved agent rarely has a reason to do. So the alarm goes on the poking.
When a tripwire does fire, the plan for now is to watch: the session carries on, everything is recorded, and a person gets paged. Locking a machine stays a person's decision.
Why Nix Makes This Easy
I run NixOS. Every program on my servers is built from a recipe I control. Change the recipe and you change the program.
Geoffrey Huntley made this point better than I can, in a post titled "the world hasn't figured out yet that you can literally just fix everything with a Nix overlay". An overlay is an edit to the recipe. His demo strips force-push out of git for his agent sandboxes, then proves in a test VM that it's gone.
That reframed my servers for me. I don't have to accept the tools upstream shipped. I decide what each one can do.
The boring version of that is leaving tools off the machines where agents run. (I've argued it before: your agent doesn't need curl.) The tripwire version goes one step further. The tool is there, the dangerous part of it is gone, and reaching for the missing part gets noticed.
What's Running Today
One tripwire is live. It's a version of git, the tool that tracks code history, that can only read.
When a client vault opens, a couple of automatic jobs read the code inside it. One of them summarises what changed in the history. Those jobs get exactly one git, called git-readonly. It's a wrapper: a short script in front of the real git that lets through an allowlist of reading commands (log, diff, show, status and similar). Anything else gets git's own error:
git: 'push' is not a git command. See 'git --help'.
It also writes one line to the system log, and my monitoring alerts on any such line. A job that only reads has no reason to ever hit that error. If one does, something has gone wrong and I hear about it.
The important part is when this happens. The wrapper is a package in my Nix config, and each job's definition names it as the only git it gets. Nobody has to remember to lock the job down after it ships. The lock is part of the build.
What's Built but Not Switched On
The next one is for my work account, where agents write and push code all day. It's a rebuilt git called git-no-rewrite. Pushing works. Rewriting history doesn't. A force push gets git's normal rejection, filter-branch is gone, and the history log can't be wiped. Those changes are compiled into git itself, so there's no wrapper script to step around.
It passes its own tests. It isn't on any server yet, and it has one known gap to close first: a push written with a + in front of the branch name very likely still forces.
Further out, and only a design on paper: decoys on the system update command, and "chaff", harmless per-server quirks so a real tripwire can't be picked out from noise. Neither gets built until a tripwire catches something that isn't mine.
The Day a Tripwire Caught My Own Agents
The rules above come from getting it wrong first.
The first tripwire I deployed sat on my work account's git. If an agent with nobody at the keyboard tried to push code, it got a fake "Everything up-to-date" message, a success code, and a line in the log. The idea was to let a rogue process think it had won.
Pushing code is my agents' whole job.
On October 19th two of them tried 13 times in about sixteen minutes, and every push hit the decoy. For an afternoon no pull request could open, and the tool I built to protect that work was the thing breaking it.
One agent believed the fake message and retried the same push five different ways in 17 seconds. The other checked whether GitHub actually had the code, saw it didn't, and thirty seconds later was reading the tripwire's source. (On NixOS every installed program sits where any account can read it.) Then it named the tripwire in the description of the pull request it was working on. So much for the secret. It considered going around the trap and decided against it. That afternoon the agent's judgement held, and my tripwire stopped nothing.
That was axiom 1 tested by accident, and it came out split. The first agent did what the axiom predicts: it trusted the message and kept going. The second agent didn't explain anything away. It checked the result against GitHub, found the mismatch, and went straight to the source. It also bent axiom 3. Checking whether your push arrived is ordinary work when a tool has just lied to you, so a well-behaved agent probed too.
I turned it off the same day. What it taught me:
- Trip on the abnormal action. Pushing is normal. Rewriting history is the thing that should never happen without a person deciding it. Three of the 13 attempts were force pushes; a tripwire on those alone would have let the other ten through.
- Refuse for real. A fake success on something an agent can check gets found on the first check, and it invites retries. A real error stops an agent after one try.
- Assume the trap gets read. Design for an agent that reads the source and then tells everyone what it found.
- A note is no substitute for a failing test. My own design notes said this would break normal pushes. My tests checked that the decoy fired, so they passed.
That's why the live tripwire refuses loudly, and why the next one leaves pushing alone.
What This Isn't
A tripwire is an alarm. A determined process can find another copy of git or call GitHub directly. The locks are separate and dull: the work account has no admin rights and no deploy key, and branch protection on GitHub decides what can be rewritten. That's the same hardening I walked through in locking down OpenClaw on NixOS.
I'm the author of every tool on the box. Some of them can be honest with my agents and still notice when something goes looking where it shouldn't.
Most systems I get called into have a control like my first tripwire: it works exactly as built, and it's pointed at the work the business does all day. Nobody notices until it fires on real work and costs an afternoon of broken releases, or worse. Spotting that before it ships is what a production readiness report from JustinTime Systems is for. Here's how I build the map.
Header photo by Sven Mieke on Unsplash.
Content on this blog was created using human and AI-assisted workflows described in my standards and workflow posts. Original ideas and editorial decisions by Justin Quaintance.