claude-code-guardrails
Hooks, commands and an output style I use with Claude Code, extracted from my private config.
- Hooks — run outside the model's judgment: block bulk
git add/git commit -aand WHERE-less SQL before the Bash call executes, record the session-start commit, snapshot work state before context compaction and feed it back in right after, run the project's own formatter after edits. - Commands — a delivery loop (
/plan→/implement→/verify→/critical-review→/crosscheck) and doc-maintenance commands (/save-learnings-to-docs,/cleanup-docs,/refactor-claude-md,/init-local-md). - Reviewer backend —
/critical-reviewruns an independent, read-only reviewer on the diff (Codex CLI, Claude Code CLI, or your own command) through one adapter script, and never shows it the task brief. - Skill —
browser-mcp: how to drive chrome-devtools-mcp cheaply and safely (which server for what, token-cheap check order, screenshot/emulate pitfalls, secrets, logged-in apps). - Output style —
terse.md: result first, no padding. - Fleet sync — push commands/skills/styles to your servers over SSH, audit drift with
--check.
Quick start (fresh Mac)
You need Claude Code ≥ 2.1, installed and logged in. Homebrew is optional.
- Command-line tools. Install them if
git --versionasks for them:xcode-select --install. They provide git and python3. - The plugin. In Claude Code, run the two commands below, then restart Claude Code:
/plugin marketplace add tillmeier/claude-code-guardrails /plugin install guardrails@tillmeier- Hooks are active right away.
- Commands and the skill are namespaced:
/guardrails:plan,/guardrails:critical-review,guardrails:browser-mcp, ….
- Check what works here.
/guardrails:setup-checkprints ✓/✗ per part, with the exact fix for each gap. It installs nothing. Each ✓ says what it rests on, e.g. "login not checked" or "browser launch not tested". It also flags hooks that run twice because yoursettings.jsonregisters them as well. - Optional browser servers, for the
browser-mcpskill and/verify --ui. They need Node (^20.19,^22.12or ≥ 23;brew install node) and Google Chrome:
Skip the second server if you don't want an agent acting with your logins.# isolated: temp profile, logged out — measuring and public pages claude mcp add -s user chrome-devtools -- npx -y chrome-devtools-mcp@1.10.1 --isolated --screenshotMaxWidth 1280 --screenshotMaxHeight 2000 # optional, attached to your running Chrome (144+) with its logins. Needs remote debugging enabled # at chrome://inspect/#remote-debugging; while that is on, any local process can attach. claude mcp add -s user chrome-live -- npx -y chrome-devtools-mcp@1.10.1 --autoConnect --redactNetworkHeaders --screenshotMaxWidth 1280 --screenshotMaxHeight 2000 - Optional extras:
/output-style Terse.- A reviewer for
/critical-review: the Codex CLI (npm i -g @openai/codex && codex login) or the Claude Code CLI. - The Codex plugin for
/plan --codex.
- Restart, then run
/guardrails:setup-checkagain.
If the same Mac also carries a hand-installed copy of these files (or a private config they came from), pick one. Step 3 shows hooks that would run twice.
By hand
For bare /plan, /verify, … and control over which hooks run:
git clone https://github.com/tillmeier/claude-code-guardrails ~/claude-code-guardrails
cp ~/claude-code-guardrails/commands/*.md ~/.claude/commands/
cp ~/claude-code-guardrails/output-styles/*.md ~/.claude/output-styles/
mkdir -p ~/.claude/skills && cp -R ~/claude-code-guardrails/skills/browser-mcp ~/.claude/skills/
then wire the hooks you want into ~/.claude/settings.json. /critical-review looks for its backend
script at ~/claude-code-guardrails/scripts/review-backend.sh; for a checkout elsewhere, export
REVIEW_BACKEND=<checkout>/scripts/review-backend.sh. Wire the hooks:
"hooks": {
"SessionStart": [
{"hooks": [{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/session-start.sh\""}]},
{"matcher": "compact", "hooks": [{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/compact-handoff-inject.sh\""}]}
],
"PreToolUse": [{"matcher": "Bash", "hooks": [
{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/sql-guard.sh\"", "timeout": 10},
{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/git-staging-guard.sh\"", "timeout": 10}
]}],
"PostToolUse": [{"matcher": "Edit|Write|MultiEdit", "hooks": [{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/format-file.sh\""}]}],
"PreCompact": [{"hooks": [{"type": "command", "command": "bash \"$HOME/claude-code-guardrails/hooks/precompact-handoff.sh\""}]}]
}
Requirements: bash ≥ 3.2, git, python3. /critical-review needs one reviewer: the
Codex CLI (logged in), the Claude Code CLI, or a command of your
own (see Reviewer backend). Tests need
bats-core; the fleet sync needs ssh + rsync.
Hooks
| File | Fires on | Does |
|---|---|---|
git-staging-guard.sh |
PreToolUse (Bash) | Denies git add -A/--all/./:/ without a -- pathspec and git commit -a/-am/--all — incl. git -C forms, command chains, (…), $(…), { …; } |
sql-guard.sh |
PreToolUse (Bash) | Denies DROP TABLE/DATABASE, TRUNCATE TABLE, DELETE FROM t with no WHERE, on any line of the command |
session-start.sh |
SessionStart | Writes HEAD, the list of already-dirty files and a blob of each one's content to per-session marker files, so /verify and /critical-review can scope to what this session changed. Write-once per session id: the re-fire after a compaction or resume keeps the original baseline |
precompact-handoff.sh |
PreCompact | Appends branch, last commits, dirty files and the newest plan file to ~/.claude/handoffs/<session>.md (override dir with CLAUDE_HANDOFF_DIR); GC after 14 days |
compact-handoff-inject.sh |
SessionStart (compact) |
Feeds the last section of that handoff file back into the context right after the compaction (additionalContext, capped at 2500 characters); silent for any other start |
format-file.sh |
PostToolUse (Edit|Write|MultiEdit) | Runs the nearest Biome/Prettier config (walking up to the git root), else black/ruff/isort/php-cs-fixer if installed, else nothing. Disable with CLAUDE_NO_FORMAT=1 |
A blocked call exits 2; the tool call never runs and the model gets the stderr as the reason:
$ git add -A && git commit -F -
BLOCKED by git-staging-guard: bare `git add` sweep (-A / --all / . / :/) with no explicit `--` pathspec
offending segment: git add -A
Stage explicitly instead — name the files:
git add <path> [<path>...]
git diff --cached --name-only # then read the index back before committing
[...]
$ git add hooks/git-staging-guard.sh tests/guard.bats
$ git commit -m "guard: judge commit flags per token"
Notes:
- Only exit 2 blocks a tool call; exit 1 is a non-blocking notice and the call proceeds. The tests assert exit codes, not just messages.
- The two Bash guards gate on a pure-bash prefilter and only parse JSON when the payload could match, so they cost ~milliseconds on unrelated calls. Numbers in docs/git-staging-safety.md.
- They are habit guards, not sandboxes: they inspect text, and an unusual spelling can evade them. A payload that merely mentions a guarded command (docs, commit messages) passes — and if it trips the guard anyway, write it with Edit/Write instead of Bash.
Commands
| Command | Does |
|---|---|
/plan |
Short cited spec + verification plan before code; approval gate. --codex adds an external plan review (needs the optional Codex plugin, says so and continues without it) |
/implement |
Executes an agreed plan in small steps, verifies each, asks instead of guessing, halts on surprises |
/verify |
Ship gate: drives the changed flow for real and emits PASS/FAIL/SKIP with evidence. --ui iterates over screenshots per breakpoint (needs a browser MCP, degrades without) |
/critical-review |
Exit gate: an independent reviewer sees this session's diff (not the brief), you judge each finding against the code, approve the fixes once in plan mode. --review-only for findings in chat, --adversarial for the deeper prompt-mode pass, --base/--since/--feature/--final for wider scopes. Stops with a clear message when no reviewer is available — it never reviews its own work instead |
/crosscheck |
Read-only audit of recent unverified work: locate the evidence, verify before asserting, "ran" ≠ "worked" |
/save-learnings-to-docs |
Routes session learnings to the cheapest doc tier (hook → path-scoped rule → doc → skill → local → memory → CLAUDE.md last) and reconciles related docs |
/cleanup-docs |
Doc audit: measure → verify → cut; token-greps every fact before removing text; reports in bytes |
/refactor-claude-md |
Restructures CLAUDE.md into a lean core + on-demand tiers, no @-imports |
/init-local-md |
Generates a gitignored .claude/local.md from detected environment |
/setup-check |
What of this plugin is set up on this machine: git/python3, reviewer, Codex plugin, Node, Chrome, browser MCP servers, output style, hooks registered twice. ✓/✗ with the exact fix; installs nothing. Not named doctor, because Claude Code has a built-in /doctor |
The loop in practice: docs/workflow-recipes.md. Plan files land in .claude/plans/,
gitignore them if you don't want them tracked.
Skill: browser-mcp
skills/browser-mcp/SKILL.md loads before the first
chrome-devtools-mcp call. It covers which
server to use for what, the cheapest check order (script → snapshot → screenshot), filePath
roots, wait_for / emulate / screenshot pitfalls, tab hygiene, keeping secrets out of the
transcript, and scraping or replaying the internal API of logged-in apps. /verify --ui points to
it. It assumes the two servers from Quick start step 4 (measured on
1.10.1); the second one is optional.
Reviewer backend
scripts/review-backend.sh is the only vendor-specific piece behind /critical-review. It picks a
reviewer, assembles the review input from git (the diff; for the working tree also untracked file
bodies), runs the reviewer read-only and prints the review — plus one stderr line saying which
backend, model and mode actually ran.
REVIEWER= |
Runs | Notes |
|---|---|---|
codex |
codex exec review (Codex's built-in reviewer), or codex exec --sandbox read-only with the adversarial prompt when there is focus text / --adversarial |
needs codex login; stdin is closed so nothing can hang |
claude |
claude -p --restricted with only Read/Grep/Glob, every write and shell tool disallowed, your settings/hooks/plugins not loaded, permission prompts denied, no session persisted, structured JSON output |
prompt mode only |
custom |
REVIEW_CUSTOM_CMD via bash -c, prompt on stdin, review on stdout |
any other vendor, or a wrapper of your own |
Untracked symlinks are listed but never followed, so a link to a file outside the repository cannot
ship that file to the reviewer. Unset, it auto-detects in that order (custom command configured →
codex logged in → claude) and
refuses with exit 3 and the list of what it checked when nothing is usable. A backend you name
explicitly but that is unavailable is also exit 3 — never a silent substitute. Models are pinned in
review-backend.conf (copy scripts/review-backend.conf.example to ~/.claude/review-backend.conf;
the real file is gitignored): a vendor default can be deprecated or unavailable on your account
without warning. The codex reasoning effort is pinned the same way (CODEX_REVIEW_EFFORT, default
high): the built-in reviewer takes no effort flag and the flagship's own default is low.
Prompt-mode output follows scripts/review-findings.schema.json.
Three helpers sit next to it, all plain bash/python3 and tested in tests/review-tools.bats:
scope-delta.sh tells /critical-review which lines of each changed file are this session's (one
verdict per file — FULL, DELTA, UNCHANGED or LIKELY — so a failed lookup lands in the
"scope unverifiable" bucket instead of silently dropping findings); review-log.py appends the
review log as immutable events (run, finding, decision, resolution, brief_drop,
brief_drop_overruled, joined by run id) to ~/.claude/logs/critical-review.jsonl
(CLAUDE_REVIEW_LOG_DIR overrides); critical-review-stats.py reads it — distinct ids, undecided
findings reported as unresolved, never guessed.
The reviewer is deliberately given no way to receive the task brief: framing a change as intended or
bug-free collapses a reviewer's true-positive rate (arXiv 2603.18740). /critical-review writes the
brief for itself and applies it only after the review, behind a mandatory re-read of the source.
One honest caveat: a Claude reviewer of Claude-written code is a fresh context, not an
independent model. It catches what the author-context missed, but shares the model's blind spots.
Codex or another vendor is the real second opinion; claude is the fallback when that is not
installed.
bash scripts/review-backend.sh --detect # which reviewer would run
bash scripts/review-backend.sh --base HEAD~3 # review the last three commits
bash scripts/review-backend.sh --scope working-tree --focus "error handling in the retry path"
bash scripts/review-backend.sh --print-prompt --base main # show the prompt-mode input, run nothing
Fleet sync
scripts/sync-commands.sh pushes selected commands, skills, scripts and output styles to a list
of servers over SSH/rsync and can activate an output style remotely. Hosts and file lists live in
sync-commands.conf (gitignored — copy sync-commands.conf.example). --check compares every
configured file against each server (POSIX cksum) and reports MISSING / DRIFT / unmanaged files,
exit 2 on findings; --dry-run shows what would transfer.
Tests
bats tests/ # 96 tests: guard patterns, SQL guard, formatter, session marker, handoff + read-back, reviewer backend, review helpers, setup check, wiring
CI runs the same suite on ubuntu; macOS bash 3.2 is the primary target. The backend tests run
against fake codex/claude executables that record their arguments — no model is ever called.
Background
The git guard exists because a chained git add -A && … && git commit once swept a parallel
session's work into a commit. The incident, the approaches that were tried and rejected, and the
measured hook costs are in docs/git-staging-safety.md. Why the
reviewer never sees the brief, why --restricted and not --bare, measured review times, and what
the reviewers found in this port are in docs/review-backend.md. What
/setup-check claims and why, and the CLI output it parses: docs/setup-check.md.
Not included
- My commit/push command (couples to private tooling; the staging discipline it implements is in the doc above).
- 20 SEO skills and 12 agents that sit next to these files in my config — they are AgriciDaniel's claude-seo (MIT), not mine.
- Anything client-specific. Scrubbed by grep before every commit.
Differences from my private copy
- Absolute paths →
${CLAUDE_PLUGIN_ROOT}/$HOME. git-staging-guard.shjudgesgit commitflags per token, so--alland-a --amendare caught too (the private copy missed both).sql-guard.shwas ajq | readone-liner in settings.json that only saw the first line of a command; it now checks every line and has the same prefilter as the git guard.session-start.shkeys the marker file by session id (${CLAUDE_SESSION_ID}) instead of an undocumented env-file export, and adds the dirty-file / blob snapshot.critical-review.mdcallsscripts/review-backend.shinstead of the Codex plugin's companion script (my copy is pinned to Codex), logs throughscripts/review-log.pyinstead of an inline node logger, classifies the session's lines withscripts/scope-delta.shinstead of inline awk (a broken snapshot lookup goes to "scope unverifiable" rather than to the full diff), and stops instead of reviewing its own diff when no reviewer is available.compact-handoff-inject.shrefuses session ids that are not[A-Za-z0-9._-]before building a path from them.format-file.shreads the file path from the hook JSON itself, nojqwrapper needed.browser-mcpkeeps the lessons and drops the sites they were learned on, my own setup and my risk decisions; server names are examples.sync-commands.shconfig moved tosync-commands.conf;--helpworks without one; emptySERVERSis an error, not a silent no-op.
License
MIT — see LICENSE.