rules.yaml
Rama main · réplica de lectura del repositorio original.
# Squidbrake rules - hot-reloaded on save, no restart needed.
#
# Policy: agents may LOOK at anything (read, list, search). Coding agents do everyday work on their own (edits,
# tests, builds, commits), but what can't be undone waits for a person and what's catastrophic is blocked. Any other
# change (sends, refunds, pays, updates, deletes, pushes) waits for a person, and so does an agent changing its own
# settings, hooks or MCP servers, Squidbrake's rules, or a secrets file.
#
# Rules are checked top to bottom; the FIRST match wins. If none match, `default` applies.
# match fields (all optional, all must match):
# kind, name, source, client, session_id glob pattern or list of globs (case-insensitive), e.g. "shell*"
# input_regex regex searched in the JSON of the call's input
# input number checks on input fields: {field: {op: number}}, ops gt gte lt lte eq ne,
# e.g. input: { amount: { gt: 100 } }; "a.b" reaches nested fields; a missing
# or non-numeric field doesn't match
# skip_reads: true a shell command that only looks (cat, grep, ls, jq ...) doesn't match;
# a secret read, a write or a redirect into a file still does
# action: allow | deny | review
#
# Example - let small refunds run on their own (put it above approve-refunds to use it):
# - id: allow-small-refunds
# action: allow
# reason: Small refund - runs on its own
# match:
# name: ["*refund*"]
# input: { amount: { gte: 0, lte: 50 } }
# For allow rules use a range: `lte: 50` alone would also let through a negative amount like -5000.
# Amounts are in your API's units - Stripe sends cents, so 50 dollars is 5000 there.
#
# review = hold the call until a person approves or rejects it (dashboard, phone, Slack).
# timeout_seconds how long to wait for a person (default APPROVAL_TIMEOUT, 300)
# on_timeout deny (default) | allow - what happens if nobody decides in time
# approvers who may decide: key names or roles, e.g. ["role:finance", "priya"]; default = any approver
# second_person true: the person whose agent asked can't approve it (maker-checker), even with
# Settings > second-person approval off
# A key can never approve its own request, and an agent can't reach the approval path at all (selfguard.py).
default: review
default_reason: This changes something, so a person has to approve it
# enforce: block and hold for real. observe (or shadow): block and hold nothing, only record what would have happened
# (Reports and `squidbrake report --week` show "would block" / "would hold"), the easy way to try Squidbrake on real
# work. Stops, catastrophic commands (rm -rf /, wiping a drive) and an agent reaching for Squidbrake's own approval
# path are enforced in observe mode too. A rule or sequence can set its own `mode: enforce | observe`, e.g. enforce
# the catastrophic ones first and observe the rest.
mode: enforce
shadow_agents: [] # shadow mode for just these agents (source or key names, globs), e.g. ["new-bot*"]
# While Squidbrake can't be reached:
# block_risky what these rules let run on its own (reads, everyday coding work) still runs; anything that would
# wait for a person or be blocked, and commands that can't be undone or can't be read, are blocked
# block everything is blocked (the safest; agents stop working until Squidbrake is back)
# allow everything runs
# warn everything runs, with a loud warning on each action; when Squidbrake is back, what ran is put on
# the record (protection.was_off). Opt in for a laptop that reboots; Squidbrake's keys stay out of reach
# Each rule can say its own with `when_unreachable: allow | block`. Agents keep a copy of these answers, so they
# follow them while it's down. What runs while it's down isn't recorded: the tradeoff is in THREAT_MODEL.md.
when_unreachable: block_risky
# Squidbrake checks every minute that its hooks in each agent, this file and its installed program haven't changed.
# alert a change is recorded and sent wherever approvals go
# fail_closed also: until a person runs `squidbrake integrity accept`, actions that would wait for a person or
# can't be undone are blocked (a change to this file alone only alerts)
integrity: alert
# Undo copies: before an action that waits for a person, or a command that can't be undone, keep a copy of what it
# changes (files and git changes on the agent's computer, the record behind an API call through /proxy, or what a
# client.py snapshot function returns). required: if no copy can be made, it doesn't run. best_effort: try, run anyway.
# off: don't. A rule can set its own `snapshot: required | best_effort | off`; `snapshot: required` on a rule also
# blocks paths that can't make a copy at all (an MCP tool).
snapshots: required
# Spend limits: pause an agent once it has used this many model tokens (or dollars, `usd:`) in a day or month.
# per: agent gives each matching agent its own limit; per: together shares one between them. Claude Code reports its
# tokens by itself; other agents send them to POST /v1/usage (client.py: gw.usage(...)).
# budgets:
# - id: daily-tokens
# match: { source: "*" }
# per: agent
# period: day
# tokens: 5000000
# Proof of work: when an agent says an action is done, look in the real system. Credentials only come from the
# gateway's environment. The answer (confirmed / not actually done) is recorded and a person is told on a mismatch.
# proof_checks:
# - id: refund-really-made
# match: { name: "*refund*" }
# stripe: { object: refunds, id: "{output.id}" } # STRIPE_SECRET_KEY
# expect: { status: succeeded }
# - id: pr-really-open
# match: { name: "*create_pull_request*" }
# github: { path: "repos/{input.owner}/{input.repo}/pulls/{output.number}" } # GITHUB_TOKEN
# expect: { state: open }
# - id: order-really-updated
# match: { name: "orders.update_status" }
# sql: { url_env: ORDERS_DB_URL, query: "select status from orders where id = :id", params: { id: "{input.order_id}" } }
# expect: { status: "{input.status}" }
# History checks look at what happened BEFORE an action, not just the action itself.
# Each one: block (stop it), review (a person decides), warn (flag it for the approver), off.
history_checks:
company_domains: [acme.com] # your real email domains; look-alikes (acrne.com, acme-corp.com) are suspicious
lookback_hours: 24
repeat_of_rejected: review # an action a person already rejected (same tool + same target) goes back to a
# person, flagged with their earlier no and note (block: refuse it outright)
impersonation: block # moving money right after reading a message from a look-alike of your domain
payment_request_in_message: review # moving money right after reading a message that asks for a payment
duplicate_change: warn # the same change on the same thing again (e.g. a 2nd refund on one charge)
# Command checks read what a shell command actually DOES before it runs: they split `a && b`, look inside
# `bash -c "..."`, `sudo`, `xargs` and `$(...)`, and understand rm / git / terraform / kubectl / cloud CLIs / SQL.
# Each one: block, review, warn, off. They apply even when a rule below allows the tool.
command_checks:
tools: ["Bash", "PowerShell", "*shell*", "*run_command*", "*execute_command*", "*terminal*", "*exec_command*"]
catastrophic: block # rm -rf /, rm -rf ~, wiping a drive (rmdir /s d:\), mkfs, dd onto a disk, chmod -R 777 /
irreversible: review # every delete (rm, git rm, docker rm, DELETE FROM), git push --force, git reset --hard, terraform destroy, kubectl delete, cloud deletes, DROP TABLE
rebuildable: warn # deletes of build output, caches or temp files only (rm -rf node_modules dist, rm app.log,
# rm -rf /tmp/x): flagged for you to see; review holds them like other deletes
hidden: review # code that can't be read first: eval, curl ... | sh, base64 -d | bash, powershell -EncodedCommand,
# programs written inline (python -c, node -e, perl -e), a program named only when it runs
rename: warn # renames or moves: mv, git mv, Rename-Item, git branch -m, aws s3 mv, ALTER TABLE ... RENAME:
# flagged for you to see; review holds them
secret: review # shows a secret to the agent (and its transcript): cat key.pem, cat ~/.kube/config,
# gh auth token, a keychain password, kubectl get secret -o yaml
written_then_run: review # a script the agent created in this conversation, then runs (python x.py): read it first
edited_then_run: warn # a script the agent changed in this conversation, then runs: flagged for you to see;
# review holds it (safer: an edit can hide anything, but fixing a script and running it
# is everyday work)
unknown: warn # a program that isn't a common developer tool: recorded so you can see what would wait;
# review holds them too
read_only: allow # commands that only look (ls, cat, grep, git status/log/diff) run without asking,
# unless a rule below says otherwise. Set to off to approve those too.
# Taint checks catch prompt injection without a model: content someone else wrote (a web page, an email, an issue)
# says "send X to <attacker>", and the agent does. When an action sends something to a destination (email address,
# URL, bank account, repo) that appears in that outside content but NOT in what the user asked (Claude Code prompts
# are recorded) or in your own tools' results, it came from the outside content.
taint_checks:
untrusted_destination: review # sends to an address/URL/account found only in outside content
after_untrusted: warn # sends anything out after outside content was read in this conversation
after_untrusted_setup_edit: warn # changes the agent's own setup (held by approve-agent-setup-changes) after outside
# content was read in the same conversation: names which content came first (#74);
# warn only, it never changes the hold into an allow
lookback_hours: 24
scope: identity # outside content read in one conversation still counts in the next (or: session)
# untrusted: [...] tools whose results are outside content (defaults: WebFetch, *fetch*, *inbox*, *issue*, *ticket* ...)
# sinks: [...] tools that send things out (defaults: *send*, *reply*, *publish*, *transfer*, *webhook*, *upload* ...)
# Shell commands count as sinks when they upload (curl -d, scp, git push, ...).
# Data checks look at text an action puts where other people read it: a pull request, an issue or comment, a gist, a
# chat post, a page (and `gh pr|issue|gist ... create|comment|edit`). Looking things up doesn't count. Email has its
# own rule above. Each one: block, review, warn, off.
data_checks:
customer_names: review # names a customer on your list (dashboard → Settings → Customers), e.g. in a PR description
personal_data: review # a card number, a social security number, an IBAN, or a list of 3+ emails or phone numbers
# where: [...] tools whose text others read (defaults: *pull_request*, *issue*, *comment*, *gist*, ...)
# Sequence rules judge an action by what came BEFORE it, and say which earlier step caused it.
# match this action (same fields as rules: name, source, input_regex, input, ...)
# after an earlier step that went ahead, within within_hours (default 24);
# scope: session (the same conversation, the default) | agent (any conversation of this agent) |
# identity (any conversation of any agent working for the same person: on your own computer, all of
# them; on a team gateway, the agents whose key "works for" that person) | all (any agent);
# the step is remembered until within_hours runs out, across restarts (`squidbrake capabilities` lists them);
# same_target: true = only if it was on the same thing (same charge, file, account ...)
# count more than `more_than` earlier matching actions within within_hours; scope: session | agent | identity | all;
# sum: <field> = add up that input field (this action plus earlier ones) instead of counting actions
# (missing or non-numeric values count as 0); same_target: true = only earlier actions on the same thing
# action deny | review | warn | notify (runs, and tells the team, like a rule's notify)
sequences:
- id: destroy-after-recovery-removed
action: review
reason: Destroying data right after its backups or deletion protection were turned off
match:
input_regex: 'delete[-_ ]?db[-_ ]?(instance|cluster|snapshot)|delete[-_ ]?(bucket|volume|database|snapshot)|volumeDelete|s3\s+rb\b|terraform\s+destroy|drop\s+(table|database)'
after:
match:
input_regex: 'backup[-_ ]?retention[-_ ]?period\W{0,4}0\b|disable[-_ ]?backup|delete[-_ ]?(db[-_ ]?)?snapshot|deletion[-_ ]?protection\W{0,4}(false|0)\b|no-deletion-protection|point[-_ ]?in[-_ ]?time[-_ ]?recovery\W{0,4}(false|disabled)'
within_hours: 24
scope: all # backups switched off by any agent, in any conversation, then a delete
- id: runaway-refunds
action: deny
reason: Too many refunds in a short time; the agent may be stuck in a loop
match: { name: ["*refund*"] }
count: { more_than: 10, within_hours: 1, scope: identity } # a loop split across two agents still counts
- id: mass-deletes
action: review
reason: Many deletions in a short time
match: { input_regex: '\b(delete|destroy|drop|rm\s+-|rmdir|remove-item)\b' }
count: { more_than: 20, within_hours: 0.25, scope: session }
- id: unusual-burst
action: notify
reason: Far more actions than usual from one agent; it may be stuck in a loop or doing something nobody planned
match: {}
count: { more_than: 300, within_hours: 0.25, scope: agent }
rules:
# ---- never allowed
- id: block-destructive-shell
action: deny
reason: Destructive shell command
match:
name: ["shell*", "bash*", "powershell*", "exec*", "run_command*"]
input_regex: 'rm\s+-(rf|fr)\s+(/\*?|~/?\*?|\*)(?=[\s"'';&|)]|$)|mkfs|dd\s+if=|:\(\)\s*\{|shutdown|reboot|Remove-Item\s.*-Recurse.*\s(C:\\\\|~|\$HOME)'
- id: block-sql-drop
action: deny
reason: Dropping or truncating tables is not allowed
match:
name: ["sql*", "db*", "*query*"]
input_regex: '\b(drop\s+(table|database|schema)|truncate\s+table)\b'
- id: block-secret-files
action: deny
reason: Reading secret files is not allowed
match:
name: ["read*", "file*", "cat*"]
input_regex: '\.env\b(?![\w.-]*(example|sample|template))|id_rsa|\.pem\b|\.p12\b|\.pfx\b|credentials|\.kube[\\/]+config\b|\.docker[\\/]+config\.json'
# ---- an agent changing its own setup: its settings, hooks and MCP servers, Squidbrake's own files,
# secrets, and files that run code at login or on every commit. Squidbrake would be easy to switch off without this.
- id: approve-agent-setup-changes
action: review
reason: Changes an AI agent's own settings, hooks or MCP servers, Squidbrake itself, a secrets file, or code that runs at login
match:
name: ["Edit", "Write", "MultiEdit", "NotebookEdit", "bash*", "powershell*", "shell*", "run_command*"]
skip_reads: true # `cat ~/.claude/settings.json` only looks; `cat .env` (a secret) and `sed -i` still wait
# an agent's own notes (Antigravity's brain/, Claude Code's plans/, todos/ and memory/) are markdown it writes on
# every task, not its setup: those .md files aren't held. Settings, hooks, CLAUDE.md, commands/ and agents/ still are.
input_regex: '(?i)\.(claude|cursor|codex|gemini|copilot|kiro|codeium|windsurf|squidbrake)[\\/](?![\\/]*(?:antigravity[\\/]+brain|plans|todos|projects[\\/]+[^"\\/]+[\\/]+memory)[\\/]+(?:(?!\.\.)[^"])*\.md")|mcp(_config)?\.json|claude_desktop_config\.json|\.vscode[\\/]+(mcp|tasks|settings)\.json|Code( - Insiders)?[\\/]+User[\\/]+settings\.json|\.env\b(?![\w.-]*(example|sample|template))|\.ssh[\\/]|\.aws[\\/]|\.(npmrc|pypirc|netrc)\b|\.(bashrc|zshrc|bash_profile|zprofile|profile)\b|PowerShell_profile\.ps1|\.git[\\/]+hooks[\\/]|managed-settings\.json|(/etc/|ProgramData[\\/]+|Program Files[\\/]+|Application Support/)(ClaudeCode|claude-code|codex|gemini-cli|cursor|squidbrake|OpenAI)\b'
# ---- looking is fine: reading, listing, searching run without asking (and are still recorded)
- id: allow-reads
action: allow
reason: Only looks at information
match:
name: ["*.list*", "*_list*", "list*", "*.get*", "*_get*", "get*", "*.read*", "*_read*", "*.find*", "*_find*", "find*",
"*.search*", "*_search*", "search*", "*.describe*", "*_describe*", "*.query*", "*.fetch*", "*_fetch*",
"*.view*", "*_view*", "*.show*", "*_show*", "*.count*", "*_count*", "*.lookup*", "*_lookup*",
# Claude Code's own read-only / housekeeping tools
"Read", "Grep", "Glob", "LS", "WebSearch", "WebFetch", "NotebookRead", "ToolSearch", "TodoWrite",
"Task", "Agent", "AskUserQuestion", "EnterPlanMode", "ExitPlanMode", "Skill", "BashOutput", "TaskOutput",
"ListMcpResourcesTool", "ReadMcpResourceTool",
# Antigravity's own read-only / housekeeping tools (its file reads, edits and commands are checked above)
"read_url_content", "view_content_chunk", "read_resource", "read_terminal", "command_status",
"manage_task", "task_boundary", "notify_user"]
# ---- changes: a person decides (these give the approver a clearer reason than the default)
- id: approve-money-out
action: review
reason: Bank transfers, wires and payouts need finance sign-off
timeout_seconds: 900
approvers: ["role:finance", "role:admin"]
match:
name: ["*transfer*", "*wire*", "*payout*"]
- id: approve-refunds
action: review
reason: Refund - money goes back to a customer
timeout_seconds: 600
match:
name: ["*refund*"]
- id: approve-outbound-email
action: review
reason: Email to someone outside the company
timeout_seconds: 600
match:
name: ["email.send", "mail.*", "*send_email*", "*email_send*", "*send_mail*", "*gmail*send*"]
input_regex: '"to"\s*:\s*"[^"]*@(?!acme\.com")' # change acme.com to your company's email domain
- id: approve-writes-to-db
action: review
reason: Changes data in the database
match:
name: ["sql*", "db*", "*query*"]
input_regex: '\b(delete\s+from|update\s+\S+\s+set|alter\s+table|insert\s+(or\s+\w+\s+)?into|replace\s+into)\b'
# Pushing a named feature branch is everyday work (it runs); pushing main, master, a release or production branch,
# tags, everything, or the current branch without naming it waits for a person. Force pushes are held by the
# command checks whatever the branch.
- id: approve-git-push
action: review
reason: Pushing to a shared branch (main, master, release, production), tags, or an unnamed branch
match:
name: ["bash*", "shell*", "powershell*", "run_command*"]
input_regex: 'git\s+push((\s+-\S+)*(\s+[^\s"&;|-]\S*)?\s*("|&&|;|\|)|[^"&;|]*\s(\S+:)?(main|master|trunk|develop|release\S*|prod|production|HEAD)\b|[^"&;|]*\s--(all|tags|mirror)\b)'
# Deploying from a terminal: the hosting CLIs' production deploys. Previews (`vercel`, `netlify deploy` without
# --prod, `firebase hosting:channel:deploy`) run on their own.
- id: approve-prod-deploys-from-shell
action: review
reason: Deploys to production from the terminal
match:
name: ["bash*", "shell*", "powershell*", "run_command*"]
input_regex: '\bvercel\b[^"&;|]*\s--prod\b|\bnetlify\s+deploy\b[^"&;|]*\s--prod\b|\bfirebase\s+deploy\b|\bfly(ctl)?\s+deploy\b|\brailway\s+up\b|\bwrangler\s+(deploy|publish)\b(?![^"&;|]*\s--env[= ]+(dev|development|staging|preview)\b)|\beb\s+deploy\b|\b(serverless|sls)\s+deploy\b[^"&;|]*--stage[= ]+prod'
- id: approve-prod-deploys
action: review
reason: Production deploy
timeout_seconds: 900
match:
name: ["deploy*", "release*"]
input_regex: '\bprod(uction)?\b'
# ---- coding agents: everyday work runs on its own, so nobody turns Squidbrake off out of approval fatigue.
# The command checks still hold what can't be undone (git push --force, cloud deletes, DROP TABLE, curl | sh)
# and block what's catastrophic (rm -rf ~/, wiping a disk); the rule above holds changes to the agents' own setup.
- id: allow-coding-work
action: allow
reason: Everyday coding work (edits, tests, builds, installs, commits)
match:
name: ["Edit", "Write", "MultiEdit", "NotebookEdit", "Bash", "PowerShell"]
# Upstreams for the HTTP proxy: /proxy/<name>/<path> -> <url>/<path>
upstreams:
httpbin: https://httpbin.org