Hooks
Run commands at agent and plugin lifecycle events.
Hooks let a plugin run commands at specific agent or plugin lifecycle events.
Use hooks for imperative behavior that must happen automatically — for example, preparing an index when a session starts, validating a tool call before it runs, or cleaning up plugin state when the plugin is removed.
If instructions are enough, prefer a skill or rule. Hooks are most useful when the behavior should not depend on the model remembering to do it.
Hooks are Reforma-specific. Agent Plugins does not define a portable hook runtime.
Hooks can come from catalog plugins or from the project's .reforma configuration.
Events
| Event | When |
|---|---|
SessionStart | Before the first model turn of a session |
SessionEnd | When the agent run ends |
UserPromptSubmit | Before the submitted user prompt reaches the model |
PreToolUse | Before a tool executes |
PostToolUse | After a tool executes successfully |
PostToolUseFailure | After a tool fails or is denied |
Stop | When the agent loop is about to finish |
Install | After this plugin package is installed or hydrated |
Uninstall | Before this plugin package is removed |
Install and Uninstall run only for the plugin that is being installed or removed.
Stop hooks can prevent the loop from finishing and ask it to continue. Reforma limits hook-driven continuation to three times per run.
Layout
A catalog plugin can put its hook configuration and scripts under hooks/:
hooks/
hooks.json
session_start.sh
pre_bash.shWhen using the conventional hooks/hooks.json path, you do not need to declare a hooks path in the source plugin.json.
Example:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "sh ./hooks/pre_bash.sh",
"timeout": 10
}
]
}
]
}
}Invoke bundled shell scripts through sh rather than relying on executable file permissions being preserved.
Matchers
matcher filters tool lifecycle hooks by tool name:
{
"matcher": "Bash|Shell",
"hooks": [
{
"type": "command",
"command": "sh ./hooks/check-shell.sh"
}
]
}Matchers are useful with:
PreToolUse
PostToolUse
PostToolUseFailureThe matcher is interpreted as a regular expression.
For MCP tools, Reforma also resolves the corresponding MCP tool name when matching.
Omit matcher for non-tool lifecycle events.
Command environment
Hook commands run with Reforma directory variables:
| Variable | Value |
|---|---|
REFORMA_PROJECT_DIR | Project workspace root |
REFORMA_CONFIG_DIR | Project .reforma directory |
REFORMA_PLUGIN_DIR | Installed plugin package root; plugin hooks only |
These variables can also be referenced directly in the command:
{
"type": "command",
"command": "sh ${REFORMA_PLUGIN_DIR}/hooks/setup.sh"
}For plugin hooks, resolved values from extensions.reforma.variables are also added to the subprocess environment using their property names.
This includes configured secret variables. OAuth tokens are not exposed to hook subprocesses; they remain part of MCP authentication.
Project-level hooks do not receive plugin integration variables.
Input
Reforma writes one JSON object to the hook command's stdin.
Every event includes:
{
"hook_event_name": "PreToolUse",
"session_id": "...",
"cwd": "..."
}Tool events can additionally include fields such as:
{
"tool_name": "Bash",
"tool_input": {},
"tool_use_id": "..."
}Post-tool events can also include the tool response or failure information.
Output
A hook can write plain text to stdout. Reforma treats that text as additional context for the agent where the event supports it.
For more control, return a JSON object.
Hooks can use structured output to:
- allow or deny a tool call
- add agent context
- replace tool input
- replace tool output
- block continuation with a reason
For example, a PreToolUse hook can deny a tool call:
{
"permissionDecision": "deny",
"reason": "Do not delete production data."
}Exit code 2 also blocks the operation. If stderr contains text, Reforma uses it as the reason when no structured reason was returned.
Other hook failures are fail-open: Reforma logs the failure and continues.
The default hook timeout is 30 seconds. Set timeout on an individual command to override it.
Project hooks
Project-level hooks live at:
.reforma/hooks/hooks.jsonTheir working directory is .reforma.
They receive the project directory variables, but not plugin-specific integration variables or REFORMA_PLUGIN_DIR.
Packing
Catalog plugin authors should use hooks/hooks.json as the canonical source layout.
During packing, Reforma writes the canonical hooks path and generated hook metadata into the packed plugin manifest.
The packer also normalizes supported vendor plugin-root placeholders such as Claude, Cursor, and Codex plugin roots to the Reforma plugin location.
Treat those rewrites as import compatibility; new Reforma hooks should use the canonical layout directly.