# Lifecycle hooks Run your own commands when a chat starts, a prompt is sent, a tool is called, or a native Run finishes. Source: https://docs.vampikez.fun/build/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](#matchers)). ## Where hooks live | File | Runs | |---|---| | `~/.wamp/hooks.json` | In every 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. ## A first hook This hook refuses any `shell` command that contains `rm -rf`: ```json title="~/.wamp/hooks.json" { "hooks": { "PreToolUse": [ { "matcher": "shell", "hooks": [{ "type": "command", "command": "~/.wamp/bin/guard-shell.sh", "timeout": 10 }] } ] } } ``` ```bash title="~/.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. ## File format `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. ### Matchers 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____`. 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. ## Events | 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`. ## Input 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. ## Output | 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. ## Denials and approvals 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`.