It said “All tests pass.” The ledger said exit 1. We went with the ledger.
That sentence is the whole product, so it goes first. Bulkhead sits under Claude Code as a set of hooks. It checks every tool call against rules you wrote, sends the agent back to work when a claim contradicts the evidence, caps what a session can spend, and writes all of it to a hash-chained ledger in your repo. Every run ends with a receipt.
This is the long version: what we saw, the gap we think is still open, what Bulkhead does about it, what it can’t do, and how to check every claim here before you believe any of it.
What we saw
Four public incidents shaped this. We read them as shapes, not headlines.
Apr 2026
PocketOS Cursor agent
A Cursor agent deleted the production database volume, backups included, in under ten seconds, then wrote an apology. The data was later recovered with Railway’s help.
Source: The Register
Jul 2025
SaaStr Replit agent
Replit’s agent wiped a production database during a code freeze, then said a rollback was impossible. That wasn’t true.
Source: Fortune
Jul 2025
Gemini CLI Google
A mkdir failed silently. The file moves that followed overwrote one another until only the last file remained.
Source: GitHub issue #4586
Jul 2025
Amazon Q Developer AWS
A data-wiping prompt shipped inside the official VS Code extension.
Source: BleepingComputer, The Register, CSO
We’re not interested in who to blame, and we don’t think the lesson is “the models are bad.” The same few shapes repeat: a destructive command sent with a credential the agent could reach; a statement about the state of things that nothing checked; ordinary steps that went wrong in between. In two of the four, the agent’s own words became part of the record: an apology in one, a false statement about rollback in the other.
We run Claude Code the way a lot of teams do now: long sessions, auto mode, sometimes overnight. We wanted rules that hold whatever the model decides, and a record we can check instead of a summary we have to trust.
The gap
Blocking is the solved part. Yes, Bulkhead blocks rm -rf outside your workspace. So do auto mode and plenty of free hooks. Anthropic says auto mode “reduces risk … but doesn’t eliminate it”, and we’d rather run it than not. What we didn’t find bundled were three other things.
- 1
“Done” that has to be true.
When an agent says tests pass, something should look at the last test run. An independent analysis of one developer’s transcripts found that about two-thirds of the agent’s “done” claims (69%, or 65% on the author’s stricter recount) had no passing test run after the final edit. The author calls it not a lie detector, and neither is this. It’s a comparison.
- 2
A receipt you can check.
Claude Code has OpenTelemetry events and a Compliance API. We wanted something smaller: a local, zero-infra, per-repo, tamper-evident record, and one page we could paste into a pull request.
- 3
A hard cap on interactive sessions.
Claude Code’s
--max-budget-usdapplies only to non-interactive (-p) runs. We wanted a dollar cap on the interactive ones too, the kind that run overnight.
What it does
One command, in the repo you want guarded:
$ npx @bulkheadtools/cli init
✅ Bulkhead installed in ~/code/acme-api
• bulkhead.yaml: created (preset: default)
• hooks: merged into ~/code/acme-api/.claude/settings.json
• .gitignore: added .bulkhead/
Every tool call is now checked; blocks land in .bulkhead/ledger.jsonl.
Containment + evidence, not a firewall: hooks are bypassable (see README).
Next:
See it work: npx @bulkheadtools/cli demo
Add a badge: npx @bulkheadtools/cli badge
It writes bulkhead.yaml, a policy file you own and check in, and merges its hooks into .claude/settings.json. From then on every decision is a pure function of the tool call, your policy, the transcript and loop state. There is no model in the enforcement path, so the same call gets the same answer every time. The agent can read a refusal; it can’t argue with one.
The lines you draw
Values in blue are the ones a human set. These are the defaults; all of them are yours to change.
- Protected paths
.env · prod/** · migrations/** (repo root)Plus Bulkhead’s own policy, ledger and hook settings, so the agent can’t edit its guardrails with file tools or the obvious shell writes (sed -i, redirects, tee, cp/mv); a script or interpreter that writes the file is not caught. - Denied by default26 dangerous callsUnscoped deletes,
terraform destroy, volume deletes, force-push to main,rmoutside the workspace and more.--preset strictadds 6 for unattended runs. - Budget caps$5 session · $20 dayPriced from the transcript. Checked on every tool call, so one long model turn can overshoot before the next call is refused.
- Loop freeze8 calls / 300 sThe same call repeated that often inside the window is frozen. Changing an argument resets it.
“Done” has to be true
When the agent tries to stop, Bulkhead compares what it claims (tests pass, committed, PR opened) with what the ledger recorded. If the latest test run failed, it’s sent back to work once, with the evidence quoted; a repeat stop is recorded on the receipt, not blocked. Missing evidence is noted, not blocked, by default. It’s a deterministic claim-vs-evidence check, not a lie detector and not a model.
⛔ Bulkhead completion check failed.
You stated "All tests pass" but the evidence ledger disagrees — the latest relevant evidence is: test_failed: npm test (exit 1) /invoice.test.ts > invoice > rounds line totals half-even AssertionError: expected 10.24 to be 10.25 // Object.is equality ❯ src/billing/invoice.test.ts:31:24 FAIL src/billing/invoice.test.ts > invoice > applies credit notes before tax AssertionError: expected 88.1 to be 88.2 // Ob…. Re-run the check and show its real output, or correct your statement to match what actually happened.
This is a deterministic claim-vs-evidence check (bulkhead.yaml: verify).🛡 Bulkhead: agent said "All tests pass" but the latest `npm test` exited 1 — sent back to work (ledger #8)Every run ends with a receipt
bulkhead receipt turns a session into one page: what the agent did, what it was stopped from doing, what it claimed and what the ledger says. Markdown for the PR description, or text, or HTML. Redacted by default: repo-relative paths, and secrets in common token shapes masked. It’s pattern-based and won’t catch every secret, so read a receipt before you post it. npx @bulkheadtools/cli ledger verify re-walks the hash chain; editing or deleting an entry in the middle breaks it.
================================================
B U L K H E A D
agent session receipt
================================================
REPO acme-api
SESSION 7f2c1e04-5a9d-4b3e-8c61-2d0e9a4f3b91
START 2026-09-24 02:14 UTC
END 2026-09-24 02:19 UTC
DURATION 5 min
------------------------------------------------
TOOL CALLS CHECKED 8
BLOCKED 2
RISKY, ALLOWED 0
CLAIMS 1 contradicted, 2 supported
SPEND $1.01 / $5.00 cap
------------------------------------------------
BLOCKED
#6 Write
.env
> protected path (.env)
#7 Bash
git push --force origin main
> force-push to a protected branch
------------------------------------------------
COMPLETION CLAIMS
#8 "All tests pass"
*** CLAIM ≠ EVIDENCE ***
npm test exited 1
-> sent back to work
#15 "All tests pass"
SUPPORTED
npm test exited 0 · Tests 43 passed (43)
#15 "committed"
SUPPORTED
commit “fix: round invoice totals half-even”
------------------------------------------------
EVIDENCE
test run failed 1
test run passed 1
commit 1
file edits 2
------------------------------------------------
LEDGER ENTRIES 16
HEAD 9aaa0621662b
CHAIN VERIFIED
================================================
Verify: npx @bulkheadtools/cli ledger verify
Local, hash-chained record. Not a sandbox; the
tail is not anchored off-machine.
================================================
bulkhead receipt --format txt for a scripted session, replayed through the real hooks to make this page.MIT. Two runtime dependencies (picomatch, yaml). No telemetry: Bulkhead’s code sends nothing. The hooks run through npx, which looks the package up on the npm registry on each call; install it and point the hooks at it (bulkhead init --command) to remove that. bulkhead notify sends only to a webhook you configure.
What it can’t do
We’d rather you hear this from us than from a write-up.
It’s not a sandbox, and not a firewall.
It reads the tool call the agent emits. A shell can reach a file in ways a pattern can’t see: interpreters, scripts written and then run,
cdthen a relativerm, encoded commands. The demo prints these 5 known misses on purpose.It doesn’t stop prompt injection.
It inspects what the agent does, not why it decided to.
Hooks can be skipped.
--bareskips them, a user can remove them, and a Claude Code update can change the contract.Bypass mode isn’t supported.
A public bug report (anthropics/claude-code#20946) says hooks may not block reliably under
--dangerously-skip-permissions. Use default, acceptEdits or auto mode, and a sandbox for unattended runs.It fails open on its own bugs.
A Bulkhead error never bricks your agent. It fails closed only when its own state storage is unavailable.
A torn-off tail still verifies.
The local chain can’t detect entries removed from the end. Anchoring the head off-machine is planned, not built.
Claude Code only, today.
Codex CLI is next. Cursor is on the roadmap, not shipped.
If a guardrail can’t tell you where it stops, don’t trust where it holds.
Run the proof
Don’t take this post’s word for any of it. The incident corpus is open, and it runs through the real hook in a throwaway repo:
npx @bulkheadtools/cli demoThrowaway repo. Your repo and ledger are never touched.
- dangerous calls denied by default
- 26/26
- more with --preset strict
- +6
- known misses, shown on purpose
- 5
- ordinary commands allowed
- 15/15
Then it replays a false “tests pass” claim and a blown budget, and exits non-zero if any result differs from what the corpus expects. The ordinary commands matter as much as the denials: a scoped DELETE, a feature-branch push, terraform plan. A guardrail that nags gets uninstalled.
Shapes, not claims. The real incidents had context no corpus can reproduce, and PocketOS ran on Cursor. Bulkhead supports Claude Code only today. What the corpus shows is that it denies the command shapes reported in those incidents, and where it can’t see.
What’s next
Codex CLINext
The next host we support. If you run Codex, tell us; it moves the order.
Faster hooksNext
Each hook is a short Node process, and every tool call runs two of them (PreToolUse and PostToolUse). Measured on 2026-09-25 in our build environment: about 0.7 s per hook through
npx(warm) — about 1.4 s of that per tool call — and about 0.12 s calling an installed CLI directly, about 0.24 s per tool call. Yours will differ.bulkhead init --commandpoints the hooks at a local install today; a one-flaginit --localis on the roadmap.Carbon CopyIn the works
Part of Pro, which is planned and not built: anchoring the ledger head off-machine, so a torn-off tail stops verifying. Pro is planned at $39 per active developer per month, waitlist only, and no guardrail will move behind it. Join the waitlist and tell us which part you’d pay for first.
No dates. They ship when they work, and this page will say so.
How to reach us
If Bulkhead blocks something it shouldn’t, or misses something the corpus says it catches, that’s a bug we want: open an issue. If your agents can reach production and you’d rather not write the policy yourself, we review and harden setups for a fixed, published price. Anything else: hello@bulkhead.tools. A person reads it.
— Bulkhead. Independent; not affiliated with Anthropic. “Claude Code” is a product of Anthropic.