Blocks Cloudflare write commands, attribution lines in commits and PRs, and claude/ branch names

Blocks three kinds of shell commands: Cloudflare writes, attribution lines in commits and PRs, and claude/ branch names. Claude gets the reason back and can retry the right way.
| Command | Why |
|---|---|
cf deploy, cf migrate, cf init | Cloudflare writes stay on each project's Wrangler flow. |
git commit, gh pr, gh issue with a Co-Authored-By or "Generated with Claude" line | No tool attribution in commits, pull requests or issues. |
git checkout -b, git switch -c, git branch, git push naming a claude/ branch | Branches use a purpose prefix such as fix/ or chore/. |
Reading credential files is secret-guard's job.
git commit -F file, shell aliases and scripts get through. Use permission deny rules in settings.json where you need a hard guarantee.hooks/register.ts to change them.claude plugin marketplace add arasovic/claude-code-mods
claude plugin install guardrails@claude-code-mods
Restart Claude Code.
claude plugin validate .
claude plugin test .
../typecheck.sh guardrailshooks/register.ts 19 lines1import type { Register } from 'claude-code'
2
3// Commands refused on their text. Credential reads are secret-guard's.
4const FORBIDDEN = [
5 { re: /\bcf\s+(deploy|migrate|init)\b/, why: 'cf write commands are never run by Claude; deploys stay on the Wrangler flow.' },
6 { re: /\b(git\s+commit|gh\s+(pr|issue))\b[\s\S]*(Co-Authored-By|Generated with \[?Claude)/i, why: 'Remove the Co-Authored-By / tool-attribution line and retry.' },
7 { re: /\bgit\s+(checkout\s+-b|switch\s+-c|branch|push)\b[^|;&]*(?:[\s:+]|refs\/heads\/)claude\//, why: 'Branches never use the claude/ prefix; use fix/, style/, chore/ etc.' },
8]
9// ponytail: matches command text only; `git commit -F file`, aliases and scripts slip through. Permission deny rules for hard guarantees.
10
11export const forbiddenReason = (command: string) => FORBIDDEN.find(f => f.re.test(command))?.why
12
13export const register: Register = on => {
14 on('tool.call', { tool: 'Bash' }, ($, e, next) => {
15 const reason = forbiddenReason(e.command)
16 return reason ? { deny: `${$.plugin.name}: ${reason}` } : next(e)
17 })
18}
19