Guides

Policies

Write Pilothouse rules in YAML with glob matches and optional CEL conditions. Deny force-push, ask before pushing to main, keep .env files out of the context — for Claude Code and Codex.

A policy is a list of rules that Pilothouse checks before every tool call an agent makes. Each rule can allow, ask or deny. Rules live in plain YAML files you can review, version and share.

Where rules come from

File Scope Notes
~/.pilothouse/policy.yaml You, every repo Created by pilothouse policy init
<repo>/.pilothouse/policy.yaml One repository Only deny and ask apply until you trust the repo
Built-in Always on pilothouse-self-protection; can’t be disabled

All applicable rules are evaluated together. Any deny wins, then ask, then allow. If no rule matches, Pilothouse has no opinion and the agent’s own permission settings decide.

A first policy

version: 1
rules:
  - name: no-force-push
    description: Never force-push
    match: { kind: shell, command: "git push*" }
    when: 'command.matches("(--force|-f)\\b")'
    action: deny
    message: Force-push is blocked by policy. Open a PR instead.

  - name: ask-before-push-main
    match: { kind: shell, command: "git push*main*" }
    action: ask

  - name: no-env-files
    match: { path: "**/.env*" }
    action: deny
    message: .env files stay out of the agent's context.

  - name: tests-are-fine
    match: { kind: shell, command: "{npm,pnpm} test*" }
    action: allow

Run pilothouse policy init for a starter file with more commented examples, and pilothouse policy check to validate a file.

Rule fields

Field Required Meaning
name yes Shown in the inbox, on your phone, in explain and in the log
description no For humans
match one of match / when Glob patterns; all listed fields must match
when one of match / when A CEL expression; AND-ed with match
action yes allow, ask or deny
message no Sent to the agent when the rule denies
agents no Limit the rule to some agents, e.g. [claude, codex]

match

Fields: agent, tool, kind, command, path, cwd, repo, branch. Values are globs: *, ** (any number of directories), ?, and {a,b} alternatives. path matches if any path the tool touches matches.

match: { tool: Edit, path: "migrations/**" }
match: { kind: shell, command: "terraform apply*" }
match: { branch: "{main,release/*}", kind: shell, command: "git commit*" }

when (CEL)

When a glob isn’t enough, add a CEL condition. Available variables:

Variable Type Example
agent string "claude"
tool string "Bash", "Edit", "Read"
kind string "shell"
command string "git push origin main"
paths list of strings every path the call touches
path string the first path, or ""
input map the raw tool input
cwd, repo, branch string where the agent is working
mode string the agent’s permission mode
hour, weekday int local time

Besides CEL’s standard library and string extensions, two helpers are available: glob(s, pattern) and has_word(s, word).

# Ask before any deploy outside working hours
- name: after-hours-deploys
  match: { kind: shell }
  when: 'has_word(command, "deploy") && (hour < 9 || hour >= 18 || weekday == 0 || weekday == 6)'
  action: ask

# Block curl | sh
- name: no-pipe-to-shell
  match: { kind: shell }
  when: 'command.matches("curl[^|]*\\|\\s*(ba|z)?sh")'
  action: deny
  message: Download the script and review it first.

Repo rules and trust

A repository can ship rules in .pilothouse/policy.yaml. Because anyone can put a file in a repo you clone, untrusted repos can only make things stricter: their deny and ask rules apply, their allow rules are ignored (and pilothouse policy status lists them as ignored).

pilothouse policy trust            # trust the repo you're in
pilothouse policy trust ~/src/api  # or a path
pilothouse policy untrust

Trusted repos are listed in ~/.pilothouse/trusted-repos.yaml.

Built-in protection

The built-in rule pilothouse-self-protection always forces ask when an agent:

  • runs Pilothouse’s own admin commands from a shell (approving, pairing, changing presence, reloading policy…),
  • edits protected paths: the Pilothouse data directory and the agents’ own settings files.

It can’t be turned off. Together with the process checks on the socket it keeps an agent from quietly approving itself.

Test before you rely on it

Check a hypothetical tool call:

pilothouse policy test --tool Bash --command "git push --force origin feat"
pilothouse policy test --tool Read --path .env.production
pilothouse policy test --agent codex --tool Bash --command "npm test" --cwd ~/src/api

Replay new rules against the tool calls your agents actually made:

pilothouse policy replay --since 7d

replay re-evaluates logged PreToolUse events and shows which outcomes would change — the easiest way to tighten a rule without surprises.

Reloading and errors

After editing a file, run pilothouse policy status to see which files are loaded, from where, and any errors. If a file has an error, Pilothouse keeps using the last good version of that file and reports the problem. It never crashes on a bad rule.

Recipes

# Never touch production credentials
- name: no-prod-creds
  match: { path: "**/{prod,production}*.{env,json,yaml,yml}" }
  action: deny

# Ask before recursive force deletes
- name: ask-rm-rf
  match: { kind: shell, command: "rm *" }
  when: 'command.matches("\\s-[a-zA-Z]*(rf|fr)")'
  action: ask

# Codex only: no network installs
- name: codex-no-installs
  agents: [codex]
  match: { kind: shell, command: "{npm,pnpm,yarn,pip,brew} {install,add}*" }
  action: ask

Found a mistake? Open an issue.