Skip to main content
A policy overrides only the hooks it needs. Each hook returns an answer, or None for no opinion. Pass policies to the workspace, or add and remove them later:
Hooks come in two groups. TypeScript spells them in camelCase (preCommand, ctx.sessionId).

Shell hooks

These fire when an agent runs a line: ws.shell, session.shell, the shell, ls and grep tools, mirage shell, HTTP /shell, RPC shell and SSH. Every command on a line passes pre_command before any of them runs, so a line refused whole never runs halfway.

pre_command

git push --force origin main gets git: Permission denied, exit 126.

pre_execute

python3 /jobs/train.py runs on the workspace’s docker runtime. See Route policy for runtimes.

post_execute

A longer output stops at 100 lines, and stderr says output truncated at limit (100 lines); ....

pre_session

export AWS_SECRET=x gets AWS_SECRET: permission denied, exit 1.

VFS hooks

These fire on every VFS call: session.vfs, the read, write, edit and glob tools, mirage vfs, HTTP /vfs/<call>, RPC vfs/<call>, FUSE, FSKit and SFTP. pre_vfs also fires for every file a shell command reads or writes. pre_vfs fires thousands of times under one grep -r, so keep it cheap. An Ask from it reaches a host only from session.vfs or a file tool; inside a line it refuses.

pre_vfs

Every command gets its own error, exit 1:
session.vfs.write("/reports/q3.md", ...) raises EACCES.

post_vfs

session.vfs.read returns at most the first 1 MB. post_vfs does not see the reads inside a command, so cat is not cut; bound a line with post_execute.

Answers

TypeScript returns { kind: 'deny', reason }, with scope: 'operand', path for an operand, { kind: 'ask', reason }, { kind: 'route', runtime } and new Limit({ maxLines }). A hook that raises refuses too. The reason never goes to stderr. It is on the result’s refusal:
The agent tools add one line to their text: policy denied: <reason>, or requires approval: <reason> (ask <id>).

When policies disagree

  • Any Deny beats any Ask, so answering an ask never reopens a refusal.
  • The first Deny speaks. Built-ins come first, then the session’s profile, then policies=, then add().
  • Limits merge to the tightest value of each field.
  • Two Routes to different runtimes refuse the line.

Which door enforces what

A line fires every hook; a VFS call fires only the VFS hooks. So a profile rule that names a command holds only on a line, and a rule that names only paths holds on both. session.tools.names() lists only the tools the session’s profile leaves it, and MCP, RPC and the agent adapters offer exactly those.

Examples

policy.py and policy.ts run a coded policy beside a profile’s policy: script. Their output is pinned in CI.