# Guardrails for vibe coders: PreToolUse rules that stop rm -rf and secret leaks

> Source: <https://dev.to/pilothousedev/guardrails-for-vibe-coders-pretooluse-rules-that-stop-rm-rf-and-secret-leaks-191o>
> Published: 2026-11-03 21:43:23+00:00

*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).
