UnattendedClaude Code Guides

AI Agent Guardrails Example: Copy-Paste Claude Code Rules

By Aoba Works · Published · Sources checked · 10 min read

How this guide was made: drafted with AI assistance from the official sources linked below, then passed an automated editorial check before publishing. Our process

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.

Which option is right for you?
Your situationWhat to set up firstFirst page to open
Beginner, interactive sessions onlyDeny rules in .claude/settings.jsonConfigure permissions
Daily work, cron or CI jobs--permission-mode dontAsk plus --max-turnsRun Claude Code programmatically
You've had a destructive command slip throughA PreToolUse blocking hookHooks reference
Tight budget, API-key billing--max-budget-usd plus a Console workspace spend limitManage 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:

What does an AI agent guardrail look like in Claude Code?
Failure scenarioWhat it looks likeGuardrail layer
Destructive commandrm -rf, git push --force, git reset --hardDeny rules + PreToolUse hook
Runaway loopThe agent keeps retrying a failing fix for hours--max-turns + tool-call counter hook
Cost spikeOne overnight job burns a week of budget--max-budget-usd + spend limits
Premature "done"The agent stops while tests still failStop 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

  1. In your repo root, create .claude/settings.json if it doesn't exist.
  2. 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)"
    ]
  }
}
  1. 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

  1. Start claude in the repo and type /hooks.
  2. Check that your script appears under PreToolUse. The costs docs describe the same check for their example hook.
  3. 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:

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:

  1. Add --output-format json and log total_cost_usd from each run (headless docs). Read the log weekly.
  2. Set --max-budget-usd at roughly twice your typical run cost.
  3. 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:

Which guardrail layer catches which failure?
LayerCatchesMissesCost to set up
Deny rulesExact known commands, secret filesReworded variants5 minutes
PreToolUse hookPattern variants, custom logicAnything your regex doesn't cover15–30 minutes
--max-turns / tool-call counterLoopsA run that spends a lot in a few turns1 minute
--max-budget-usdCost spikesOvershoot past the cap1 minute
Stop hookQuitting with failing testsBad code that still passes the tests10 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):

  1. Paste the permissions block into .claude/settings.json and commit it.
  2. Add block-destructive.sh, register it, and confirm it shows up under /hooks.
  3. Add --max-turns and --max-budget-usd to every scheduled claude -p line. 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:

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

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.