Guardrails for coding agents you actually leave unattended: secrets, destructive git, config weakening, context bloat, verify-before-done, token metering. Measured friction (destructive-git guard): ~0.4 asks per day, zero false positives on a month of real multi-agent history.
Coding agents with shell access will eventually type git reset --hard at the
wrong moment, paste an API key into a source file, or declare "done!" while the
tests are red. Permission popups for everything train you to click yes;
no guardrails at all trains you to fear unattended runs. This suite sits in the
middle, built on three rules:
- Ask, don't wall. Destructive commands are sometimes exactly right. A
guard's job is a deliberate pause with a reason, not a lockout. Hard
denyis reserved for one thing only: high-confidence secrets. - Fail-open, always. A broken guard must never block your agent. Every handler and the dispatcher swallow their own failures.
- Friction is a budget. Every pattern here earned its place by being quiet on real history. If a guard nags, it is a bug.
| Guard | Event | Decision | What it catches |
|---|---|---|---|
secrets-guard |
before Write/Edit/Bash | deny (high-confidence) / ask (generic) | AWS/GitHub/Anthropic/OpenAI/Slack keys, private key blocks, JWTs, password = "..." assignments; softened to ask in docs/tests/examples paths |
git-guard |
before Bash | ask | reset --hard, force-push (--force-with-lease stays frictionless), clean -f, checkout -- ., restore, branch -D, stash drop, filter-branch |
config-protection |
before Write/Edit | ask | edits to linter/formatter configs - the agent should fix the code, not weaken the rules |
claude-md-guard |
before Write/Edit | ask | a CLAUDE.md growing past its line budget (default 200) - long instruction files eat context and lower rule adherence; shrinking is always silent |
verify-guard |
on Stop | block (capped) | "done" with failing checks: runs your project's verify commands and hands the failing output back so the agent self-corrects; attempt cap prevents infinite loops |
cost-guard |
on Stop | silent observer | sums real token usage from the session transcript into per-session snapshots; optional warning threshold |
All guards run through a single dispatcher (hooks/run.js) that chains
comma-separated guard ids in ONE node process - on Windows, per-hook spawns
cost ~75 ms each, so chaining three guards saves ~150 ms on every single tool
call. Semantics: run in order, first non-null decision wins, deny-capable
guards go first.
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.js\" secrets-guard,git-guard",
"timeout": 10
}As a Claude Code plugin:
/plugin marketplace add NiceLeader/agent-guardrails
/plugin install agent-guardrails@agent-guardrails
hooks/hooks.json wires all six guards with sane chains.
Manually / other runtimes: any agent that speaks the hooks stdin-JSON
contract (tool_name, tool_input, cwd, session_id - Codex CLI does) can
call the dispatcher directly. Wire the PreToolUse chains and the Stop chain
from hooks/hooks.json into your runtime's hook config.
// .agent-guardrails/verify.json in your project root
{ "commands": ["npm run typecheck", "npm test"], "timeout": 120, "maxAttempts": 3 }With a dirty working tree, every Stop runs the commands in order; the first
failure blocks the turn and pastes the failing output back to the agent.
After maxAttempts blocked turns it lets go with a notice - a fundamentally
broken approach must not loop forever. Pick hermetic commands: an interactive
first-run prompt or an env-dependent test will block every turn for reasons
that have nothing to do with the agent's work.
GUARD_HOOK_PROFILE=minimal(secrets only) /standard(default, everything)GUARD_DISABLED_HOOKS= comma-separated ids to switch offGUARD_CLAUDE_MD_MAX= line budget for claude-md-guard (default 200)GUARD_VERIFY_CMD= one-off verify command overrideGUARD_VERIFY_STATE_DIR= where verify-guard keeps per-session attempt stateGUARD_COST_DIR,GUARD_COST_WARN_USD= cost snapshots dir / opt-in warning
Timeout math for verify-guard: the hook-level timeout in hooks.json is 600s
total; verify.json's timeout is per command (default 120s). Keep
commands.length x timeout under the hook budget or a slow suite gets killed
mid-run - which fails open, i.e. no verification at all.
These guards protect against accidents, not adversaries. A hostile agent
could edit .agent-guardrails/verify.json, split a secret across two edits,
or otherwise route around a fail-open system - by design: a guard suite that
can hard-lock your agent is worse than one that can be circumvented. For the
after-the-fact layer that catches what slipped through, pair with the
flight recorder below.
The ask-rate claim comes from replaying these patterns against a full month of
real audit trails (23k tool calls across two agents): destructive-git asks
averaged 0.4/day and every single one was a genuinely destructive command. The
--force-with-lease and restore --staged exclusions exist because the first
draft flagged them and that was noise. The test suite (48 cases) pins both
sides: everything dangerous fires, every named safe variant stays silent.
- agent-flight-recorder - the observe half: zero-token JSONL trail of every tool call across your agents. Guards pause the dangerous moment; the recorder proves what actually happened.
- llm-wiki-ops - operations engine for agent-maintained Obsidian vaults.