AI coding agent guardrails: a checklist
The threat model, the layers that contain it — permissions, hooks, secret guards, network approval, sandboxing — and a copy-run checklist for what to automate versus stop and ask.
A guardrail system for an AI coding agent is five layers, not one setting: permissions (what the agent may touch at all), hooks (enforcement before and after each tool call), secret guards (keeping credentials unreadable), network approval (no silent outbound calls), and a sandbox for anything you cannot fully trust. Claude Code gives you the permissions block and the hooks API to build all of it; this guide wires them into a defense-in-depth setup and ends with a checklist you can run against your own settings.json.
Start from what already ships. Claude Code's free /security-review command and security-guidance plugin are a real baseline for catching risky code patterns, and you should keep them running. They are review tools, though — they do not stop the agent from doing damage in the first place. The layers below are the enforcement half.
The threat model
An agent that can edit files, run shells, and fetch URLs has three failure modes worth designing against: destructive commands (an rm -rf or a force push that eats work), exfiltration (secrets or source pushed to an attacker-controlled endpoint), and privilege escalation (the agent acquiring or writing code that acquires more access than the task needs). These are not hypothetical at scale.
| Failure mode | Concrete example | Primary control |
|---|---|---|
| Destructive command | rm -rf on an absolute path, non-lease force push | PreToolUse deny-list on Bash |
| Exfiltration | curl POST of a file to an external host | Deny curl/wget, gate WebFetch |
| Secret exposure | Agent reads .env or a private key into context | Permission deny on secret paths |
| Privilege escalation | chmod 777, sudo, editing CI credentials | Deny + ask rules, sandbox |
| Indirect injection | Instruction hidden in a fetched page or README | PostToolUse output scan |
Layer 1 — Permissions (allow / ask / deny)
Permissions decide what the agent can reach before any hook runs. The model is allow for the safe and repetitive, ask for the consequential, deny for the never. Anchor the setup here, because a path the agent cannot read cannot be leaked and a tool it cannot call cannot be abused:
{
"permissions": {
"allow": [
"Bash(npm run test:*)",
"Bash(npm run lint:*)",
"Bash(git status)",
"Bash(git diff:*)"
],
"ask": [
"WebFetch",
"Bash(git push:*)",
"Bash(npm publish:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(./**/*.pem)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(sudo:*)"
]
}
}The deny list is the load-bearing part. Everything else is convenience; this is the boundary an injected or over-eager instruction physically cannot cross.
Layer 2 — Hooks (PreToolUse and PostToolUse)
Permissions are coarse — allow or not. Hooks add judgement: a PreToolUse hook inspects the exact command and can veto it, and a PostToolUse hook inspects the result and can flag it. Community references like dwarvesf/claude-guardrails and disler/claude-code-hooks-mastery are good reading for the range of what is possible. The minimal wiring:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.py" }
]
}
],
"PostToolUse": [
{
"matcher": "WebFetch|Bash|Read",
"hooks": [
{ "type": "command", "command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/guard-output.py" }
]
}
]
}
}The PreToolUse guard exits 2 to block a catastrophic command and 0 to allow; the PostToolUse guard warns without rolling anything back. For the exact scripts and the reasoning behind block-early / warn-late, see harden Claude Code against prompt injection, and the hooks cookbook for the other lifecycle events.
Layer 3 — Secret guards
Denying .env and key files is the floor, not the ceiling. Two more moves matter: exclude secret paths from what the agent can even see when it lists a directory, and add a PreToolUse check that refuses commands which would print environment variables (env, printenv, cat .env). The goal is that a credential never enters the context window in the first place — once it is in context, it can end up in a diff, a log, or a fetched request.
Layer 4 — Network and curl approval
Every guardrail set treats outbound network access as consequential. Deny raw curl and wget so nothing can silently POST your data outward, and route legitimate fetches through WebFetch under an ask rule so a human sees the destination. This is the single most effective exfiltration control, because it removes the tool rather than trying to detect misuse of it.
Layer 5 — Sandbox guidance
What to automate vs stop and ask
Guardrails only survive if they match how work actually flows. Automate the reversible and repetitive; stop and ask for the consequential and irreversible.
- Automate (allow): running tests, linters, formatters, type-checks,
git status/git diff, reading source files, building. - Stop and ask:
git push, publishing packages, any outbound fetch, editing CI/CD or infrastructure config, installing new dependencies, touching migrations. - Never (deny): reading secret files, raw
curl/wget,sudo, recursive deletes of absolute paths, world-writable permissions.
The guardrails checklist
Run this against your project's settings.json and .claude/hooks/. Each item is either present or it is a gap:
permissions.denyblocks.env,.env.*,secrets/**, and private-key globs.permissions.denyblocks rawcurl,wget, andsudo.permissions.askgatesWebFetch,git push, and package publishing.- A PreToolUse hook on
Bashblocks recursive absolute deletes, fork bombs, pipe-to-shell, non-lease force pushes, andchmod 777. - The PreToolUse deny-list is short enough that it never fires on ordinary commands (test it).
- A PostToolUse hook scans
WebFetch/Bash/Readoutput for injection-shaped text and warns without blocking. - A PreToolUse check refuses commands that would print environment variables.
- Untrusted repos and self-executed code run in a sandbox with no host credentials or production network.
- New-dependency installs require approval rather than running automatically.
- Claude Code's free
/security-reviewruns on generated code before it is committed. - Every guardrail has been observed firing at least once, and re-tested after the last
settings.jsonchange.
/security-review and security-guidance plugin, alongside the Engineer Kit.The mistake to avoid
The failure is treating guardrails as a one-time config commit. The threat surface grows every time you add an MCP server, a dependency, or a new tool, and a hook silently breaks the moment a settings.json typo slips in. Re-run the checklist when the setup changes, and pair enforcement with review — read how to review AI-generated code for security for the half that catches what the hooks let through.