Skip to content

Lifecycle hooks

A hook is a shell command that WAMP runs at a fixed point in a turn. It can add context for the model, refuse a prompt, refuse a tool call, or ask you to approve one. A hook can only restrict. It cannot allow anything WAMP would otherwise refuse, and it cannot rewrite a tool’s input or output.

The file format and the input fields are the ones Claude Code uses, so a script written for Claude Code can read its input unchanged. Tool names are WAMP’s (see Matchers).

File Runs
~/.wamp/hooks.json In every project
<project>/.wamp/hooks.json Only after you trust the project, together with yours

A chat with no repository runs in your home directory, so there the project file and your own file are the same file, and it is read once. Hooks are read once per send: an edit applies from the next message, and a running turn keeps the hooks it started with. Revoking trust in a project stops its hook commands at once, including any that are still running. A Cloud session trusts its own workspace, so a repository’s project hooks run there as they do in a trusted project.

This hook refuses any shell command that contains rm -rf:

~/.wamp/hooks.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "shell",
"hooks": [{ "type": "command", "command": "~/.wamp/bin/guard-shell.sh", "timeout": 10 }]
}
]
}
}
~/.wamp/bin/guard-shell.sh
#!/bin/sh
input=$(cat)
case "$input" in
*'rm -rf'*) echo "rm -rf is not allowed from chat" >&2; exit 2 ;;
esac
exit 0

Make the script executable with chmod +x ~/.wamp/bin/guard-shell.sh. Exit code 2 refuses the call, and the model receives stderr as the reason.

hooks maps an event name to a list of groups. Each group has an optional matcher and a list of handlers.

Field Meaning
matcher Tool events only. Selects the tools the group runs for (see below)
type Must be "command". Other handler types are skipped with a warning
command Run by the shell, with the project root as the working directory
timeout Seconds. Defaults to 60 and is capped at 600

An unknown event, field or handler type is reported as a warning in the chat. The rest of the file still applies.

An empty matcher or * matches every tool. A list such as shell|write matches those exact names. Anything else is an unanchored JavaScript regular expression.

Matchers use WAMP tool names, such as shell, write, edit, apply_patch and mcp__<server>__<tool>. Claude Code names such as Bash are not translated. A literal name that matches no registered tool produces a warning, so a copied "Bash" matcher does not fail silently.

Event Fires A hook can
SessionStart On the first message of a conversation, before UserPromptSubmit Add context
UserPromptSubmit On each message sent from a chat or through engine.run.start, before the turn is accepted Refuse the prompt, add context
PreToolUse Before the permission decision for each tool call Deny the call, ask for approval
PostToolUse After the call has run, whether it succeeded or failed Add feedback to the result
Stop At a native Run’s natural finish, after the Goal gate Keep the agent working with a reason

Goal continuations and scheduled turns are not prompts, so they fire neither SessionStart nor UserPromptSubmit. Tool events still fire during those turns; native root Runs still fire Stop when they naturally finish. A blocking Stop reason becomes the next user message. The agent can be kept working twice; if the hook still blocks on the final check, the Run finishes with that reason unresolved. An abort, error or user Stop does not fire the hook. With a foreign agent runtime, such as an ACP agent, the prompt events fire, but tool events fire only for WAMP tools lent to that agent. The agent’s own tools run inside its process, where WAMP does not see them. ACP turns do not fire Stop.

Each handler receives one JSON object on stdin:

Field Present
hook_event_name, session_id (the conversation id), cwd (the project root), run_id, agent, runtime Always
tool_name, tool_input, tool_use_id Tool events
tool_response ({ content, is_error }) PostToolUse
prompt UserPromptSubmit
source ("startup") SessionStart
stop_hook_active (true after a Stop block in this Run) Stop

There is no transcript_path, because WAMP keeps no transcript file. The environment adds WAMP_PROJECT_DIR and removes WAMP_CORE_URL and WAMP_CORE_SECRET_FILE, so a hook does not inherit the engine’s control connection.

Result Effect
Exit 0, no output Nothing
Exit 2 Stderr is the reason. UserPromptSubmit refuses the prompt, PreToolUse denies the call, PostToolUse adds the reason as feedback, Stop keeps the agent working, and SessionStart ignores it
Any other exit code, or a timeout A warning in the chat. The turn continues
Plain text on stdout Context for SessionStart and UserPromptSubmit. A warning for the other events
JSON on stdout Read as shown below

JSON fields:

Field Effect
hookSpecificOutput.permissionDecision: "deny" or "ask", with permissionDecisionReason PreToolUse refuses the call or asks you about it
decision: "block" with reason UserPromptSubmit refuses the prompt. PostToolUse adds the reason as feedback. Stop keeps the agent working
hookSpecificOutput.additionalContext Context for SessionStart and UserPromptSubmit, or feedback for PostToolUse
continue: false with stopReason Same as exit 2, with stopReason as the reason
updatedInput or updatedOutput Same as exit 2, because WAMP does not apply rewrites
permissionDecision: "allow", suppressOutput Ignored, with a warning

Several matching handlers run in parallel. If any of them denies, the call is denied, and a deny outranks an ask. Context from all handlers is joined and capped at 10,000 characters. Each handler’s stdout and stderr are each read up to 64 KiB.

A deny refuses the call in every mode, including Full access, even if you allowed that tool earlier in the chat. An ask shows one approval card with the hook’s reason. The card offers only Allow and Deny, never “allow for this chat”. When nobody can answer, as in an unattended run, an ask becomes a refusal.

A refused prompt never enters the chat, and its reason appears as a warning. Through engine.run.start, the call fails with a failed-precondition error whose data carries the reason.