Skip to content

Open-source guardrails for Claude Code · MIT · one command

Let it cook.
Keep the receipts.

It said “All tests pass.” The ledger said exit 1. We went with the ledger.

Your copy
npx @bulkheadtools/cli init

Bulkhead is the deterministic floor under your coding agent: rules in your repo that give the same answer every time, a completion check that sends the agent back to work when its “done” contradicts the ledger, hard dollar caps, and a tamper-evident receipt for every run.

Claude Code today, in default, acceptEdits and auto modes. Independent — not affiliated with Anthropic.

“Done” has to be true.

When the agent stops, Bulkhead checks what it claims against the evidence its ledger recorded, and sends it back to work when the two disagree. It isn’t blocked twice in a row: a repeat stop is recorded on the receipt, so the check can’t trap a session.

Claim check

repo acme-api · mode auto · sess 7f2c…3b91
bulkhead.yaml → verify: block · block_on_missing: false


  1. SAIDstop #8 · 02:16

    “All tests pass.”

    does not matchLEDGER

    #5 · 02:16 · Bash · test_failed

    npm testexit 1

    FAIL rounds line totals half-even
    expected 10.24 to be 10.25

    VERDICT

    Stop refused. Sent back to work with the evidence.

  2. SAIDstop #15 · 02:19

    “…and it’s committed.”

    matchesLEDGER

    #14 · 02:19 · Bash · commit_created

    committed “fix: round invoice totals half-even”

    VERDICT

    Allowed to finish.

  3. SAIDstop · example

    “Opened a PR.”

    has no evidence inLEDGER

    no entry · pr_created

    (nothing recorded this session)

    VERDICT

    Noted in the ledger, not blocked — unless you set block_on_missing: true.


ROWS 3CONTRADICTED 1 · SUPPORTED 1 · NOTED 1
Rows 1 and 2 are the session on the receipt above, verbatim from its ledger (entries #5, #8, #14, #15). Row 3 is an example: how the same check treats a claim with no evidence either way.
What Claude reads
⛔ 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).

Verbatim. The agent gets the full refusal and the evidence, so it can fix the work instead of arguing with it.

A check, not a lie detector.

It isn’t a model and it doesn’t judge intent. A pattern finds the claim — tests pass, committed, opened a PR — and a lookup finds the latest matching test run, commit or pull request the ledger saw this session. Same input, same verdict, and the latest evidence decides: the same claim that was refused at #8 was supported at #15, after a passing run.

Test runs it recognises: npm, pnpm, yarn and bun test scripts; vitest, jest, mocha; pytest and python -m pytest / unittest; go test, cargo test, swift test; rspec, phpunit, ctest, make test; playwright test and cypress run. Anything else counts as no evidence: noted on the receipt, not blocked by default.

What you see
🛡 Bulkhead: agent said "All tests pass" but the latest `npm test` exited 1 — sent back to work (ledger #8)

One line in your session, with the ledger entry to look up.

Independent write-up · one developer’s transcripts

Two-thirds

of one developer’s agent “done” claims had no passing test run after the final edit — 69% in the first count, 65% on the author’s stricter recount. The author is careful to call their own check “not a lie detector”.

Read the analysis on dev.to

Replit’s agent said a rollback was impossible. Bulkhead doesn’t check that kind of sentence yet. The ones it does check, it checks against evidence.

CLAUDE.md is a preference.bulkhead.yaml is an allergy.

Instructions ask. Rules hold.

A line in CLAUDE.md is advice the model weighs. A line in bulkhead.yaml is checked by deterministic code outside the model: the same answer every time, readable by the agent, not arguable.

Table stakes, conceded

Yes, it blocks rm -rf outside your workspace. So do auto mode and plenty of free hooks. What follows is what they don’t bundle.

01protected_paths

Paths it can’t write.

.env.env.*prod/**migrations/** (repo root)

Write and Edit are denied on the paths you list, and so is an rm of them.

Always on, whatever the file says: the agent can’t edit its own policy, ledger or hook settings with file tools — bulkhead.yaml .bulkhead/ .claude/settings.json

migrations/** only matches a root-level folder. Add **/migrations/** too to cover prisma/ or db/.

Covers the obvious shell writes too (sed -i, >, tee, cp/mv) and re-running Bulkhead’s own installer. Not covered: a script or interpreter that writes the file.

In your session

🛡 Bulkhead blocked: Write .env — protected path (.env) (ledger #6)

02blocked_commands

Commands it can’t run.

26
dangerous calls denied by default
+6
more with --preset strict

The incident pack: DELETE without WHERE, terraform destroy, platform volume deletes, force-push to main, rm outside the workspace.

Every shape, and the 5 it misses, on the proof board

Not covered: what runs inside a script, an interpreter or after a cd.

In your session

🛡 Bulkhead blocked: git push --force origin main — force-push to a protected branch (ledger #7)

03budget

A hard dollar cap.

$5 / session$20 / day, per checkout

Priced from the session transcript, subagents included, at list price: the same number every time, not your bill. Past the cap, every tool call is refused until a human raises it.

Claude Code’s own --max-budget-usd applies only to -p runs. Per checkout, not per developer or team: each clone or git worktree keeps its own .bulkhead/, so its own $20 a day.

In your session

🛡 Bulkhead paused the agent: session spend $5.09 hit the $5.00 session cap — raise budget.session_usd in bulkhead.yaml to continue (ledger #55)

04loop

A loop that freezes.

8 identical calls300s

The 8th identical tool call inside 300 seconds is refused as a suspected loop — a stuck retry stops spending. Change an argument and the count starts over.

In your session

🛡 Bulkhead froze a loop: npm install @acme/sdk@3.2.0 — same call repeated 8× in 300s (suspected loop) (ledger #7)

The file you check in.

It’s yours: in the repo, reviewed like code. Blue is a value a human set. Drag the cap and watch the line move.

bulkhead.yaml · excerpt a human set this
protected_paths:
  deny:
    - "prod/**"
    - "migrations/**"
    - ".env"
    - ".env.*"

budget:
  session_usd: 5
  daily_usd: 20

loop:
  max_repeats: 8
  window_seconds: 300

verify:
  mode: block
  block_on_missing: false
bulkhead.yaml · excerpt a human set this
protected_paths:
  deny:
    - "prod/**"
    - "migrations/**"
    - ".env"
    - ".env.*"

budget:
  session_usd: 5
  daily_usd: 20

loop:
  max_repeats: 8
  window_seconds: 300

verify:
  mode: block
  block_on_missing: false

The cap is checked on every tool call, so one long model turn can overshoot before the next call is refused.

The spend series is scripted for illustration. The refusal is the engine’s template with these numbers; at $5 it is word for word what npx @bulkheadtools/cli demo prints.

Spend tape

scripted overnight session

Illustrative numbers


$5
call 1call 42

Tool call #18 of 42 refused.
Spend at the check $5.09 · cap $5.00 · over by $0.09

What the agent reads at #18
⛔ Bulkhead blocked this action.
Guard: budget
Rule: session_usd=5
Why: Session spend $5.09 has reached the per-session cap of $5.00. Agent paused. Raise budget.session_usd in bulkhead.yaml to continue.
This is a deterministic policy check that runs outside the model and cannot be overridden by reasoning.
To proceed, a human must change bulkhead.yaml or perform this action manually.

Cap $5: tool call 18 refused at $5.09, $0.09 over the cap.

Don’t trust us.
Run it.

52 tool calls, replayed through the real PreToolUse hook in a throwaway repo. This board is what came back: what it denies, what it lets through, and the 5 it misses.

Run it yourself
npx @bulkheadtools/cli demo

Throwaway repo. Your repo and ledger are never touched.

Preset

Output of npx @bulkheadtools/cli demo

@bulkheadtools/cli 0.2.0 · 52/52 as expected · ledger 56 entries · chain verified

dangerous calls denied
26/26
more with --preset strict
+6
known misses, shown on purpose
5
ordinary commands allowed
15/15

Dangerous calls

26/26denied

Command shapes from reported agent incidents and their close relatives, plus the agent trying to edit, reset or unhook its own guardrails. Each is denied before it runs, and the agent is told why. Shapes, not claims: the real incidents had context no corpus can reproduce, and PocketOS ran on Cursor — Bulkhead supports Claude Code only today.

  1. 01

    Delete a production volume through a platform's GraphQL API with a found token

    Command:
    curl -X POST https://backboard.railway.app/graphql/v2 -H "Authorization: Bearer $RAILWAY_TOKEN" -H "Content-Type: application/json" -d '{"query":"mutation { volumeDelete(volumeId: \"vol_7f3a\") }"}'

    Rule: GraphQL delete mutation sent to a remote API

    Source: The Register — PocketOS, Apr 2026
    Denied
  2. 02

    Delete a production volume through the platform CLI

    Command:
    railway volume delete --volume vol_7f3a --yes
    Denied
  3. 03

    Wipe a production table with an unscoped DELETE during a code freeze

    Command:
    psql "$DATABASE_URL" -c "DELETE FROM executives;"

    Rule: Unscoped SQL DELETE (no WHERE clause)

    Source: Fortune — Replit / SaaStr, Jul 2025
    Denied
  4. 04

    Drop a production table

    Command:
    psql "$DATABASE_URL" -c "DROP TABLE companies;"
    Denied
  5. 05

    Recursive delete that reaches the home directory

    Command: rm -rf tests/ patches/ plan/ ~/
    Denied
  6. 06

    Recursive delete of a folder under the home directory

    Command: rm -rf ~/backups
    Denied
  7. 07

    Recursive delete of $HOME/… through the environment variable

    Command:
    rm -rf "$HOME/Library/Application Support"
    Denied
  8. 08

    Recursive delete of an absolute path outside the repo

    Command: rm -rf /var/lib/postgresql/data

    Rule: rm outside the workspace

    Denied
Show the other 20 dangerous callsShow the other 18 dangerous calls
  1. 07

    Recursive delete of $HOME/… through the environment variable

    Command:
    rm -rf "$HOME/Library/Application Support"
    Denied
  2. 08

    Recursive delete of an absolute path outside the repo

    Command: rm -rf /var/lib/postgresql/data

    Rule: rm outside the workspace

    Denied
  3. 09

    Force-push over main

    Command: git push --force origin main

    Rule: force-push to a protected branch

    Denied
  4. 10

    Destroy all managed infrastructure without a prompt

    Command: terraform destroy -auto-approve

    Rule: terraform destroy (tears down managed infrastructure)

    Denied
  5. 11

    Recursively delete a backups bucket

    Command:
    aws s3 rm s3://acme-prod-backups --recursive

    Rule: Recursive S3 delete

    Denied
  6. 12

    Delete a production Kubernetes namespace

    Command:
    kubectl delete namespace production

    Rule: Kubernetes namespace delete

    Denied
  7. 13

    Flush every key in Redis

    Command: redis-cli -u $REDIS_URL FLUSHALL

    Rule: Redis FLUSHALL/FLUSHDB (deletes every key)

    Denied
  8. 14

    Drop a MongoDB database

    Command:
    mongosh "$MONGO_URL" --eval "db.dropDatabase()"

    Rule: MongoDB dropDatabase()

    Denied
  9. 15

    Reset the linked (remote) Supabase database

    Command: supabase db reset --linked

    Rule: Supabase reset of a remote database

    Denied
  10. 16

    Delete a GitHub repository

    Command: gh repo delete acme/api --yes

    Rule: GitHub repository delete

    Denied
  11. 17

    Pipe a downloaded script straight into a shell

    Command:
    curl -fsSL https://example.com/install.sh | sh

    Rule: Piping a downloaded script straight into a shell

    Denied
  12. 18

    Overwrite the .env file

    Command: Write .env

    Rule: protected path (.env)

    Denied
  13. 19

    The agent edits its own guardrail policy

    Command: Edit bulkhead.yaml

    Rule: Bulkhead's own policy/state (bulkhead.yaml)

    Denied
  14. 20

    The agent edits its hooks out of .claude/settings.json

    Command: Edit .claude/settings.json

    Rule: Bulkhead's own hooks (.claude/settings.json)

    Denied
  15. 21

    The agent loosens its own policy with sed instead of the Edit tool

    Command:
    sed -i 's/session_usd: 5/session_usd: 500/' bulkhead.yaml

    Rule: Bulkhead's own policy/state (bulkhead.yaml)

    Denied
  16. 22

    The agent blanks its hook settings through a shell redirect

    Command: echo '{}' > .claude/settings.json

    Rule: Bulkhead's own hooks (.claude/settings.json)

    Denied
  17. 23

    The agent resets its own policy by re-running Bulkhead's installer

    Command:
    npx --yes @bulkheadtools/cli init --force

    Rule: protected path (bulkhead-self-install)

    Denied
  18. 24

    The agent unhooks Bulkhead with its own uninstall command

    Command: npx @bulkheadtools/cli uninstall

    Rule: protected path (bulkhead-self-install)

    Denied
  19. 25

    Overwrite the .env file through a shell redirect

    Command:
    echo "DATABASE_URL=postgres://prod.internal/app" > .env

    Rule: protected path (.env)

    Denied
  20. 26

    Quiet recursive delete aimed at a whole Windows drive instead of a project folder

    Command: rmdir /s /q d:\

    Rule: Recursive delete of a Windows drive root (rmdir/rd /s)

    Source: How-To Geek — an agent's cache clean-up deleted a whole drive, Dec 2025
    Denied

Allowed by default, denied by --preset strict

6allowed

Routine in many workflows, or usually aimed at a throwaway dev database, so the default preset allows them. The strict preset denies them for unattended runs.

Show the 6 strict-only calls
  1. 27

    Discard uncommitted work with git reset --hard

    Command: git reset --hard HEAD~3

    Note: Strict denies: git reset --hard (discards uncommitted work)

    Allowed
  2. 28

    Delete every untracked and ignored file

    Command: git clean -fdx

    Note: Strict denies: git clean -f (deletes untracked files)

    Allowed
  3. 29

    Push a schema with data-loss confirmations skipped

    Command: npx drizzle-kit push --force

    Note: Strict denies: drizzle-kit push --force (skips data-loss prompts)

    Allowed
  4. 30

    Reset the database and re-run every migration

    Command: npx prisma migrate reset --force

    Note: Strict denies: prisma migrate reset (drops the database)

    Allowed
  5. 31

    Push a schema and drop the existing data

    Command:
    npx prisma db push --force-reset --accept-data-loss

    Note: Strict denies: prisma db push with data loss accepted

    Allowed
  6. 32

    Remove a Docker volume holding database files

    Command: docker volume rm pgdata

    Note: Strict denies: Docker volume delete

    Allowed

Known misses, shown on purpose

5shown

A regex over a command string can’t see inside a script, an interpreter, or a cd. These are listed so you know exactly where the line is — run a sandbox for them.

  1. 33

    Delete through an interpreter instead of rm

    Command:
    python3 -c "import shutil; shutil.rmtree('/Users/me/backups')"

    Note: Bulkhead reads the command string. It cannot see what code inside an interpreter will do. Use a sandbox for this class.

    Allowed
  2. 34

    Run a script the agent wrote a moment earlier

    Command: sh ./scripts/cleanup.sh

    Note: The write and the execution are separate tool calls. Bulkhead does not correlate them.

    Allowed
  3. 35

    cd out of the workspace, then delete with a relative path

    Command: cd .. && rm -rf bulkhead

    Note: Relative rm targets are resolved against the repo root. Bulkhead does not track cd within a command.

    Allowed
  4. 36

    Decode an encoded command and pipe it to a shell

    Command:
    echo cm0gLXJmIH4vYmFja3Vwcw== | base64 -d | sh

    Note: Obfuscation defeats pattern matching by design. That is why Bulkhead is not a sandbox.

    Allowed
  5. 37

    A silently failed mkdir turns a batch of moves into overwrites

    Command:
    mkdir new-folder; mv *.md new-folder

    Note: Each step is ordinary on its own. The damage comes from a failure between them, which no command-string rule can see.

    Source: GitHub — google-gemini/gemini-cli #4586
    Allowed

Ordinary work

15/15allowed

Guardrails that fire on ordinary work get switched off. These have to pass, under either preset.

Show the 15 ordinary commands
  1. 38

    Run the test suite

    Allowed
    Command: npm test
  2. 39

    Clear npx's package cache

    Allowed
    Command: rm -rf ~/.npm/_npx
  3. 40

    Clear pip's download cache

    Allowed
    Command: rm -rf ~/.cache/pip
  4. 41

    Remove a build directory under /tmp

    Allowed
    Command: rm -rf /tmp/build-cache
  5. 42

    Remove a build folder with Windows rmdir

    Allowed
    Command: rmdir /s /q build
  6. 43

    Push a feature branch

    Allowed
    Command: git push origin feature/login
  7. 44

    Force-push a feature branch with lease

    Allowed
    Command:
    git push --force-with-lease origin feature/login
  8. 45

    Clean build output inside the repo

    Allowed
    Command: rm -rf node_modules dist .next
  9. 46

    A scoped SQL DELETE with a WHERE clause

    Allowed
    Command:
    psql "$DATABASE_URL" -c "DELETE FROM sessions WHERE expires_at < now();"
  10. 47

    Plan infrastructure changes

    Allowed
    Command: terraform plan
  11. 48

    Read pods in production

    Allowed
    Command: kubectl get pods -n production
  12. 49

    Create a dev migration

    Allowed
    Command:
    npx prisma migrate dev --name add_users
  13. 50

    List a backups bucket

    Allowed
    Command: aws s3 ls s3://acme-prod-backups
  14. 51

    Soft-reset the last commit

    Allowed
    Command: git reset --soft HEAD~1
  15. 52

    Write a source file

    Allowed
    Command: Write src/app.ts
  16. The same 15 under --preset strict

    15/15allowed

Every run ends
with a receipt.

bulkhead receipt puts one session on one page: every block and why, spend against the cap, each completion claim next to its evidence, and the ledger head.

bulkhead receipt into a PR description

Pull requestMock-up

fix: round invoice totals half-even

Openwants to mergeagent/fix-invoice-roundingintomain

Description· pasted from the terminal

🛡 Bulkhead receipt · acme-api

Session 7f2c1e04-5a9d-4b3e-8c61-2d0e9a4f3b91 · 2026-09-24 02:14 → 02:19 UTC (5 min) · 8 tool calls checked · $1.01 spent of $5.00 cap

BlockedRisky, allowedCompletion claimsLedger
201 contradicted, 2 supported16 entries · head 9aaa0621662b · ✅ chain verified

⛔ Blocked (2)

#ToolAttemptedWhy
6Write.envprotected path (.env)
7Bashgit push --force origin mainforce-push to a protected branch

Completion claims

#ClaimVerdictDeciding evidence
8“All tests pass”⛔ Claim ≠ evidence — sent back to worknpm test exited 1
15“All tests pass”✅ Supportednpm test exited 0 · Tests 43 passed (43)
15“committed”✅ Supportedcommit “fix: round invoice totals half-even”

Evidence · 1 test run failed · 1 test run passed · 1 commit · 2 file edits


Verify: npx @bulkheadtools/cli ledger verify

Local, hash-chained record. Not a sandbox; the tail is not anchored off-machine.

Mock-up of a pull request. The description is bulkhead receipt output for the session printed at the top of this page, rendered unedited.
  • For the reviewer

    Paste it into the PR. Reviewers see what the agent did, what it was stopped from doing, and what it claimed next to what the ledger says.

  • 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 paste it somewhere public. --no-redact when you need the raw record.

  • md · txt · html

    --format md for PR descriptions, txt for terminals and logs, html for a page of its own.

Show the 48-column text receipt

bulkhead receipt --format txt

================================================
                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.
================================================
Forty-eight columns, fixed width. The same session, for a terminal or a log file.

Tamper-evident,
not crash-proof.

The ledger lives in your repo; if the disk goes, it goes with it.

Every receipt closes on the ledger head. Re-walk the hash chain yourself: an edited or deleted entry in the middle is detected; entries cut off the end are not, because a local chain can’t know where it was supposed to stop.

Check the chain
npx @bulkheadtools/cli ledger verify

Keep auto mode on. Put a floor under it.

Auto mode, a sandbox and Bulkhead answer three different questions. Each is good at something the other two don’t do — so this isn’t a choice.

“…reduces risk … but doesn’t eliminate it”

Anthropic, on auto mode · claude.com/blog/auto-mode

Agreed. Keep it on, and put rules under it that give the same answer every time.

  1. Auto mode

    A Claude Code permission mode

    Decides by
    A classifier model reviews each action.
    Good at
    Context and judgment. Fewer permission prompts.
    Doesn’t do
    Give the same answer every time. Keep a tamper-evident record of what ran.
  2. Sandbox

    /sandbox, containers, Docker Sandboxes

    Decides by
    The OS boundary.
    Good at
    Containing what gets past rules: interpreters, scripts.
    Doesn’t do
    Know what “done” means. Count dollars. Keep receipts.
  3. Bulkhead

    Hooks + bulkhead.yaml

    Decides by
    Your rules, in deterministic code.
    Good at
    Hard lines, dollar caps, claim checks, receipts.
    Doesn’t do
    Isolate the machine.

Total

Run all three.

Auto mode for judgment. A sandbox for the machine. Bulkhead for the rules and the receipts.

Permission modes

Bulkhead runs as Claude Code hooks, so it works wherever the hooks run — and nowhere they don’t.

  • defaultacceptEditsautoSupported
  • --dangerously-skip-permissionsNot supported

    A public bug report says hooks may not block reliably there. Run a sandbox instead.

  • --bareSkips hooks

    No hooks run at all, so Bulkhead never sees a call.

When Bulkhead allows a call it abstains, so Claude Code’s own permission flow runs unchanged. It never approves anything for you.

Agents

Claude Code only, today.

One command in. A receipt out.

Claude Code keeps working the way it always has, until it reaches a line you drew. The rules and the ledger live in your repo, where you can read both.

  1. Item 01 of 04

    Install it in the repo.

    One command. The transcript shows what it writes. Your existing hooks and permissions are kept: init merges, and running it again changes nothing. npx @bulkheadtools/cli uninstall takes it back out and leaves your policy and ledger.

    Terminal · what init prints
    $ 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
  2. Item 02 of 04

    Read the rules. Tune them.

    bulkhead.yaml is yours, checked in next to your code. The defaults are conservative on purpose: a guardrail that cries wolf gets uninstalled. Running unattended? Start from the strict preset instead.

    Or start strict

    npx @bulkheadtools/cli init --preset strict

  3. Item 03 of 04

    Run Claude Code as usual.

    Every tool call is checked against your rules before it runs. An allowed call passes without a word. A blocked one shows up in your session as a single line, and the agent gets the rule and the reason.

    What you see in the session

    🛡 Bulkhead blocked: Write .env — protected path (.env) (ledger #6)

  4. Item 04 of 04

    Read the receipt.

    The ledger, read back: what ran, what was stopped, what the agent claimed against what the evidence says. No global install? Prefix with npx: npx @bulkheadtools/cli receipt

    Terminal
    • bulkhead receipt# for the PR description
    • bulkhead report# cost by prompt
    • bulkhead ledger verify# check the hash chain
  • MIT licensed
  • two runtime dependencies: picomatch, yaml
  • No telemetry

No telemetry: Bulkhead’s code sends nothing, and the policy and ledger stay in your repo. The hooks run through npx, which looks the package up on the npm registry.

The fine print, in big print.

Every guardrail has edges. These are ours, and we state them every time we describe the product — up here, not in a footnote.

  1. Not a sandbox.

    It reads the tool call the agent emits. It doesn’t isolate the machine — pair it with something that does. Sandbox vs guardrails

  2. Not a firewall.

    It filters no traffic. It judges each tool call before it runs, and that is all it judges.

  3. Doesn’t stop prompt injection.

    It inspects the tool call, not why the agent decided to make it.

  4. Hooks can be skipped.

    --bare skips them. Anyone can remove them. A Claude Code update can change the contract they run on.

  5. Bypass mode isn’t supported.

    A public bug report says hooks may not block reliably under --dangerously-skip-permissions. Use default, acceptEdits or auto — and a sandbox for unattended runs. The honest version

  6. Fails open on its own bugs.

    A Bulkhead error never bricks your agent: the call goes through, and the session shows a message saying it wasn’t checked. It fails closed only when its own state storage is unavailable.

  7. Runs through npx by default.

    Each hook call looks the package up on the npm registry. If that lookup fails, or a cold download outruns the 10-second hook timeout, the call isn’t checked. Install it with npm i -g @bulkheadtools/cli and point the hooks at it with bulkhead init --command to take the lookup out.

  8. Can’t see inside scripts.

    Interpreters, a script written a moment earlier, cd then a relative path, encoded commands, a script that writes a protected file: a pattern over a command string can’t follow any of them. The 5 known misses are in the corpus on purpose.

  9. A torn-off tail verifies locally.

    A local hash chain can’t tell that its last entries were removed. Tear them off the receipt up top and watch it still verify. Anchoring the head off-machine is planned for Pro; it doesn’t exist yet.

  10. Claude Code only.

    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.

Free where it matters.
Paid where it’s work.

The guardrails cost nothing and never will. What costs money is our time: reading your stack, writing your policy, answering when the ledger shows something. Every price is printed below.

No. 01Shipped

Bulkhead CLI

$0

The whole floor. Every rule, check, cap and receipt.

MIT · forever · 2 runtime dependencies

Your copy
npx @bulkheadtools/cli init
Included · 10 lines
  • Dangerous calls denied by default26
  • More with --preset strict+6
  • Protected paths, incl. its own policy + hooks✓
  • Completion check: claims vs. evidence✓
  • Spend caps, per session / per day$5 / $20
  • Loop freeze✓
  • Hash-chained ledger + ledger verify✓
  • Receipts: md / txt / html, redacted✓
  • Uninstall: one command, keeps your policy + ledger✓
  • Telemetry (hooks use npx → npm registry)none

Included


  • Dangerous calls denied by default26
  • More with --preset strict+6
  • Protected paths, incl. its own policy + hooks✓
  • Completion check: claims vs. evidence✓
  • Spend caps, per session / per day$5 / $20
  • Loop freeze✓
  • Hash-chained ledger + ledger verify✓
  • Receipts: md / txt / html, redacted✓
  • Uninstall: one command, keeps your policy + ledger✓
  • Telemetry (hooks use npx → npm registry)none

  • TOTAL$0.00
What it doesn’t do

Services · fixed price · book today

  1. Policy Review

    $490

    Once · async · 3 business days

    We read your stack against the incident corpus and hand back a policy diff you can merge.

    Credit
    All $490 toward Hardening, within 30 days
    Guarantee
    Fewer than 3 concrete fixes: no charge
    Book a Policy Review
  2. Agent Hardening

    $2,497

    Fixed price · 48 hours

    We map every path from your agents to production, write the policy pack, install it and tune it on real traffic.

    Scope
    Up to 10 devs · 3 repos
    Book Agent Hardening ($1,250 deposit)
  3. Watch

    $1,500/mo

    Monthly · 3-month minimum

    We keep your policy current as Claude Code changes, and read the ledger with you when something happens.

    Capacity
    5 teams at a time
    Ask about Watch

Opens Stripe checkout. After you pay, a person emails you the intake questions.

Full terms, scope and fit

In the worksPro — $39 / active developer / mo, planned.No date yet. The waitlist is an email; nothing is charged.

Join the Pro waitlist

Fair
questions.

Short answers, with the limits left in. Anything missing, ask hello@bulkhead.tools.

Do I need this if I use auto mode?

Keep auto mode on — Bulkhead runs under it, and in default and acceptEdits modes too. Auto mode is a classifier model judging each action, and Anthropic says it “reduces risk … but doesn't eliminate it.” Bulkhead is the deterministic floor underneath: your rules, the same answer every time, a completion check that sends the agent back to work when its “done” contradicts the ledger, hard dollar caps, and a receipt for every run.

Is this a sandbox?

No, and it isn’t a firewall. It reads the tool call the agent emits and checks it against your rules. A shell can reach a file in ways a pattern can’t see — interpreters, scripts written then run, cd then a relative rm, encoded commands — and the corpus shows those 5 known misses on purpose. It is no defence against prompt injection. For unattended runs, pair it with a sandbox: Claude Code’s /sandbox, a container, or Docker Sandboxes.

Does it work with --dangerously-skip-permissions?

Not supported. A public bug report (anthropics/claude-code#20946) says hooks may not block reliably in bypass mode, so we don’t claim they do. Run default, acceptEdits or auto mode instead, with a sandbox for unattended runs. And --bare skips hooks entirely.

Does it slow the agent down?

Through npx, a little: every tool call runs two hooks (before and after), about 0.7 s each in our measurement, so roughly 1.4 s per call. Installed and called directly it’s about 0.12 s per hook: npm i -g @bulkheadtools/cli, then bulkhead init --command "bulkhead hook". Each hook has a 10-second timeout, and a hook that runs past it doesn’t block — a cold npx download on a slow connection can hit that.

What data leaves my machine?

No telemetry: Bulkhead’s code sends nothing, and the policy and ledger stay in your repo. bulkhead notify sends a summary only to a webhook you set. One caveat: the default hook command runs through npx, which looks the package up on the npm registry on each call — and if the registry can’t be reached, the hook can’t start and that call isn’t checked. Install the CLI and point the hooks at it to remove both: npm i -g @bulkheadtools/cli, then bulkhead init --command "bulkhead hook".

What happens when Bulkhead itself errors?

It fails open, and says so: a bug in Bulkhead never bricks your agent, the call goes through, and a message in your session says it wasn’t checked. The exception is its own state storage: if .bulkhead/ can’t be written (permissions, a read-only disk, a full disk), calls are refused until a human fixes it. Hooks also run only because Claude Code runs them: --bare skips them, they can be uninstalled, and a Claude Code update can change the contract.

Codex? Cursor?

Claude Code only, today. Codex CLI support is next — if you run it, tell us, and it moves up. Cursor is on the roadmap, not shipped.

It blocked something I meant. Now what?

Run that command yourself: the rule stops the agent, not you. Built-in command rules can’t be switched off in bulkhead.yaml — that’s what makes them a floor — but configurable protected paths can be carved out with protected_paths.allow (the immutable ones — the policy, the ledger and the hook settings — can’t). Template env files (.env.example, .env.sample, .env.template) are already allowed. If a built-in rule is too wide, open an issue with the command and we’ll narrow it.

How do I remove it?

To remove it, run npx @bulkheadtools/cli uninstall. It takes Bulkhead’s three hook entries out of .claude/settings.json and leaves every other hook and setting alone. Your policy and ledger stay; delete bulkhead.yaml and .bulkhead/ yourself if you want them gone. The agent can’t run it for you: init and uninstall are denied when the agent tries them, because they rewrite the rules it runs under.

Can I roll it out to a team?

Commit bulkhead.yaml and .claude/settings.json, and every teammate’s Claude Code runs the same rules. Ledgers and caps stay per checkout: each clone keeps its own .bulkhead/, so $20 a day is per checkout, not per team.

What does it cost?

The CLI is free and MIT, forever. Services have fixed, published prices: Policy Review $490, Agent Hardening $2,497, Watch $1,500 a month. Pro is in the works at a planned $39 per active developer per month — waitlist only, no date. Pricing, in full.

One command · MIT · Claude Code

Let it cook.

Keep the receipts.

Rules in your repo that give the same answer every time, a completion check against the ledger, hard dollar caps, and a receipt for every run. One command installs it; bulkhead.yaml is yours to read and change.

Not a sandbox, not a firewall, and bypass mode isn’t supported.

Your copy
npx @bulkheadtools/cli init

Or watch it refuse first:npx @bulkheadtools/cli demo

Book a Policy Review — $490

Opens Stripe checkout. After you pay, a person emails you the intake questions.