Skip to content

Policy guards

PreToolUse guards come in two kinds: anti-gaming guards stop Claude from weakening the standards, and blast-radius guards keep irreversible operations with you. Guards always apply, including in mode: warn. The dependency check runs alongside them on install commands and manifest edits.

Anti-gaming

Guard Trigger Decision
protected-config Edit/Write to .guardrails.yaml, .golangci.{yml,yaml,toml,json}, .claude/settings*.json, .claude/rules/*.md, or changes to [tool.ruff], [tool.mypy], [tool.coverage], [tool.pytest] in pyproject.toml. Also shell writes to those files (>, >>, tee, sed -i, perl -pi, rm, mv, cp, …). deny
suppression New lines adding //nolint, # noqa, # type: ignore, # pragma: no cover, //go:build ignore, # tflint-ignore, # hadolint ignore, # shellcheck disable, //lint:ignore deny, unless the line directly above is a guardrails:allow <reason> comment you wrote
skipped-test New t.Skip/t.Skipf/t.SkipNow, @pytest.mark.skip/skipif, pytest.skip(, @unittest.skip deny, unless guarded by testing.Short()
skipped-test New xfail without a linked issue (URL to an issue/PR, or a PROJ-123 key) deny
lowered-threshold An edit that lowers or removes --cov-fail-under / fail_under, or a Bash command passing --cov-fail-under below the configured threshold deny
generated-file Edit to a file with a Code generated ... DO NOT EDIT header deny
lockfile Direct edit of go.sum, uv.lock, poetry.lock, .terraform.lock.hcl deny
secrets New content matching high-confidence secret patterns (AWS, GitHub, Slack, Stripe, Google, Anthropic, OpenAI keys; private key blocks), or any write to .env* (except .env.example, .sample, .template) deny
test-writer The test-writer subagent editing anything but test files deny

The suppression, skipped-test, secret and threshold guards compare the file before and after the edit, so they only fire on new lines — never on ones already in the file.

Allowing a suppression

When a suppression really is right, write the tag yourself, on the line above:

// guardrails:allow gosec G404 is fine here, this RNG seeds test fixtures
rng := rand.New(rand.NewSource(seed)) //nolint:gosec

Claude adding a guardrails:allow tag is itself denied, so the escape hatch stays yours.

Blast radius

Guard Trigger Decision
terraform-state terraform/tofu/terragrunt apply, destroy, import, taint, untaint, force-unlock, state rm/mv/push/…, workspace delete, or any -auto-approve deny
registry-push docker/podman push or image push, buildx build --push, --output type=registry, crane/skopeo/oras/ko push or copy ask
git-history git push --force/-f/--force-with-lease/--mirror/--delete, +refspec, --no-verify on push/commit/merge/rebase, commit -n, reset --hard deny
git-history Push to a protected branch (main, master by default), explicitly or implicitly from the current branch deny
git-history git clean -f ask
cluster-write kubectl/oc apply, delete, edit, patch, replace, scale, create, drain, cordon, rollout restart…, or helm install/upgrade/uninstall/rollback, against a context not matching guards.kubectl_safe_contexts (default kind-*, *-dev) ask

Bash commands are split on ;, &&, ||, |, &, newlines, subshells and command substitution, and leading sudo, env, VAR=value, command, exec, time and nohup are stripped, so cd infra && sudo terraform apply is caught.

Belt and braces

The hooks doc calls if-matching best effort, and hooks can be disabled, so hard denies should also live in permissions.deny. Plugins can't ship permission rules; guardrails init prints a ready-made block to merge into .claude/settings.json.