Guardrails for vibe coders: PreToolUse rules that stop rm -rf and secret leaks A developer built Pilothouse, a rules engine that hooks into coding agents' PreToolUse points to allow, ask, or deny tool calls across Claude Code, Codex, OpenCode, and Grok Build. The tool ships presets like guarded-autopilot, positioned as a safer alternative to skipping permissions, and no-secrets, which blocks access to .env files, private keys, and credential dumps such as printenv and gh auth token while permitting .env.example. A hand-written policy.yaml example shows rules denying broad rm -rf deletes, force-pushes, and production database access, and asking before pushes to main or piping downloads into a shell. Originally published at pilothouse.dev/blog/guardrails-for-vibe-coders https://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. 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 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. Here's a hand-written ~/.pilothouse/policy.yaml that covers the same ground, so you can see how the rules work and adjust them: js 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 paths , hour , weekday and more available. has word matches whole words, so --force-with-lease doesn't trip --force . .env matches at any depth. Start it with / or ~/ to anchor it. deny wins ask , then allow . Run any rule against a command without an agent involved: bash $ 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 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 . 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. 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 https://pilothouse.dev/docs and POLICY.md on GitHub https://github.com/sabr-software/pilothouse/blob/main/docs/POLICY.md .