Guardrails for vibe coders: PreToolUse rules that stop rm -rf and secret leaks
Originally published at pilothouse.dev/blog/guardrails-for-vibe-coders. Vibe coding works until the agent runs rm -rf on the wrong folder, prints your .env into the transcript, or force-pushes over a teammate's branch.
Originally published at pilothouse.dev/blog/guardrails-for-vibe-coders.
Vibe coding works until the agent runs rm -rf on the wrong folder, prints your .env into the transcript, or force-pushes over a teammate's branch. Most of us respond in one of two ways: approve every prompt by hand, or turn on "skip permissions" and hope.
There's a middle option. Every major coding agent now has a PreToolUse hook: a point right before a tool call runs where something can say allow, ask or deny. Pilothouse (which I build) puts a rules engine at that hook, for Claude Code, Codex, OpenCode and Grok Build at once. This post is the rule file I'd hand a friend, with each rule tested.
Setup (2 minutes)
brew install sabr-software/tap/pilothouse # or: curl -fsSL https://pilothouse.dev/install.sh | sh
pilothouse setup # installs the hooks (shows the diff first)
pilothouse policy init # writes a commented starter ~/.pilothouse/policy.yaml
The fast path: presets
If you only do one thing, do this:
pilothouse policy preset guarded-autopilot on
pilothouse policy preset no-secrets on
guarded-autopilot is meant as a safer alternative to --dangerously-skip-permissions. It lets routine repo work run without prompts (reading, editing inside the repo, tests, builds, linters, read-only git). It blocks destructive commands like rm -rf ~, git reset --hard, force-pushes and DROP TABLE. And it asks before pushes, deploys, secrets and sudo. no-secrets blocks .env files, private keys, cloud credentials and credential dumps such as printenv and gh auth token, while leaving .env.example alone.
The rule file I actually use
Here's a hand-written ~/.pilothouse/policy.yaml that covers the same ground, so you can see how the rules work and adjust them:
version: 1
rules:
# 1. Never let an agent wipe a broad path.
- name: no-broad-rm-rf
description: Recursive force-deletes of /, ~, $HOME, . or *
match: { kind: shell }
when: 'command.matches(r"\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|-r\s+-f|-f\s+-r)\s+(/|~/?|\$HOME/?|\./?|\*)(\s|;|&|\||$)")'
action: deny
message: Recursive delete of a broad path is blocked. Delete the specific files instead.
# 2. No force-push, in any spelling (but --force-with-lease is fine).
- name: no-force-push
match: { kind: shell, command: "*git*push*" }
when: 'has_word(command, "--force") || has_word(command, "-f")'
action: deny
message: Force-push is blocked. Open a PR instead.
# 3. Pushing to main needs a person.
- name: ask-before-pushing-main
match: { kind: shell, command: "*git*push*" }
when: 'has_word(command, "main") || has_word(command, "master") || branch == "main"'
action: ask
message: This pushes to main.
# 4. Keep agents out of secrets (templates are fine).
- name: no-secret-files
match: { path: [".env", ".env.*", "*.pem", "*.key", "~/.ssh/**", "~/.aws/**"] }
when: '!paths.all(p, p.endsWith(".example") || p.endsWith(".sample"))'
action: deny
message: Secrets are off limits. Use .env.example or ask me for the value.
# 5. No dumping the environment or tokens into the transcript.
- name: no-credential-dumps
match: { kind: shell }
when: 'has_word(command, "printenv") || command.matches(r"\benv\s*($|\|)") || command.contains("gh auth token")'
action: deny
message: Don't print credentials into the session.
# 6. Piping a download into a shell needs a person.
- name: ask-curl-pipe-sh
match: { kind: shell }
when: 'command.matches(r"\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh)\b")'
action: ask
message: This runs a script straight from the internet.
# 7. Production databases are off limits.
- name: no-prod-db
match: { kind: shell, command: ["*psql*prod*", "*mysql*prod*"] }
action: deny
message: Use the staging database.
A few things worth knowing about the format:
-
match:fields are globs, and they're AND-ed together:kind,command,path,tool,cwd,repo,branch,agent. -
when:is a CEL expression withcommand,paths,branch,repo,hour,weekdayand more available.has_word()matches whole words, so--force-with-leasedoesn't trip--force. - A relative path pattern like
.envmatches at any depth. Start it with/or~/to anchor it. - If several rules match, any
denywins, thenask, thenallow. - A file with an error isn't used, and the last version that compiled stays in force, so a typo never silently drops your deny rules.
Test before you trust it
Run any rule against a command without an agent involved:
$ pilothouse policy check
Γ’Εβ ~/.pilothouse/policy.yaml 7 rules
$ pilothouse policy test "rm -rf ~"
Decision: DENY by no-broad-rm-rf
Message: Recursive delete of a broad path is blocked. Delete the specific files instead.
$ pilothouse policy test "rm -rf node_modules"
Decision: none Γ’β¬β no rule matched; the agent's own permission settings apply.
$ pilothouse policy test "git push --force-with-lease origin feat"
Decision: none Γ’β¬β no rule matched; the agent's own permission settings apply.
$ pilothouse policy test "git push origin main"
Decision: ASK by ask-before-pushing-main
$ pilothouse policy test --tool Read --path ~/app/.env.example
Decision: none Γ’β¬β no rule matched; the agent's own permission settings apply.
Those are real results from the file above. Then see what your new rules would have done to last week's actual tool calls:
pilothouse policy replay --since 7d
Rules without YAML
Most of my rules started as one tap on an approval card. When a request reaches your phone, the card offers rules built from the call: Always allow npm test in api-server, Always ask before git push, Never allow force-pushes. From the terminal:
pilothouse policy add ask-push --action ask --command "git push" --command "git push *"
pilothouse policy add no-prod-db --action deny --command "psql *prod*" --message "Use the staging database."
pilothouse policy rules # everything, in the order it is checked
These "managed" rules are matched one command at a time, so allowing npm test never allows npm test && curl Γ’β¬Β¦ | sh.
Repo rules for your team
A repo can ship its own .pilothouse/policy.yaml. Its deny and ask rules apply to anyone who works in that repo. Its allow rules only count once you run pilothouse policy trust, because anyone can publish a repo with a permissive policy in it.
Lean on deny
deny rules are enforced on Claude Code, Codex, OpenCode and Grok Build alike, which is why the rule file above leans on deny for the things you never want. Keep ask for things a person should see.
Pilothouse also has a built-in rule you can't turn off: an agent can't run pilothouse approve, edit your policy, or change its own hook settings without a person confirming. Agents can't approve themselves.
Full reference: pilothouse.dev/docs and POLICY.md on GitHub.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.