Hooks
React to agent lifecycle events with shell commands, prompts, and agent invocations
Hooks are callback functions that execute in response to agent lifecycle events — before a tool runs, when a session starts, after a command executes, and more. They let you inject custom logic into the agent pipeline without modifying core code: audit logging, security gates, notification dispatching, analytics, and dynamic prompt injection.
How Hooks Work
Aleph's hook system sits between the agent loop and tool execution, forming an event-driven middleware pipeline. When a lifecycle event fires, the HookExecutor finds all registered hooks for that event, checks their matcher patterns, and executes their actions according to their kind (interceptor, observer, or resolver).
User Input ──▶ Agent Loop ──▶ [PreToolUse hooks] ──▶ Tool Execution
│
[PostToolUse hooks] ◀──┘
│
Response ──▶ [SessionEnd hooks]Hook Events
Every hook is bound to a specific event. The single source of truth for the exhaustive list is the HookEvent enum in src/extension/types/hooks.rs. Serialization uses snake_case (before_tool_call); PascalCase aliases are kept for backward compatibility with legacy hooks.json (PreToolUse ↔ BeforeToolCall). The hooks.events RPC and the hooks_manage events command both project HookEvent::ALL and do not maintain their own hand-written lists.
Tool Lifecycle
| Event | When It Fires | Can Block? |
|---|---|---|
BeforeToolCall | Before a tool is executed | Yes |
AfterToolCall | After a tool completes successfully | Yes (can rewrite the output) |
AfterToolCallFailure | After a tool fails | No |
ToolResultPersist | When a tool result is being persisted | No |
Session and Agent
| Event | When It Fires | Can Block? |
|---|---|---|
BeforeAgentStart | Before the agent begins processing | Yes |
AgentEnd | When the agent completes | Yes (can rewrite the final output) |
SessionStart | When a new session begins | Yes |
SessionEnd | When a session ends | No |
BeforeCompaction | Before session compaction | Yes |
AfterCompaction | After session compaction | No |
UserPromptSubmit | After the user submits a prompt, before the first think call | Yes |
Stop | When the model produces a turn with no tool calls | Yes (interceptor may veto) |
Subagent
| Event | When It Fires | Can Block? |
|---|---|---|
SubagentStart | When a sub-agent is spawned | No (observer-only) |
SubagentStop | When a sub-agent completes | No |
Message Channels
| Event | When It Fires | Can Block? |
|---|---|---|
MessageReceived | When a channel message is received | Yes |
MessageSending | Before a message is sent to a channel | Yes |
MessageSent | After a message has been sent | No |
Provider and Gateway
| Event | When It Fires | Can Block? |
|---|---|---|
PreApiRequest | Before an LLM provider request is issued | No |
PostApiRequest | After an LLM provider response is received | No |
GatewayStart | When the gateway starts | No |
GatewayStop | When the gateway stops | No |
Notification and Permission
| Event | When It Fires | Can Block? |
|---|---|---|
Notification | When a notification is sent | No |
PermissionRequest | When a tool requests elevated permission | Yes |
Hook Kinds
Not all hooks behave the same way. Aleph classifies hooks into three kinds, each with different execution semantics:
Interceptor
Pipeline execution. Interceptors run sequentially by priority and can modify the request context or block execution entirely. They short-circuit on the first block.
Use interceptors for:
- Security gates (block dangerous tool calls)
- Input validation (reject malformed arguments)
- Request modification (rewrite tool parameters)
# Block all shell commands that contain 'rm -rf'
[[hooks]]
event = "before_tool_call" # PascalCase aliases PreToolUse / BeforeToolCall also accepted
kind = "interceptor"
priority = "system"
matcher = "shell:exec"
actions = [{ type = "command", command = "echo $ARGUMENTS | grep -q 'rm -rf' && echo 'block: Dangerous command blocked' || echo 'pass'" }]When an interceptor command outputs a line starting with block:, execution is halted and the rest of the line becomes the block reason shown to the user.
Observer
Fire-and-forget. Observers run in parallel, receive read-only context, and cannot block or modify anything. Errors are logged but never propagated.
Use observers for:
- Audit logging
- Analytics and metrics
- External notifications
- Telemetry
[[hooks]]
event = "PostToolUse"
kind = "observer"
actions = [{ type = "command", command = "echo \"$(date): Tool $TOOL_NAME completed\" >> /var/log/aleph/tool_audit.log" }]Resolver
First-win competition. Resolvers run sequentially by priority and stop as soon as one returns a value. Used for dynamic resolution where multiple sources might provide an answer.
Use resolvers for:
- Dynamic configuration lookup
- Credential resolution
- Route selection
Hook Priority
Hooks execute in priority order within their kind. Lower numeric values run first:
| Priority | Value | Use Case |
|---|---|---|
system | -1000 | Security, audit — always runs first |
high | -100 | Important business logic |
normal | 0 | Default priority |
low | 100 | Non-critical extensions |
For interceptors, priority determines who gets to block first. For resolvers, it determines who gets to resolve first. For observers, priority has no practical effect since they run in parallel.
Hook Actions
Each hook can execute one or more actions when triggered. Three action types are available:
Command
Execute a shell command. The command runs in a subprocess with environment variables populated from the hook context:
actions = [{ type = "command", command = "${PLUGIN_ROOT}/scripts/validate.sh" }]Available environment variables:
| Variable | Description |
|---|---|
$TOOL_NAME | Name of the tool being invoked |
$ARGUMENTS | Tool arguments as JSON string |
$TOOL_INPUT | Tool input content |
$FILE | File path (if applicable) |
$SESSION_ID | Current session ID |
$PLUGIN_ROOT | Root directory of the plugin |
Both $VAR and ${VAR} syntax are supported. Commands have a default timeout of 300 seconds.
Prompt
Inject a prompt string into the conversation context. The prompt is passed to the LLM for evaluation, allowing hooks to influence the agent's reasoning:
actions = [{ type = "prompt", prompt = "Before executing ${TOOL_NAME}, verify the arguments are safe: ${ARGUMENTS}" }]Agent
Invoke a named agent to handle the event. The agent runs as a sub-agent with its own session:
actions = [{ type = "agent", agent = "review-agent" }]Matcher Patterns
Tool-related hooks can use matcher to restrict which tools they apply to. The matcher is a regex pattern tested against the tool name:
# Match only the Write tool
matcher = "Write"
# Match Write or Edit
matcher = "Write|Edit"
# Match any shell tool
matcher = "shell:.*"
# Match all tools (default when no matcher is specified)
# matcher not set — hook fires for every toolIf no matcher is set, the hook fires for all tool invocations of that event type.
Registering Hooks
In Plugin Configuration
Plugins define hooks in their aleph-plugin.json manifest:
{
"name": "security-gate",
"hooks": [
{
"event": "PreToolUse",
"kind": "interceptor",
"priority": "system",
"matcher": "shell:exec|Write|Edit",
"actions": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/check-safety.sh"
}
]
},
{
"event": "PostToolUse",
"kind": "observer",
"actions": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/log-action.sh"
}
]
}
]
}In TOML Configuration
Hooks can also be defined directly in config.toml:
[[hooks]]
event = "SessionStart"
kind = "observer"
plugin_name = "session-logger"
actions = [
{ type = "command", command = "echo 'Session started: $SESSION_ID' >> ~/aleph-sessions.log" }
]
[[hooks]]
event = "PreToolUse"
kind = "interceptor"
priority = "high"
matcher = "shell:exec"
plugin_name = "command-guard"
actions = [
{ type = "command", command = "~/scripts/check-command.sh" }
]Hook Execution Context
Every hook receives a HookContext with information about the triggering event:
| Field | Type | Description |
|---|---|---|
session_id | string | The active session ID |
tool_name | string? | Tool name (for tool events) |
arguments | string? | Tool arguments as JSON |
tool_input | string? | Tool input content |
file_path | string? | Relevant file path |
working_dir | string? | Working directory for commands |
env | map | Additional environment variables |
For command actions, all context fields are injected as environment variables. For prompt actions, they are available as ${VAR} template substitutions.
Interceptor Results
Interceptor hooks return structured results that control the pipeline:
| Outcome | What Happens |
|---|---|
| Pass | Execution continues to the next interceptor or the tool itself |
| Block | Execution halts. The block reason is reported to the user. |
| Block (silent) | Execution halts without showing a message to the user |
| Modified | Execution continues with a modified context (rewritten arguments, etc.) |
When an interceptor hook action fails (command exits non-zero, timeout, etc.), the default behavior is to block execution for safety. A broken security hook should fail closed, not open.
Practical Examples
Audit Trail
Log every tool invocation to a file for compliance:
{
"event": "PostToolUse",
"kind": "observer",
"actions": [{
"type": "command",
"command": "jq -n --arg tool \"$TOOL_NAME\" --arg args \"$ARGUMENTS\" --arg session \"$SESSION_ID\" --arg ts \"$(date -u +%FT%TZ)\" '{timestamp: $ts, tool: $tool, arguments: $args, session: $session}' >> /var/log/aleph/audit.jsonl"
}]
}Slack Notification on Session End
Notify a Slack channel when a long-running session finishes:
{
"event": "SessionEnd",
"kind": "observer",
"actions": [{
"type": "command",
"command": "curl -s -X POST \"$SLACK_WEBHOOK\" -H 'Content-Type: application/json' -d '{\"text\": \"Aleph session '$SESSION_ID' completed.\"}'"
}]
}File Write Protection
Prevent the agent from writing to sensitive directories:
{
"event": "PreToolUse",
"kind": "interceptor",
"priority": "system",
"matcher": "Write|Edit",
"actions": [{
"type": "command",
"command": "echo $ARGUMENTS | jq -r '.file_path // .path // \"\"' | grep -qE '^/(etc|usr|System)' && echo 'block: Writing to system directories is not allowed' || echo 'pass'"
}]
}Dynamic Prompt Injection
Add context-aware instructions before tool execution:
{
"event": "PreToolUse",
"kind": "observer",
"matcher": "shell:exec",
"actions": [{
"type": "prompt",
"prompt": "Safety reminder: You are about to execute a shell command. Verify that the command does not modify or delete files outside the project directory."
}]
}See Also
- Events — Subscribe to system events programmatically
- Cron Jobs — Schedule recurring tasks
- Extensions — Plugin system that registers hooks
- Security — Security model and permission system
26.7.x Addendum
Cron / Heartbeat Realized (stubs → real)
26.7.x: the historical stub RPCs (cron.*_stub / heartbeat.*_stub) have been realized.
hooks.* Full RPC
hooks.list/hooks.registry/hooks.add/hooks.remove/hooks.reload/hooks.events
Hooks: Bounded Injection, Consent, Inventory
26.7.x Hooks hardening:
context_injection_limit_bytes— bound injected context (avoid unbounded growth)require_consent— consent gate- Runtime inventory — list registered hooks
Guardian Judge
26.7.21+: the LLM judge for heartbeat probes — payload masks secrets.
Heartbeat Refuses Dangerous Tools
26.7.21+: heartbeat probes refuse to use dangerous tools or confirmation-gated tools.
Day-Boundary Log Rotation
26.7.24+: the daemon supports day-boundary log rotation; ALEPH-BOOT marker.
Full RPC
See Gateway Methods Reference for cron / heartbeat / hooks.