# Hooks

> Source: https://parallelworks.com/docs/ai/code/hooks

# Hooks

Hooks are commands `pw code` runs automatically on lifecycle events: before a tool call, when you submit a prompt, when a session ends, and around thirty other moments. A hook can observe what the agent is doing, block an action, or rewrite it. Use them to audit shell commands, enforce a policy the agent cannot talk its way past, format files after every edit, or send a notification when a long run finishes.

A hook handler can be a shell command, an HTTP endpoint, an MCP tool, or an LLM prompt.

## Configuring Hooks

Hooks live under a top-level `hooks` key in a settings file, keyed by event name. Each event holds a list of **matcher groups**, and each group holds a list of **hook definitions**:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.config/agents/hooks/bash-audit.sh", "timeout": 5 }
        ]
      }
    ],
    "Stop": [
      { "hooks": [{ "type": "prompt", "prompt": "Are the tests green? $ARGUMENTS" }] }
    ]
  }
}
```

`matcher` selects which occurrences the group runs for, testing the payload field named in the *Matcher* column of the [event table](#supported-events). Omit it, or use `"*"`, to match every occurrence. Matchers are compared **exactly**, not as substrings, so `code-reviewer` does not match `my-code-reviewer-2`.

Edits take effect on the next run, or immediately with `/hooks reload`.

### Where Hooks Are Configured

Hooks are read from these files, from lowest to highest priority:

| Priority | File | Layer |
| --- | --- | --- |
| 1 (lowest) | `~/.config/agents/settings.json` | User, vendor-neutral |
| 2 | `~/.config/pw/code.json` | User |
| 3 | `<workspace>/.mcp.json` | Project, the cross-tool MCP file |
| 4 | `<workspace>/.pw/code/settings.json` | Project, legacy |
| 5 | `<workspace>/.agents/settings.json` | Project |
| 6 | `<workspace>/.pw/code/settings.local.json` | Local, legacy |
| 7 (highest) | `<workspace>/.agents/settings.local.json` | Local, git-ignored |

The pw-specific user file is `~/.config/pw/code.json`, seeded on first access from the vendor-neutral `~/.config/agents/settings.json`. Most settings are read only from `code.json` after that seed, but hooks, `disableAllHooks`, and `allowedEnvVars` are always read live from the vendor-neutral file too, so a hooks block kept there keeps working. The legacy `<workspace>/.pw/code/` files are read only when the `.agents/` equivalents are absent.

:::warning Hooks are additive, not overriding
Every layer's hooks for an event run. A higher-priority layer does **not** replace a lower one — priority only decides listing order and which value wins for the scalar keys below. (This differs from `mcpServers`, where the highest-priority file wins outright. See [MCP Servers](/docs/ai/code/mcp).) There is no per-hook opt-out: a higher layer cannot disable one hook from a lower layer. Your only levers are user-level `disableAllHooks` and leaving a project's hooks untrusted.
:::

Two scalar keys are honored from the **user layer only**. A project or local file that sets them is warned about and ignored, so a cloned repository cannot weaken your guards or exfiltrate secrets:

- `disableAllHooks: true` turns every hook off.
- `allowedEnvVars: ["MY_TOKEN", …]` names the environment variables an HTTP hook may interpolate into its headers. Referencing any other variable is a loud error, never a silent empty value.

### Project Hooks Must Be Trusted

Project and local settings ship with a repository, so their hooks are arbitrary code written by whoever authored the clone. They stay **inert** until you trust the exact configuration:

1. Run `/hooks` and review the pending hooks, marked *untrusted*.
2. Run `/hooks trust` to enable them for this workspace.

Trust is bound to the exact bytes, so any later edit re-gates the hooks — including edits you did not make. A teammate's commit or an automated formatter touching a settings file silently re-gates them until you review and trust again. User-level hooks are always active and never need trusting.

## Handler Types

| `type` | Runs | Notes |
| --- | --- | --- |
| `command` (default) | A shell command | Payload arrives on stdin as JSON. Exit code 2 blocks. Runs with `cwd` set to the workspace and `PW_PROJECT_DIR` / `CLAUDE_PROJECT_DIR` set. `"async": true` fires and forgets. Supplying `args` uses exec form, with no shell. |
| `http` | An HTTP POST | Payload is the request body. `headers` values may interpolate allowlisted environment variables as `$VAR` or `${VAR}`. |
| `mcp_tool` | An MCP tool | Takes `server` and `tool`; string leaves in `input` may interpolate payload fields as `${payload.path}`. |
| `prompt` | One LLM completion | `$ARGUMENTS` expands to the payload JSON. The hook replies `{"ok": bool, "reason": "…"}`. Optional `model` overrides the session model. |
| `agent` | A bounded read-only mini-agent | Inspects the workspace, then returns the same `{"ok", "reason"}` verdict. |

Every payload carries a common envelope — `session_id`, `transcript_path`, `cwd`, `hook_event_name`, `permission_mode` — plus the event-specific fields. All field names are snake_case.

### Blocking and Rewriting

Events marked blockable in the table below can stop the action: a `command` hook exits 2, or any hook returns `{"decision": "block", "reason": "…"}`. Hooks can also inject `additionalContext`, or replace the tool input or prompt through `updatedInput` — a rewritten tool call is re-checked by the [permission engine](/docs/ai/code/permissions) rather than trusted as-is.

:::note Guards fail closed
For `PreToolUse` and `PermissionRequest`, a `prompt` or `agent` guard that times out or returns garbage is treated as a **deny**. A guard you can bypass by breaking it is not a guard. Every other event fails open, so a broken notification hook never wedges your session.
:::

### Timeouts

The default timeout is **600 seconds**. Shorter defaults apply to `SessionEnd` (30s), `UserPromptSubmit` (30s), and `MessageDisplay` (10s), and LLM-backed handlers are capped further: `prompt` hooks at 30s and `agent` hooks at 60s, or the event default if that is shorter. Set `"timeout"` in seconds on any hook to override. On timeout, the hook's entire process group is killed.

## Supported Events

Every event below is valid in configuration and appears in the `/hooks` browser. **Dormant** events parse and list but never fire yet, because the feature they depend on does not exist in `pw code` — they light up automatically when it lands. Configuring one warns at startup, so a guard that can never fire is never mistaken for an armed one.

| Event | When it fires | Matcher | Blockable | Status |
| --- | --- | --- | --- | --- |
| `SessionStart` | A new session is started | `source` | | Active |
| `SessionEnd` | The session ends | `reason` | | Active |
| `Setup` | `pw code` runs an init/maintenance mode | `trigger` | | Dormant |
| `UserPromptSubmit` | You submit a prompt | | Yes | Active |
| `UserPromptExpansion` | A custom command expands into a prompt | `command_name` | Yes | Active |
| `Stop` | Right before the agent concludes its response | | Yes | Active |
| `StopFailure` | A turn ends in an error | `error` | | Active |
| `PreToolUse` | Before tool execution | `tool_name` | Yes | Active |
| `PostToolUse` | After tool execution | `tool_name` | Yes | Active |
| `PostToolUseFailure` | After a tool call fails | `tool_name` | | Active |
| `PostToolBatch` | After a parallel batch of tool calls completes | | Yes | Active |
| `PermissionRequest` | Before a permission dialog is shown | `tool_name` | Yes | Active |
| `PermissionDenied` | A permission request is denied | `tool_name` | | Active |
| `Notification` | A notification is sent | `notification_type` | | Active |
| `MessageDisplay` | An assistant message is displayed | | | Active |
| `SubagentStart` | A subagent starts | `agent_type` | | Active |
| `SubagentStop` | Right before a subagent concludes its response | `agent_type` | Yes | Active |
| `TeammateIdle` | An agent teammate goes idle | | | Dormant |
| `TaskCreated` | A task is created | | | Dormant |
| `TaskCompleted` | A task is completed | | | Dormant |
| `ConfigChange` | Settings change mid-session | `source` | | Active |
| `CwdChanged` | The working directory changes | | | Dormant |
| `DirectoryAdded` | A directory is added to the workspace mid-session | | | Dormant |
| `FileChanged` | `pw code` edits a workspace file | `file_path` | | Active |
| `InstructionsLoaded` | Instruction files are loaded | `file_path` | | Active |
| `WorktreeCreate` | A git worktree is created | | | Dormant |
| `WorktreeRemove` | A git worktree is removed | | | Dormant |
| `PreCompact` | Before conversation compaction | `trigger` | Yes | Active |
| `PostCompact` | After conversation compaction | `trigger` | | Active |
| `Elicitation` | An MCP server requests user input | `server` | | Dormant |
| `ElicitationResult` | An MCP elicitation request resolves | `server` | | Dormant |

Ten events are dormant, and `/hooks` explains why for each:

| Event | Waiting on |
| --- | --- |
| `Setup` | An init/maintenance mode in `pw code` |
| `TeammateIdle` | Agent teams |
| `TaskCreated`, `TaskCompleted` | A task system |
| `CwdChanged` | A movable working directory; today the workspace is fixed for the life of a run |
| `DirectoryAdded` | Adding directories mid-session; today they are fixed at launch via `--add-dir` |
| `WorktreeCreate`, `WorktreeRemove` | Worktree isolation |
| `Elicitation`, `ElicitationResult` | Surfacing MCP elicitation requests |

## Reviewing Hooks

- **`/hooks`** opens an interactive browser: an arrow-navigable event list, then matcher groups, then hooks, then hook detail. It shows each hook's type, timeout, and source file, tags dormant events, marks untrusted project hooks, and lists recent hook activity. Non-interactive `-p` runs print a text listing instead.
- **`/hooks trust`** enables the project hooks you just reviewed for this workspace.
- **`/hooks reload`** re-reads every settings file mid-session. Project hooks still need trust.

## Behavior Notes and Limits

- `FileChanged` fires only for `pw code`'s own `WriteFile` and `EditFile` edits. There is no filesystem watcher, so edits made by a shell command the agent ran do not trigger it.
- `ConfigChange` reports in-app changes with the sources `model`, `permission_mode`, and `allowlist`. Settings files are not watched — run `/hooks reload` after editing one.
- `tool_response` in a `PostToolUse` payload is the tool's text output, truncated to 50 KB.
- The per-hook fields `if`, `statusMessage`, `once`, `shell`, and `asyncRewake` are accepted but not implemented. Each is warned about and ignored, so an `if`-narrowed hook runs on every matched occurrence and an `asyncRewake` hook never wakes the agent.
- `permissionDecision: "defer"` is treated as `"ask"` with a notice: interactive runs prompt, and one-shot `-p` runs fail closed and deny the tool call.
- `suppressOutput` is accepted but has no effect.
- An `http` hook refuses cross-origin redirects, since following one would forward your custom secret headers to a host you did not configure.

## Related Documentation

- [Skills](/docs/ai/code/skills): Packaged instruction sets invoked by you or the agent
- [Settings](/docs/ai/code/settings): Settings files and workspace trust
- [Permissions](/docs/ai/code/permissions): Permission modes and allow rules
- [MCP Servers](/docs/ai/code/mcp): Connect MCP servers, including servers a `mcp_tool` hook can call
- [Non-Interactive Mode](/docs/ai/code/non-interactive): One-shot runs, where guards fail closed
