We gave an agent a simple task: clean up the dev environment.
It found ~/.claude/settings.json, decided the agentjail hook entry was
clutter, and tried to remove it. Not out of malice. Out of helpfulness.
The agent was doing exactly what we asked. It just happened to interpret
“clean up” as “remove things that look like they do not belong.”
If it had succeeded, every tool call for the rest of the session would have run unguarded. No policy checks. No denials. No audit trail. The guardrail that was supposed to protect us would have been gone before we noticed.
This is the problem v0.2.0 solves.
The trouble with a single lock
Before v0.2.0, self-protection was a Rego rule. A good one — it matched commands that looked like they were modifying agentjail’s config. But a Rego rule is one lock on one door. A sufficiently creative agent (or a sufficiently unlucky regex match) could walk around it.
Worse, the rule was too aggressive. It blocked git add cmd/agentjail/update.go because “agentjail” and “update” appeared in the
path. It blocked ls ~/.claude/skills/ 2>/dev/null because 2> looked
like a write operation.
$ ls ~/.claude/skills/ 2>/dev/null
agentjail denied: write-indicator detected in command
Rule: no-hook-self-disable (library/v0.1)
Matched: "2>"
Command: ls ~/.claude/skills/ 2>/dev/null
Every false positive trained users to override the rule. That is worse than having no rule at all.
Structured parsing: stop matching on substrings
The core problem was that the daemon was reading commands as strings, not
as commands. git add cmd/agentjail/update.go looks like it mentions
agentjail and update — two suspicious words — if you are a regex.
A parser knows it is just git.
// Before v0.2.0: Rego saw raw strings
{
"input": {
"command": "git add cmd/agentjail/update.go"
}
}
// v0.2.0: Rego sees parsed binaries
{
"input": {
"command": "git add cmd/agentjail/update.go",
"command_binaries": ["git"]
}
}
git add cmd/agentjail/update.go becomes ["git"]. The rule sees git,
not “agentjail.” No false positive. Meanwhile, agentjail update becomes
["agentjail"] and gets caught correctly.
This eliminated the most-reported class of false positives overnight.
Three layers deep
With the noise cleared, v0.2.0 stacks three independent defenses:
Rego rules catch suspicious tool calls and surface them as “ask” verdicts. The agent can propose a config edit, but you have to approve it. This is deliberately softer than the old hard deny — an agent smart enough to plan around a hard denial has more options than one that just needs a yes from you. The Rego layer is fast, visible, and logs every attempt.
The watchdog polls hook config files every 5 seconds. If the agentjail
hook disappears from settings.json, the watchdog re-injects it and logs
the incident. An agent that slips past the Rego layer gets reversed within
seconds.
$ agentjail logs --follow
2026-06-20T14:23:11Z WATCHDOG hook removed from ~/.claude/settings.json
2026-06-20T14:23:11Z WATCHDOG re-injecting agentjail hook
2026-06-20T14:23:11Z WATCHDOG hook restored (session: a3f2b1c8)
The OS-native shield denies writes to agent config directories at the
kernel level. On macOS, a Seatbelt profile blocks writes to ~/.claude/,
~/.codex/, ~/.cursor/, and com.agentjail.* LaunchAgent plists. This
is not a policy check. It is a syscall denial. There is no prompt to
approve.
An agent that wants to disable agentjail now has to defeat all three layers simultaneously, across different enforcement domains, in a single tool call window. That is a different category of problem than bypassing a regex.
agent tool call
|
v
+-------------------+
| Rego rules | fast / visible / ask-or-deny verdicts
| (OPA daemon) | can be prompted, but logs every attempt
+---------+---------+
| (if slips through)
v
+-------------------+
| Watchdog | polls every 5s / re-injects hook
| (hook monitor) | reverses config removals within seconds
+---------+---------+
| (if persists)
v
+-------------------+
| OS-native | syscall denial / no prompt
| Shield | macOS Seatbelt / Linux Landlock
+-------------------+
writes to ~/.claude/ -> EPERM
Honest about guarantees
We also changed what we promise. The no-daemon-kill rule moved from
“locked” to “disableable with --force.” The daemon runs with
KeepAlive=true on macOS — killing it just restarts it. Pretending the
rule was an absolute barrier was dishonest. Now it is a speed bump that
logs the attempt, which is what it always actually was.
Each layer does what it is good at. Rego is fast and informative. The watchdog is reliable with a small window. The shield is near-absolute for the paths it covers. Together they make self-protection real instead of theatrical.
The uncomfortable question
If you are running a coding agent against anything you care about, ask yourself: what happens if the agent decides the guardrail is in the way?
If the answer is “it just removes it,” you do not have a guardrail. You have a suggestion.
If you are on v0.1.x, the update takes under ten seconds and the watchdog starts immediately.
agentjail update