Parallel Works

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:

{
  "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. 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:

PriorityFileLayer
1 (lowest)~/.config/agents/settings.jsonUser, vendor-neutral
2~/.config/pw/code.jsonUser
3<workspace>/.mcp.jsonProject, the cross-tool MCP file
4<workspace>/.pw/code/settings.jsonProject, legacy
5<workspace>/.agents/settings.jsonProject
6<workspace>/.pw/code/settings.local.jsonLocal, legacy
7 (highest)<workspace>/.agents/settings.local.jsonLocal, 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.

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

typeRunsNotes
command (default)A shell commandPayload 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.
httpAn HTTP POSTPayload is the request body. headers values may interpolate allowlisted environment variables as $VAR or ${VAR}.
mcp_toolAn MCP toolTakes server and tool; string leaves in input may interpolate payload fields as ${payload.path}.
promptOne LLM completion$ARGUMENTS expands to the payload JSON. The hook replies {"ok": bool, "reason": "…"}. Optional model overrides the session model.
agentA bounded read-only mini-agentInspects 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 rather than trusted as-is.

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.

EventWhen it firesMatcherBlockableStatus
SessionStartA new session is startedsourceActive
SessionEndThe session endsreasonActive
Setuppw code runs an init/maintenance modetriggerDormant
UserPromptSubmitYou submit a promptYesActive
UserPromptExpansionA custom command expands into a promptcommand_nameYesActive
StopRight before the agent concludes its responseYesActive
StopFailureA turn ends in an errorerrorActive
PreToolUseBefore tool executiontool_nameYesActive
PostToolUseAfter tool executiontool_nameYesActive
PostToolUseFailureAfter a tool call failstool_nameActive
PostToolBatchAfter a parallel batch of tool calls completesYesActive
PermissionRequestBefore a permission dialog is showntool_nameYesActive
PermissionDeniedA permission request is deniedtool_nameActive
NotificationA notification is sentnotification_typeActive
MessageDisplayAn assistant message is displayedActive
SubagentStartA subagent startsagent_typeActive
SubagentStopRight before a subagent concludes its responseagent_typeYesActive
TeammateIdleAn agent teammate goes idleDormant
TaskCreatedA task is createdDormant
TaskCompletedA task is completedDormant
ConfigChangeSettings change mid-sessionsourceActive
CwdChangedThe working directory changesDormant
DirectoryAddedA directory is added to the workspace mid-sessionDormant
FileChangedpw code edits a workspace filefile_pathActive
InstructionsLoadedInstruction files are loadedfile_pathActive
WorktreeCreateA git worktree is createdDormant
WorktreeRemoveA git worktree is removedDormant
PreCompactBefore conversation compactiontriggerYesActive
PostCompactAfter conversation compactiontriggerActive
ElicitationAn MCP server requests user inputserverDormant
ElicitationResultAn MCP elicitation request resolvesserverDormant

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

EventWaiting on
SetupAn init/maintenance mode in pw code
TeammateIdleAgent teams
TaskCreated, TaskCompletedA task system
CwdChangedA movable working directory; today the workspace is fixed for the life of a run
DirectoryAddedAdding directories mid-session; today they are fixed at launch via --add-dir
WorktreeCreate, WorktreeRemoveWorktree isolation
Elicitation, ElicitationResultSurfacing 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.
  • Skills: Packaged instruction sets invoked by you or the agent
  • Settings: Settings files and workspace trust
  • Permissions: Permission modes and allow rules
  • MCP Servers: Connect MCP servers, including servers a mcp_tool hook can call
  • Non-Interactive Mode: One-shot runs, where guards fail closed