# Claude Code guardrails: hooks, permissions and CLAUDE.md > Pick your deploy tools and get Claude Code deny rules, a PreToolUse hook and a CLAUDE.md section that stop your agent deleting an app, a volume or a database. Facts checked 2026-10-17. *Pick your deploy tools and get Claude Code deny rules, a PreToolUse hook and a CLAUDE.md section that stop your agent deleting an app, a volume or a database. The tool runs on the web page: https://ployz.dev/tools/claude-code-guardrails* ## What it blocks Each tool gets the commands that delete something you can't get back: a deployed app, a project, a volume, a database or its tables. `railway down`, `fly apps destroy`, `heroku pg:reset`, `docker volume rm`, `kubectl delete namespace`, `terraform destroy` and `DROP DATABASE` through `psql` are denied outright. So are the flags that skip a confirmation, like `vercel remove --yes` and `terraform apply -auto-approve`. A force push to `main`, a force push with `--all`, and `git push --mirror` are denied too. A few commands are part of normal work but still change real things, so they ask instead: `terraform apply`, `vercel remove` without `--yes`, and `git reset --hard`. Claude Code stops and shows you the command, and you say yes or no. Each command gets a rule for every spelling its CLI documents. `railway delete` also answers to `railway remove` and `railway rm`, and `railway project delete` to `railway projects rm`. Fly runs as `fly` or `flyctl`, and OpenTofu's `tofu` takes the same commands as `terraform`. Each CLI's docs are listed under Sources. The hook also catches Railway run through `npx @railway/cli` or `pnpm dlx @railway/cli`. ## Install it 1. Save the hook script as `.claude/hooks/guardrails.sh` in your project. It needs `jq` on your machine. 2. Merge the settings into `.claude/settings.json`. If the file already has `permissions` or `hooks`, add these entries to the lists it has. 3. Paste the section into your `CLAUDE.md`. 4. Start a new Claude Code session and ask it to run one of the blocked commands. It should refuse with the hook's reason. Commit all three files, and everyone who opens the project in Claude Code gets the same guard. ## Why a hook as well as permission rules Claude Code's permission rules match the command text, and its docs say a rule "isn't a security boundary around the program". `Bash(rm *)` stops `rm -rf build/` but not `/bin/rm -rf build/` or `bash -c 'rm -rf build/'`. Deny rules do catch a command inside a chain like `cd app && railway down`. The hook reads every command before it runs. It joins lines that end in `\`, splits the command at `;`, `&`, `|` and new lines, and looks for the guarded words anywhere in each part. So it also catches a full path, `bash -c`, flags in another order or run together like `-af`, and options between the words like `kubectl -n prod delete ns`. When it finds one it exits with code 2, which blocks the command, and tells Claude why. An ask match prints the JSON that makes Claude Code ask you instead. The settings also set `onFailure` to `block`, so on Claude Code v2.1.295 or later a hook that can't start or times out blocks the command rather than letting it run. The CLAUDE.md section tells the agent the same rules up front, so it stops and asks you before it reaches for a blocked command. Claude Code treats CLAUDE.md as context, not as a rule it enforces. That's why the section ships with the other two. ## What it can't stop Permission rules and hooks run on your own machine, and they only read the text of a command. An agent with a shell can work around them: a command built at run time, a script it writes and runs, an alias, or a CLI the guard doesn't know. A plain `git push --force` while `main` is checked out names no branch, so the hook can't tell it from a push to a feature branch. The hook also blocks `echo railway down`, `grep 'railway down'` and a commit message that names a blocked command, because it matches text and doesn't parse the shell. Treat it as a guard against mistakes, not against an agent set on getting through. The real check belongs where the deploy happens. Give the agent a token that can't touch production, or put production behind a step only a human can take. ## Where Ployz fits I build Ployz, and it puts that check on the server. When a coding agent saves or deploys a change that would remove a running service, delete a volume, detach a volume or remove a domain, Ployz refuses with `approval_required` and opens an approval for you. The same goes for removing a server, or draining the last server a global service runs on. It works the same from the CLI, from `ployz mcp`, and from an agent connected to Ployz Cloud. You approve or deny it in Ployz Cloud, and no setting turns it off. An agent connected to Ployz Cloud can't approve its own request. An agent running the `ployz` CLI with your login or a token can, so give it a short-lived token you can revoke. Keep this guard as well. Commands like `git push --force` and `terraform destroy` never go through Ployz, so only the guard sees them. To try Ployz with your own agent, paste `Read ployz.dev/agents.md and deploy this repo to my server.` into Claude Code, or [start free on your own servers](https://ployz.dev/auth/github). ## Questions ### What are Claude Code hooks? Hooks are commands Claude Code runs at fixed points. You set them in ~/.claude/settings.json for all your projects, .claude/settings.json for one project, .claude/settings.local.json for just you, or managed policy settings for a whole organization. A PreToolUse hook with the matcher Bash runs before every shell command Claude wants to run and gets the command as JSON on stdin. It blocks the command by exiting with code 2, and Claude gets the hook's stderr as the reason. A hook that exits 0 and prints JSON with permissionDecision set to deny blocks it too. Any other non-zero exit lets the command run, so a guard hook must exit 2 or print that JSON. ### How do Claude Code permissions work? Claude Code checks the rules in the permissions block of .claude/settings.json deny first, then ask, then allow, and the first match decides. A rule like Bash(railway down *) matches the command and anything after it. Deny and ask rules also catch the command inside a chain like cd app && railway down. They match the command text, though, so /usr/bin/railway down or bash -c 'railway down' gets past them. The hook catches those. ### Where does the CLAUDE.md file go? At the root of your project, as CLAUDE.md or .claude/CLAUDE.md. Claude Code reads it at the start of every session, along with a CLAUDE.local.md if you keep one. If the project has an AGENTS.md and no CLAUDE.md or CLAUDE.local.md, Claude Code v2.1.277 or later reads the AGENTS.md instead. CLAUDE.md is context, not a rule Claude Code enforces, which is why this tool pairs it with deny rules and a hook. ### Can Claude Code get around a deny rule or a hook? Yes, if it tries. Both run on your machine and read the command text. A command built at run time, a script file, or a program the guard doesn't know about runs anyway. Use them to stop mistakes, and keep production behind a token the agent doesn't have or an approval on the server. ### Why does the hook need jq? The hook reads the command out of the JSON Claude Code sends, and jq decodes it exactly, escapes included. Without jq the hook can't read the command, so it blocks every shell command and says jq is missing rather than letting them all through. ## Sources - [Claude Code: configure permissions](https://code.claude.com/docs/en/permissions) - [Claude Code: hooks reference](https://code.claude.com/docs/en/hooks) - [Claude Code: CLAUDE.md and memory](https://code.claude.com/docs/en/memory) - [Railway CLI: down](https://docs.railway.com/cli/down) - [Railway CLI: delete](https://docs.railway.com/cli/delete) - [Railway CLI: service](https://docs.railway.com/cli/service) - [Railway CLI: volume](https://docs.railway.com/cli/volume) - [Railway CLI: environment](https://docs.railway.com/cli/environment) - [Railway CLI: bucket](https://docs.railway.com/cli/bucket) - [flyctl apps destroy](https://fly.io/docs/flyctl/apps-destroy/) - [flyctl volumes destroy](https://fly.io/docs/flyctl/volumes-destroy/) - [Vercel CLI: remove](https://vercel.com/docs/cli/remove) - [Vercel CLI: project](https://vercel.com/docs/cli/project) - [Heroku CLI commands](https://devcenter.heroku.com/articles/heroku-cli-commands) - [Docker: system prune](https://docs.docker.com/reference/cli/docker/system/prune/) - [Docker: volume rm](https://docs.docker.com/reference/cli/docker/volume/rm/) - [Docker: compose down](https://docs.docker.com/reference/cli/docker/compose/down/) - [Docker: container rm](https://docs.docker.com/reference/cli/docker/container/rm/) - [Docker: compose rm](https://docs.docker.com/reference/cli/docker/compose/rm/) - [Kubernetes: namespaces](https://kubernetes.io/docs/tasks/administer-cluster/namespaces/) - [Kubernetes: persistent volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) - [Helm: uninstall](https://helm.sh/docs/helm/helm_uninstall/) - [PostgreSQL: DROP DATABASE](https://www.postgresql.org/docs/current/sql-dropdatabase.html) - [PostgreSQL: dropdb](https://www.postgresql.org/docs/current/app-dropdb.html) - [MySQL: DROP DATABASE](https://dev.mysql.com/doc/refman/8.4/en/drop-database.html) - [MariaDB: mariadb-admin](https://mariadb.com/docs/server/clients-and-utilities/administrative-tools/mariadb-admin) - [Terraform: destroy](https://developer.hashicorp.com/terraform/cli/commands/destroy) - [Terraform: apply](https://developer.hashicorp.com/terraform/cli/commands/apply) - [Git: push](https://git-scm.com/docs/git-push) - [Git: reset](https://git-scm.com/docs/git-reset) - [Ployz CLI](https://ployz.dev/docs/cli/overview) - [Ployz: destructive actions ask first](https://ployz.dev/docs/account/organizations#destructive-actions-ask-first) - [Ployz: connect an agent](https://ployz.dev/docs/account/connect-an-agent) [Deploy with Claude Code and other coding agents](https://ployz.dev/features/coding-agents)