<< All versions

Skill v1.0.0

currentAutomated scan0/100
paldom/python-skills/agent-guardrails
──Details
PublishedSeptember 27, 2026 at 02:10 PM
Content Hashsha256:0fa37eafbf6aa352...
Git SHA
──Files
Files (1 file, 16.2 KB)
SKILL.md16.2 KBactive
SKILL.md · 270 lines · 16.2 KB

version: "1.0.0" name: agent-guardrails description: Installs agentic guardrails into a Python repo — Claude Code hooks, a Stop gate that signs off only what pre-commit accepts, vetted public skills — and troubleshoots hooks that don't fire or block. Use for 'add Claude Code hooks', 'write an AGENTS.md', 'make this repo agent-ready', 'agent keeps bypassing checks', 'agent says done but pre-commit fails'. Not for authoring the pre-commit config. license: MIT


agent-guardrails

Install the agent-facing enforcement layer of a Python repo: Claude Code hooks that give a coding agent instant, deterministic feedback (and hard-block the few things it must never do), the settings wiring that ships them, a concise rules file, and vetted public skills. The failure this skill fixes is advisory-only guardrails — rules the model forgets under context pressure, hooks that silently never fire, and exit 1 "blocks" that block nothing.

When NOT to use

  • Git hooks — .pre-commit-config.yaml, commit-msg/pre-push stages. The

python-precommit skill, if installed, owns the commit-time layer.

  • CI workflows, required checks, branch rulesets — the python-ci skill. CI is

the authoritative gate this skill's layers feed into, not what it installs.

  • Validating LLM output data — Pydantic/Instructor schema validation,

retry-with-error-context, SQL safety validators. That is runtime data validation in your application, not repo guardrails; this skill does not cover it.

  • Tuning what the checks themselves do — ruff rules (python-lint), type

checker config (python-typing), test infra (python-testing). This skill wires when checks run against agent actions, not their contents.

  • Generic prompt engineering or system-prompt writing.

The layer model (read this first)

Guardrails only work layered; each layer catches what the previous cannot (convention, not an official spec — but it is strong cross-source consensus):

LayerRunsCatchesBypassable?
Rules file (AGENTS.md/CLAUDE.md)read at session startconventions, commandsyes — advice the model can forget
Claude Code hooksat the moment of each agent actionbad writes/commands before or as they happenyes — local, user-controlled
pre-commitat commitwhole-diff issuesyes — --no-verify
CI + rulesetsat mergeeverything, on infra the agent doesn't controlno — the only real gate

Two consequences to state to the user every time: anything that must always happen belongs in a hook, not prose (rules files are wishes; hooks are contracts the harness executes), and hooks are never the security boundary — they are local and user-editable, and alternate tool paths (bash heredocs instead of Edit) route around file matchers, so CI and server-side rules stay authoritative. (Hooks from settings files do run inside subagents, with agent_id in the input — that gap is closed.) Hooks' one unique power: PreToolUse denial happens before the file ever exists on disk — the only layer that can do prevention rather than cleanup.

Workflow

1. Audit what exists

bash
python3 "${CLAUDE_SKILL_DIR}/scripts/check_guardrails.py" --root .

Read-only: reports settings files and scopes, wired hooks and their matchers, rules files and their length, obvious defects (lowercase matchers, exit 1 used as a block, missing script files, unguarded Stop hooks). Also check claude --version — hook behavior has version drift; the current docs are the source of truth: https://code.claude.com/docs/en/hooks

2. Rules file — context, not enforcement

Write (or trim) AGENTS.md at the repo root, with CLAUDE.md containing only @AGENTS.md so every agent reads one canonical file. Start from assets/AGENTS.md.template. Rules that earn their place in a Python repo:

  • exact commands, not prose — uv sync, uv run pytest,

uv run ruff check --fix, uv run mypy src/

  • "Use uv, never pip or poetry" (the single most-reported agent failure:

defaulting to global pip installs)

  • "Run lint + typecheck + tests before calling a task done"
  • "Never weaken a gate to pass it — no lowering coverage, skipping tests, or

|| true" (agents under pressure take the cheapest path to green)

  • blast-radius lines: "flag changes to auth, dependencies, or lockfiles for

human review"

Keep it under ~200 lines and hand-curated: auto-generated context files have measurably hurt agent success in evaluations — write what is specific to this repo, not a generic overview. Recurring PR-review corrections are the best source of new lines.

3. Hooks — the deterministic layer

Copy the three hook scripts from assets/hooks/ into .claude/hooks/, make them executable, then wire assets/settings.json.template into .claude/settings.json. The architecture is thin JSON, heavy script: the settings file only routes events; logic lives in versioned scripts.

GoalEvent + matcherScript
Block dangerous bash (--no-verify, SKIP=, hooksPath tricks, force-push main, bare pip, broad rm -rf)PreToolUse on Bashguard_bash.py
Instant ruff feedback on the Python file just edited (report, never rewrite)PostToolUse on `Edit\Write\NotebookEdit`lint_edited_file.py
Sign off only what the commit gate would acceptStopstop_verify.py

All three are stdlib Python (no jq): the bash guard lexes the command (wrappers, bash -c, newlines, git -c/-C options) instead of grepping it, so git push origin HEAD:main --force, git -c core.hooksPath=/dev/null commit and python3.12 -m pip are caught, and git commit -uno is not.

The parity rule. The Stop gate never re-implements checks: when the repo has .pre-commit-config.yaml, stop_verify.py runs pre-commit itself over the changed + untracked files — the identical gate git commit will run (pre-commit run --files checks the worktree as-is; no stashing). Auto-fixing hooks (prettier, ruff --fix, whitespace fixers) exit non-zero after repairing the tree, so the gate runs twice and a clean second run passes. A hand-rolled subset (ruff check . alone) drifts from the config and produces the classic failure: agent says done, the owner's commit fails on prettier. Repos without pre-commit keep the single fast VERIFY_CMD fallback.

The exit-code contract — the part everyone gets wrong (verified against https://code.claude.com/docs/en/hooks, 2026-09-10):

  • `exit 0` — proceed. stdout may carry a JSON decision; on exit 0 plain

stderr goes only to the debug log, so a warning the user must see goes in {"systemMessage": "..."} on stdout.

  • `exit 2` — the blocking exit code on blockable events, and nothing in

JSON can override it. The reason shown is the JSON decision's reason if you printed one, otherwise stderr — so the simple recipe is still "exit 2 + reason on stderr". What "block" means is event-dependent: PreToolUse prevents the call; PostToolUse cannot undo — stderr is feedback only (and it fires only on success; failed calls go to PostToolUseFailure); Stop prevents stopping and forces the agent to keep working.

  • `exit 1` (or any other code) — non-blocking unless stdout holds valid

decision JSON, which is honored on every exit code. A bare exit 1 with a message is logged and ignored; this single fact breaks a large share of hook setups.

Rules for hooks that survive contact with a team:

  • Scope PostToolUse to the single edited file and keep it sub-second — it

runs synchronously on every matching call. Anything slower (mypy, tests) belongs in the Stop hook, pre-commit, or CI.

  • Stop hooks MUST honor stop_hook_active in the stdin JSON — the template

re-verifies once, then releases (exit 0) instead of blocking again — or they loop; the harness force-overrides after 8 consecutive blocks.

  • Give the Stop gate a generous explicit timeout (the template wires 300 s)

and pre-build hook environments at setup (uv run pre-commit install --install-hooks): a hook that hits its timeout is non-blocking, so a cold first-run env build silently skips verification.

  • Matchers are case-sensitive: bash never matches Bash. Letters, digits,

_, -, spaces, commas and | are exact-match lists; any other character makes it an unanchored regex (Edit.* also catches NotebookEdit).

  • Edit|Write|NotebookEdit misses shell-driven edits (sed -i, heredocs).

Cover the gap with the Stop-hook verification pass, which sees the whole worktree; FileChanged exists for watching paths if you need per-file reaction to shell writes.

  • ConfigChange (matcher project_settings) can block edits to

.claude/settings.json — the one hook that stops an agent from rewriting its own guardrails mid-session.

  • Hard-deny (exit 2) only deterministic, unambiguous violations; prefer

non-blocking feedback for style-level issues — an over-eager PreToolUse block can stall the agent entirely instead of guiding it.

  • Roll out observe → warn → enforce: log-only first, then narrow denies for

patterns that caused real damage.

Full mechanics (per-event exit-2 semantics, matcher and if-field rules, JSON decision channels, scopes, known version-drift issues): references/hooks-reference.md. Which check belongs at which layer: references/layer-assignment.md.

4. Settings scopes and permissions

  • .claude/settings.json — committed, applies to every contributor. This is

the team contract; changes to it deserve PR-level scrutiny (see security below).

  • .claude/settings.local.json — gitignored, personal experiments.
  • ~/.claude/settings.json — user-global. All scopes merge additively; a

project file cannot switch off a user hook, only add.

  • Hooks tighten policy but do not replace the permission system: a hook

exit 0 does not approve anything, and the if filter is best-effort and fails open on ambiguous commands. Hard allow/deny of tools belongs in permissions rules; hooks add checks the permission grammar cannot express.

5. Install and vet public skills

Recommend established public skills instead of rebuilding them — but treat every third-party skill, plugin, and MCP server as an unreviewed dependency with full user permissions. Minimum vet before install: read the plugin manifest (an mcpServers entry means network access), every hook it ships and the commands they run, and each full SKILL.md body; check requested permissions are proportional to the stated function; pin versions and review updates like dependency bumps. Curated starting points and the full checklist: references/vetting-skills-and-plugins.md.

6. Verify by tripping every wire

A guardrail that has never fired is unverified. Test each one deliberately:

bash
# 1. Scripts standalone, with realistic stdin — check exit code + stderr
echo '{"tool_input":{"command":"git commit --no-verify -m x"}}' \
| python3 .claude/hooks/guard_bash.py; echo "exit=$?" # expect exit=2
echo '{"tool_input":{"command":"git status"}}' \
| python3 .claude/hooks/guard_bash.py; echo "exit=$?" # expect exit=0
# 2. Audit passes
python3 "${CLAUDE_SKILL_DIR}/scripts/check_guardrails.py" --root .

Then, in a live session: ask the agent to do a blocked action (expect a blocked call with the reason), edit a file with a deliberate lint error (expect hook feedback), and end a turn with a failing check (expect the Stop gate to push back). In a repo with pre-commit, also end a turn with an unformatted YAML/Markdown file in the tree — the Stop gate must run the commit gate and auto-fix or block, never sign it off. If a hook doesn't fire: matcher case, settings nesting, and that python3 is on PATH — in that order.

Output spec

Done means:

  • AGENTS.md (≲200 lines, repo-specific) with CLAUDE.md importing it.
  • .claude/settings.json wiring PreToolUse guard, PostToolUse single-file

check, and a Stop gate that runs the repo's own commit gate (pre-commit over the change set) or a single fast verify command; scripts in .claude/hooks/, executable, blocking only via exit 2 + stderr, Stop guarded by stop_hook_active.

  • Every hook demonstrated to fire and to block (step 6 transcript).
  • scripts/check_guardrails.py exits 0.
  • The user told, in one sentence each: hooks are convenience, CI is the gate;

and committed hooks execute on every contributor's machine.

Failure modes & gotchas

SymptomCause / fix
Hook "blocks" but the action proceedsexit 1 — only exit 2 blocks; everything else is logged and ignored
Block reason never shown to the agentreason printed as plain text on stdout — on exit 2 the reason is the JSON reason field if any, else stderr; plain stdout text is neither
Warning from an exit-0 hook never appearsstderr on exit 0 goes to the debug log only — print {"systemMessage": "..."} on stdout
Hook never firescase-sensitive matcher (bash≠Bash), wrong settings nesting, unexpanded $HOME, or a python3 that is not on PATH — all fail silently
Stop hook loops forevermissing stop_hook_active guard; harness caps at 8 blocks but fix the guard
Agent "done" but the owner's git commit fails (prettier reformats, whitespace fixers fire)Stop gate re-implements a subset instead of running the repo's pre-commit — wire stop-verify.sh's parity path (pre-commit run --files over the change set)
First Stop in a fresh clone hangs or times outcold hook-env build; pre-build at setup (pre-commit install --install-hooks) and keep the generous timeout — a timed-out hook is non-blocking, i.e. a silent pass
Agent edits files but the format hook is silentedit made via Bash (sed -i, cat >) — `Edit\Write` never fires; rely on the Stop-hook worktree pass
Formatter hook fights the agentan auto-fixing PostToolUse hook rewrites the file under the agent and its next edit fails the stale-file check — the shipped hook only reports; auto-fix belongs to pre-commit and the Stop gate's second pass
PreToolUse exit-2 stalls the agent instead of correcting itknown behavior in some versions — keep hard denies rare and actionable; put style feedback in non-blocking channels
Hooks treated as securitythey are local and user-controlled; alternate tool paths (shell writes) route around file matchers — CI + rulesets are the boundary. Hooks do run inside subagents.
Committed hook = code execution on every clone.claude/ changes are code: CODEOWNERS them, review in the web UI before checking out PR branches (real CVEs exist here)
Plugin/skill turns out maliciousvet before install (step 5), pin versions, prefer official/curated sources
Hook advice from tutorials doesn't match behaviorthe event surface and blocking semantics drift across versions — trust https://code.claude.com/docs/en/hooks over any static list, including this skill's

Files

  • scripts/check_guardrails.py — read-only audit of settings, hooks, matchers

and rules files; non-zero exit on defects.

  • assets/settings.json.template — hook wiring for the three-script setup.
  • assets/hooks/guard_bash.py — PreToolUse deny (lexer-based, stdlib):

--no-verify/-n/abbreviations, SKIP= hook skips, core.hooksPath redirects, force-push to main in any argument order, bare pip / python -m pip outside uv, recursive force-delete of broad paths.

  • assets/hooks/lint_edited_file.py — PostToolUse single-file ruff check +

format --check via the project's own ruff (uv run); reports, never rewrites.

  • assets/hooks/stop_verify.py — Stop gate with the stop_hook_active guard,

runs in $CLAUDE_PROJECT_DIR: the repo's pre-commit over changed + untracked files (twice, so auto-fix hooks converge; uv run fallback), or one VERIFY_CMD list in repos without pre-commit; releases via systemMessage.

  • assets/AGENTS.md.template — starting rules file.
  • references/hooks-reference.md — full hook mechanics + known issues.
  • references/layer-assignment.md — which check runs at which layer.
  • references/vetting-skills-and-plugins.md — vetting checklist + curated skills.
All versions