The Seatbelt

The seatbelt is a small catastrophe denylist that is on by default on every machine after switchboard setup. With zero grants and zero configuration, routed MCP calls that match a catastrophe pattern are denied, and the same patterns guard agent shell commands through a harness hook. Everything else behaves exactly as before: the seatbelt only speaks up on catastrophe-class calls.

Curation rule: a pattern ships in the default list only if the action it matches is IRREVERSIBLE and EXTERNALLY VISIBLE. The goal is that everyday flows pass untouched: Vercel preview deploys, Convex dev and preview deploys, Stripe test-mode calls, dev-database teardown, and force-pushes to feature branches. A false positive on a common flow is treated as a release blocker, not a tuning item.

Two surfaces, evaluated differently

The seatbelt is enforced on two surfaces, and they do NOT use the same matching. Conflating them is what makes a naive denylist trip on a commit message or a grep.

Because the surfaces differ, coverage is not identical. The prod-deploy-tool name match applies ONLY to the MCP surface (on shell it would match filenames like deploy-prod.sh). DNS/domain/route53 and the Stripe live-mode CLI flag are shell-surface rules; the MCP surface covers deploy tool names and live secret keys in arguments. Both surfaces share one denylist source of truth (the global config), but each applies the rules that make sense for it.

Honesty note: the hook is harness-level, not OS enforcement. It guards commands issued through Claude Code's Bash tool. It does not sandbox the machine, and a process outside the harness is not covered. It is defense-in-depth, not a boundary.

Because the hook inspects the invoked command by static parsing, it does not see into everything a shell line can run. It evaluates sh -c/bash -c payload strings, but it does NOT resolve command substitution (vercel --prod hidden inside $(...) or backticks) or opaque wrapper scripts (a ./ship.sh that internally deploys): the invoked command there is the substitution or the script, not the deploy tool. There is also one known bare-form gap: git push --force origin with no explicit refspec is not tripped, because the hook has no current-branch context to know whether the pushed branch is the default one. These are properties of a static, harness-level check; the MCP surface (which sees the actual tool call) is not affected by them. If the switchboard binary is missing at hook time, the hook fails open.

The v1 list

Pattern Surface Trips on Passes (examples)
prod-deploy-flag shell a deploy command with --prod/--production/--target production pnpm build, npm install --production
vercel-prod shell + MCP vercel --prod, vercel deploy --prod/--target production, vercel promote, vercel alias set; MCP: a vercel deploy tool with a production target vercel build --prod, vercel deploy (preview), vercel
convex-prod-deploy shell + MCP convex deploy with no preview flag; MCP: a convex deploy tool with no preview arg convex deploy --preview-create x, npx convex dev
prod-deploy-tool MCP only tool names like ..._deploy_prod any shell command (filenames like deploy-prod.sh)
stripe-live-secret-key shell + MCP an sk_live_/rk_live_ key in the command or arguments sk_test_..., pk_test_..., pk_live_...
stripe-live-mode-flag shell stripe ... --live stripe products list
vercel-dns-mutation shell vercel dns add/rm/import vercel dns ls/inspect
vercel-domain-mutation shell vercel domains add/rm/buy/move/transfer-in vercel domains ls/inspect
route53-record-change shell aws ... change-resource-record-sets aws route53 list-resource-record-sets
force-push-default-branch shell git push with a force flag (--force, -f, --force-with-lease, --mirror, or a + refspec) targeting main/master (including refs/heads/main, HEAD:main, +refs/heads/main, and git -C <path> push) force-push to a feature branch; git push origin main without force; main:not-main

Destructive SQL is deliberately NOT in v1: QA teardown legitimately drops dev tables, and SQL patterns without target awareness cannot tell dev from prod.

What a trip looks like

A tripped call is denied immediately with the pattern name, the reason, and the exact recovery commands, and an approval request is queued:

switchboard seatbelt: vercel-prod; Vercel production deploy (previews deploy
without --prod); approval request approval-3 is pending; approve with:
switchboard approve approval-3 --reason "<why this is safe>"; then retry the
same call; or disable the seatbelt with "seatbelt: off" in
~/.config/switchboard/config.yaml

Agent loops never hang: the denial returns instantly, and a human can approve from another terminal. After approval, retrying the same call succeeds. The approval covers that pattern and tool: under an active pass it lasts until the pass expires; with no pass bound it lasts 15 minutes. Trips, approvals, and the successful retry all land in the local audit log (switchboard logs, switchboard audit verify). Hook-layer denials land in the same log via switchboard audit append.

Opting out and tuning

The seatbelt reads only the machine-level global config (~/.config/switchboard/config.yaml, respecting XDG_CONFIG_HOME). A repo's .switchboard.yaml cannot turn it off or trim it, so a writable repo file is not a bypass.

Turn it off entirely:

seatbelt: off

Or for one session: switchboard mcp --no-seatbelt / switchboard serve --no-seatbelt.

Extend or trim the list under the machine policy stanza:

policies:
  default:
    seatbelt:
      add:
        - name: my-launcher
          pattern: "\\bmy-cli\\s+launch-prod\\b"
          reason: "launches production"
      remove:
        - route53-record-change

Remove the Claude Code hook with switchboard hooks uninstall claude; it restores the settings file to its pre-install content.