A Claude Code plugin that puts hard limits around what Claude can do in your project, tells you what Claude actually loads at session start, and lints your CLAUDE.md files against Anthropic's own guidance.
Three features, all enforced with plain python3 scripts and no dependencies:
- Enforcement hooks (
PreToolUse): destructive shell commands and edits to secrets or protected paths are denied before the tool runs. Deployment and infrastructure files require your approval. - Session start report (
SessionStart): a compactOK / WARN / SKIPPEDchecklist of instruction files, rules, skills, hooks and plugins, with warnings for the mistakes the docs warn about. - Skills :
/guardrails:guardrails-lint,/guardrails:guardrails-statusand/guardrails:guardrails-test.
The repository is its own marketplace (alstaty), so it takes two commands in a Claude Code session:
/plugin marketplace add ahmed-alstaty/guardrails-plugin
/plugin install guardrails@alstaty
Or from your shell:
claude plugin marketplace add ahmed-alstaty/guardrails-plugin
claude plugin install guardrails@alstaty # add --scope project to enable it for the whole repo
From a local checkout, replace the GitHub shorthand with the path: claude plugin marketplace add ./guardrails-plugin. To try it for one session without installing: claude --plugin-dir ./guardrails-plugin.
Requirements: Claude Code with plugin support and python3 (3.8 or newer) on PATH. No pip packages.
Every Bash command is tokenized (quotes, escapes, &&, ||, ;, |, $( ), backticks, bash -c, eval, sudo/ env/ xargs wrappers) and each simple command is judged on its own, so true && rm -rf / is caught and rm build/old.log is not.
| Category | Denied | Asks for approval | Allowed |
|---|---|---|---|
rm /shred /unlink |
recursive removal of / ,~ ,$HOME ,. ,.. ,* , the project directory, anything outside it, an unexpanded$VAR/ ,--no-preserve-root , protected or secrets paths |
non-recursive rm of a file outside the project;xargs rm -r ;find with-delete and no filter |
files and directories inside the project ( rm -rf build ,rm -rf node_modules ) |
git |
push --force /-f /+ref /--mirror tomain ormaster (current branch is detected when no refspec is given); pushing a deletion ofmain /master ;reset --hard ;clean -fd /-fx ;branch -D ;checkout -- . ,checkout . ,restore . ;filter-branch ,filter-repo |
push --force with an unknown branch;--force-with-lease to main;checkout -- <file> ,restore <file> ;clean -f ;stash drop/clear ;reflog expire ;gc --prune |
everything else, including push -f to a feature branch |
| Databases | DROP DATABASE ,DROP SCHEMA ,dropdb ,dropDatabase() ,FLUSHALL /FLUSHDB ,db:drop ,migrate reset (inline,-c /-e , or heredoc, when a database client or migration tool is in the command) |
DROP TABLE ,TRUNCATE ,DELETE FROM withoutWHERE |
|
| Permissions | chmod -R 777 ,chmod/chown -R on root or outside the project |
||
| Remote code | curl ... | sh ,wget ... | sudo bash ,bash <(curl ...) ,sh -c "$(curl ...)" |
curl ... | jq |
|
| Disks and system | mkfs* ,wipefs ,fdisk ,dd of=/dev/... ,> /dev/sd* , fork bombs,shutdown /reboot ,kill -1 ,crontab -r ,mv x /dev/null |
sudo su ,sudo -i |
|
| Shell writes | redirection ( > ,>> ,&> ,tee ,cp /mv destination,sed -i ,truncate ) into a secrets file, a protected path, or.git/ |
writes outside the project or into an ask path |
File tools (Edit, Write, MultiEdit, NotebookEdit) are judged by path:
| Verdict | Paths |
|---|---|
| Denied | secrets: .env ,.env.* ,*.pem ,*.key ,*.p12 ,*.pfx ,*.jks ,id_rsa* ,id_ed25519* ,credentials.json ,service-account*.json ,secrets/** ,.netrc ,.htpasswd ,.aws/credentials ,.npmrc ,.pypirc ; protected:.git/** ; anything outside the project directory (the session scratchpad is exempt) |
| Asks | Dockerfile* ,docker-compose*.yml ,compose*.yml ,.github/workflows/** ,.gitlab-ci.yml ,terraform/** ,*.tf ,k8s/** ,kubernetes/** ,helm/** ;migrations/** only when an edit deletes content or aWrite overwrites an existing migration |
| Allowed | .env.example ,.env.sample ,.env.template ,*.example ,*.sample ,*.template ,*.pub , and everything else inside the project |
Reading is never blocked; cat .env goes through so Claude can still understand configuration.
A block is never silent. The hook returns the documented permissionDecision: "deny" with a permissionDecisionReason that Claude sees, plus a systemMessage for you, and echoes the reason to stderr. Every reason ends with how to proceed:
Guardrails blocked: git push --force to main rewrites shared history. To allow once, run the command yourself in your terminal. To allow always, add it to .claude/guardrails.json allow_commands.
If the hook itself fails (unreadable input, internal error) it returns ask, not allow.
Create .claude/guardrails.json in the project (committed, applies to everyone) or ~/.claude/guardrails.json (just you). Both extend the defaults; lists are appended, nothing is replaced.
{
"allow_commands": ["rm -rf /tmp/scratch*", "git push --force origin main"],
"block_commands": ["npm publish*", "terraform apply*"],
"protected_paths": ["package-lock.json", "docs/adr/**"],
"ask_paths": ["config/production.yml"],
"secrets_globs": ["*.token", "config/keys/**"],
"allow_paths": ["Dockerfile", ".env.ci"]
}
allow_commandsandblock_commandsare shell-style globs matched against the whole command and against each simple command inside it.block_commandswins overallow_commands;allow_commandswins over the built-in rules.protected_pathsare always denied,ask_pathsprompt you,secrets_globsare denied, andallow_pathsis checked first and exempts a path from all of them.- A pattern without
/matches a file name anywhere (*.pem); a pattern with/matches the path relative to the project root (secrets/**); an absolute or~/pattern matches outside the project.
The session report shows which config files were loaded and warns about invalid JSON or unknown keys. A broken config never disables enforcement; the defaults keep applying.
At startup, resume and /clear the plugin prints this checklist in your terminal (as a system message) and hands Claude a short summary, so Claude knows the guardrails are on and will report a block instead of trying to route around it. Run /guardrails:guardrails-status to see the full report again at any time:
GUARDRAILS session report (cwd: /work/app)
Instruction files (load at launch)
WARN [project] CLAUDE.md (243 lines) - over 200 lines (243); @import missing: docs/setup.md
OK [user] ~/.claude/CLAUDE.md (31 lines)
SKIPPED AGENTS.md present but NOT read: a CLAUDE.md/CLAUDE.local.md exists at cwd or above ...
Rules (.claude/rules)
OK [project] .claude/rules/api.md (12 lines) paths: src/api/**/*.ts (on demand)
Nested instruction files (load when Claude reads files there)
OK packages/web/CLAUDE.md (18 lines) on demand
Skills
WARN [project] /deploy (.claude/skills/deploy/SKILL.md): no description, Claude cannot decide when to use it
Hooks
OK .claude/settings.json: PostToolUse(1)
SKIPPED .claude/settings.local.json: not present
OK plugin guardrails: PreToolUse, SessionStart (this plugin)
Plugins
OK guardrails@alstaty v1.0.1 (enabled in user settings)
Guardrails
OK config: .claude/guardrails.json (allow_commands 2, block_commands 1, ...)
Summary: 7 OK, 2 WARN, 2 SKIPPED. Run /guardrails:guardrails-lint to fix instruction files.
Warnings cover: instruction files over 200 lines or over 4 MiB (Claude Code skips those), @imports that point at missing files, the same subject under "always" in one file and "never" in another, repeated IMPORTANT/ ALWAYS/ NEVER, CLAUDE.local.md not in .gitignore, skills without a name or description, SKILL.md files that are a summary with no steps and no pointer to steps, AGENTS.md present but not read (or present without a CLAUDE.md), invalid settings JSON, and plugins enabled in settings but not installed.
Two channels, two costs. The full report is a system message: it is shown to you and never enters the model, so it is free. What Claude receives is a few lines (the OK / WARN / SKIPPED counts, up to four warnings, and the instruction not to route around a block), about 150 to 300 tokens once per session start, cached on every later turn. Each denied or asked tool call adds one sentence, about 40 tokens. Running /guardrails:guardrails-status or /guardrails:guardrails-lint puts that output into the conversation like any other tool result, so use them when you want them, not on every session.
| Command | What it does |
|---|---|
/guardrails:guardrails-lint [files] |
Lints every instruction file that loads (or the files you name) against the documented rules: line count vs 200, procedures that should be skills, path-by-path directory descriptions Claude can derive from code, repeated emphasis words, vague rules, broken @paths , duplicated rules across nested files, contradictions. Writes a suggested trimmed copy of each file and aSKILL.md draft for each procedure into a scratch directory. It changes nothing until you pick what to apply. |
/guardrails:guardrails-status |
Re-runs the session report on demand. |
/guardrails:guardrails-test ["cmd" ...] [--path FILE] |
Dry-runs the blocklist against a built-in sample set, or the commands and paths you pass, and prints ALLOW /ASK /DENY with the reason. Nothing is executed. |
All three are user-invoked only (disable-model-invocation: true), so they cost no context until you call them.
tests/run_tests.sh
The script copies a fixture project to a temporary directory, feeds sample hook JSON to the real hook scripts and checks the decisions: destructive rm blocked, safe rm allowed, .env edits blocked, force push to main blocked, normal git push allowed, Dockerfile edits ask, config overrides, fail-safe on garbage input, the session report's warnings, the linter's findings, and the manifests. It exits 0 when everything passes.
To check the manifests with the CLI: claude plugin validate . (marketplace) and claude plugin validate .claude-plugin/plugin.json --strict.
- This is enforcement for Claude Code only. The hooks run when Claude Code calls a tool. They do nothing for commands you type yourself, for other agents, or for scripts Claude writes and you run later.
- It is not a sandbox. A hook sees a command string, not what the command does. A build script that deletes files, a
make cleantarget,docker compose down -v, or a program invoked through a name the parser does not recognise all pass. Use Claude Code's sandbox and permission modes for isolation; use this plugin for guardrails on the common mistakes. - It does not replace backups or branch protection. Force pushes to
mainare blocked here, but the remote should refuse them too. Commit often. - Heuristics have edges. The tokenizer handles the common shell constructs, not every one. Variables are not expanded (a command built from
$VARis judged on its text, andrm -rf $VAR/is denied for that reason). Branch detection for a baregit push -frunsgit symbolic-refin the project; if that fails the hook asks instead of guessing. The lint and contradiction checks are pattern-based and will produce some false positives; the skill tells Claude to verify each one before reporting it. python3must be onPATHfor the hook processes. If it is missing, Claude Code reports a hook error and the tool call proceeds; the session report will not appear, which is your signal.- Windows : paths are handled with forward slashes and the scripts avoid POSIX-only calls, but the shell rules target bash-style commands. PowerShell commands are not parsed.
guardrails-plugin/
βββ .claude-plugin/
β βββ plugin.json plugin manifest
β βββ marketplace.json marketplace "alstaty" listing this plugin at "."
βββ hooks/hooks.json PreToolUse (Bash; Edit|Write|MultiEdit|NotebookEdit) and SessionStart
βββ scripts/
β βββ guardrails_common.py config , path classification, hook output
β βββ shell_guard.py tokenizer and command rules
β βββ pretooluse.py PreToolUse entry point
β βββ instruction_scan.py instruction-file discovery and heuristics
β βββ session_report.py SessionStart entry point (also --text)
β βββ lint_claude_md.py linter used by guardrails-lint
β βββ dry_run.py used by guardrails-test
βββ skills/
β βββ guardrails-lint/ SKILL.md + reference.md
β βββ guardrails-status/SKILL.md
β βββ guardrails-test/SKILL.md
βββ tests/run_tests.sh hook tests with a fixture project
MIT. Copyright (c) 2026 Ahmad Alstaty.