AI Agent Guardrails Example: Copy-Paste Claude Code Rules
Here is a working AI agent guardrails example for Claude Code. Use deny rules in .claude/settings.json to block dangerous tools, a PreToolUse hook that exits with code 2 to catch the destructive commands those rules miss, and the --max-turns and --max-budget-usd flags to cap loops and spending. Each layer covers a different failure. Together they let a scheduled run work without someone watching it.
Claude Code's hooks give you, in Anthropic's words, "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them." So a guardrail written as a hook doesn't depend on the model remembering an instruction.
Which option is right for you?
The right first guardrail depends on how you run Claude Code today. Start with the row that fits you and add the others later.
| Your situation | What to set up first | First page to open |
|---|---|---|
| Beginner, interactive sessions only | Deny rules in .claude/settings.json | Configure permissions |
| Daily work, cron or CI jobs | --permission-mode dontAsk plus --max-turns | Run Claude Code programmatically |
| You've had a destructive command slip through | A PreToolUse blocking hook | Hooks reference |
| Tight budget, API-key billing | --max-budget-usd plus a Console workspace spend limit | Manage costs |
Why each pick: deny rules cost nothing and apply to every session. dontAsk keeps unattended runs from stalling. Hooks can match patterns that rules can't. Budget caps stop the one failure you otherwise find out about from your invoice.
What does an AI agent guardrail look like in Claude Code?
In Claude Code, a guardrail is a permission rule, hook script or CLI cap that limits the agent's actions, runtime and spending.
Most guardrail explainers you'll find for this query sort guardrails into input, output and system-level types, then use chatbot examples like PII redaction or bank transfer approvals. That framing works for customer-facing bots. A coding agent running at 3 a.m. fails in other ways, so this guide is organized around three of them:
| Failure scenario | What it looks like | Guardrail layer |
|---|---|---|
| Destructive command | rm -rf, git push --force, git reset --hard | Deny rules + PreToolUse hook |
| Runaway loop | The agent keeps retrying a failing fix for hours | --max-turns + tool-call counter hook |
| Cost spike | One overnight job burns a week of budget | --max-budget-usd + spend limits |
| Premature "done" | The agent stops while tests still fail | Stop hook |
If you haven't set up hooks before, read Claude Code Hooks Lifecycle: Every Event, Explained first. It shows where PreToolUse and Stop fire.
How do you block destructive commands with permission rules?
Claude Code permission rules go in permissions.allow, ask and deny arrays in .claude/settings.json. Deny rules block matching tool calls before they run.
Step 1: Create the project settings file
- In your repo root, create
.claude/settings.jsonif it doesn't exist. - Paste this starting point and change the commands to match your stack:
{
"permissions": {
"allow": [
"Bash(npm run test *)",
"Bash(npm run lint *)",
"Bash(git status *)",
"Bash(git diff *)",
"Bash(git commit *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Read(./.env)",
"Read(./.env.*)",
"Edit(./.env)"
]
}
}
- Commit the file so every machine and every scheduled job uses the same rules. The permissions docs note that you "can check permission settings into version control to share them."
Step 2: Get the wildcard right
The trailing turns a rule into a prefix match. The headless docs warn that "the space before is important": Bash(git diff*) would also match git diff-index.
Common mistake: trusting a scoped deny rule to catch every variant
The CLI reference says a scoped rule like Bash(rm ) "denies only calls that match as written." For example, Bash(rm -rf ) won't match rm -r -f build/ or find . -delete. The fix is the hook in the next section, which reads the actual command string and can match several patterns.
Also notice the ask rule for git push. In a -p run with no permission host, the headless docs say requests that would prompt "are denied either way". So in unattended runs, ask effectively means "never". That's usually what you want for pushes.
How do you write a hook that stops dangerous commands?
A Claude Code PreToolUse hook runs before every matching tool call. If the script exits with code 2, the call is blocked. The hooks reference puts it this way: "Exit code 2 is the way a hook signals 'stop, don't do this'."
Step 1: Save the script
Save this as .claude/hooks/block-destructive.sh and run chmod +x on it. It needs jq installed.
#!/bin/bash
# Example guardrail: block destructive shell commands
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eiq \
'rm +-[a-z]*r[a-z]* *-?[a-z]*f|rm +-r +-f|find .* -delete|git +push .*(--force|-f)|git +reset +--hard|git +clean +-[a-z]*f|drop +(table|database)|terraform +destroy'; then
echo "Blocked by guardrail: '$cmd' matches a destructive pattern. Use a safer alternative or leave a note for a human." >&2
exit 2
fi
exit 0
Step 2: Register it
Add a hooks block next to permissions in the same settings file. This follows the structure in the hooks reference:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh"
}
]
}
]
}
}
Step 3: Verify
- Start
claudein the repo and type/hooks. - Check that your script appears under PreToolUse. The costs docs describe the same check for their example hook.
- Ask Claude to run
rm -r -f tmp/on a throwaway folder. The call should come back blocked.
Common mistakes: the script isn't executable (chmod +x fixes it), jq is missing on the cron host, or the matcher is bash in lowercase. Matchers are tested as JavaScript regular expressions, so case matters (hooks reference). For six more scripts in the same style, see Claude Code Hooks Examples: 7 Copy-Paste Scripts.
How do you stop runaway loops in unattended runs?
Claude Code's --max-turns flag caps agentic turns in print mode. The CLI reference is clear that there is "No limit by default." Set it on every scheduled job.
The cron command
Here is a cron-safe invocation that combines the caps:
claude -p "Run the test suite, fix failing tests in src/, and commit the fix" \
--permission-mode dontAsk \
--permission-prompts none \
--max-turns 30 \
--max-budget-usd 3.00 \
--output-format json
What each flag does, per the official docs:
--permission-mode dontAsk: "Claude Code denies every call that would otherwise prompt, which is useful for locked-down CI runs" (headless docs). Yourallowrules still apply.--permission-prompts none: Claude is told that nobody can approve a request and not to retry it, so it doesn't burn turns asking again. This needs v2.1.259 or later (headless docs).--max-turns 30: the run "exits with an error when the limit is reached" (CLI reference). Your script can branch on the non-zero exit code.
The number 30 is an example. Pick it by looking at how many turns a normal successful run takes and adding a margin.
A second layer: count tool calls
Turns and tool calls are different things, so a hook that counts tool calls per session gives you a backstop. This example uses session_id, which every hook receives in its input:
#!/bin/bash
# Example: deny tool calls after 150 in one session
input=$(cat)
sid=$(echo "$input" | jq -r '.session_id')
f="/tmp/claude-toolcalls-$sid"
n=$(( $(cat "$f" 2>/dev/null || echo 0) + 1 ))
echo "$n" > "$f"
if [ "$n" -gt 150 ]; then
echo "Tool-call limit reached. Stop, summarize progress, and list what is unresolved." >&2
exit 2
fi
exit 0
Register it under PreToolUse with the matcher .* so every tool counts. If runs stop before you expect, work through Why Claude Code Stops Mid-Automation (and How to Fix It). Your own guardrail may be the cause.
Stop hook: don't let it quit while tests fail
A Stop hook can send Claude back to work by returning {"decision": "block", "reason": "..."} (hooks reference). Always check stop_hook_active first, or the hook can bounce Claude back indefinitely:
#!/bin/bash
input=$(cat)
[ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ] && exit 0
if ! npm test --silent >/dev/null 2>&1; then
echo '{"decision":"block","reason":"Tests still fail. Fix them, or explain why they cannot be fixed, before stopping."}'
fi
exit 0
How do you cap Claude Code costs per run?
Claude Code's --max-budget-usd flag stops a print-mode run once estimated API spend reaches your cap. Treat it as a soft cap, not an exact one.
The CLI reference gives two caveats. The cap is checked against a client-side estimate that "can differ from your bill," and "Spend can pass the cap, so leave headroom." Subagent spend counts toward it (CLI reference).
For a sense of scale, Anthropic reports that across enterprise deployments the average is "around $13 per developer per active day". That's an interactive-use average, not a figure for automations, but it's a useful sanity check. A nightly job that costs more than a full day of human use is probably looping.
Practical layers, from cheapest to strongest:
- Add
--output-format jsonand logtotal_cost_usdfrom each run (headless docs). Read the log weekly. - Set
--max-budget-usdat roughly twice your typical run cost. - On API billing, set a workspace spend limit in the Claude Console. The costs docs list "Workspace spend limits" as the cap for Console users.
To find where tokens go in the first place, see Claude Code: How to See Token Usage (and Reduce It).
Which guardrail layer catches which failure?
Claude Code's layers overlap on purpose. No single layer covers everything, so pick by failure mode:
| Layer | Catches | Misses | Cost to set up |
|---|---|---|---|
| Deny rules | Exact known commands, secret files | Reworded variants | 5 minutes |
PreToolUse hook | Pattern variants, custom logic | Anything your regex doesn't cover | 15–30 minutes |
--max-turns / tool-call counter | Loops | A run that spends a lot in a few turns | 1 minute |
--max-budget-usd | Cost spikes | Overshoot past the cap | 1 minute |
Stop hook | Quitting with failing tests | Bad code that still passes the tests | 10 minutes |
One limitation applies to all of them. A -p session runs the hooks in a project's .claude/settings.json "even in a folder you've never trusted." Only point unattended jobs at repos you control, or use --bare with an explicit --settings file. Also keep a recovery path, because guardrails reduce damage but don't undo it. Claude Code Checkpoints: How Rollback Works in Automations covers what you can roll back.
Next steps
Do today (DIY):
- Paste the
permissionsblock into.claude/settings.jsonand commit it. - Add
block-destructive.sh, register it, and confirm it shows up under/hooks. - Add
--max-turnsand--max-budget-usdto every scheduledclaude -pline. The cron setup guide shows where those lines live.
If you'd rather not build this yourself: a Claude subscription doesn't configure guardrails for you, but it does put a hard ceiling on usage. Pro is $20/month and Max starts at $100/month, and Claude Code is included in paid plans (as of October 2026; check claude.com/pricing). A subscription suits solo builders who want predictable monthly spend. It's a poor fit if you need per-run dollar accounting: the costs docs note that the /usage session cost figure "isn't relevant for billing purposes" for Pro and Max subscribers. For per-token control, API billing through the Claude Console with workspace spend limits is the better choice.
Related guides on this site:
- Claude Code Hooks Examples: 7 Copy-Paste Scripts
- Claude Code Hooks Lifecycle: Every Event, Explained
- Why Claude Code Stops Mid-Automation (and How to Fix It)
FAQ
What is an example of an agent in AI?
Claude Code is an example of an AI agent. It reads files, runs shell commands, edits code and decides its next step from the results, all in a loop. That autonomy is what makes guardrails necessary. A chatbot only produces text, while an agent acts on your system.
Is a CLAUDE.md instruction enough as a guardrail?
No. A line like "never force-push" in CLAUDE.md is guidance the model usually follows, not enforcement. Permission rules and hooks are checked by Claude Code itself on every tool call, so use them for anything that would be costly to get wrong.
Should I use bypassPermissions for overnight jobs?
For most solo setups, no. --dangerously-skip-permissions is equivalent to bypassPermissions (CLI reference). dontAsk with explicit allow rules lets routine work through and denies everything else, which is a safer default for runs nobody watches.
Do ask rules work in cron runs?
Not as prompts. In a -p run with no permission host, anything that would prompt is denied (headless docs). Treat ask as "deny when unattended, ask when I'm at the keyboard."
Sources
- Configure permissions – Claude Code Docs
- Hooks reference – Claude Code Docs
- Automate actions with hooks – Claude Code Docs
- Run Claude Code programmatically – Claude Code Docs
- CLI reference – Claude Code Docs
- Choose a permission mode – Claude Code Docs
- Manage costs effectively – Claude Code Docs
- Claude pricing
- AI Guardrails in Agentic Systems Explained – AltexSoft
- 5 Types of AI Guardrails, Examples, and Implementation Best Practices – Cequence
- 8 Best AI Agent Guardrails Solutions in 2026 – Galileo
- GEO: Generative Engine Optimization (arXiv:2311.09735)
Spotted something out of date or wrong? Tell us at shigoto.ai.note@gmail.com. Guides are re-checked against official sources on a rolling schedule.