Lifecycle Hooks
Lifecycle Hooks let you run local commands when AdaL reaches key points in an agent turn. Use them to enforce safety policies, inject project context, notify other tools, or run checks around tool use.
For example, you can block a dangerous shell command before it runs, add lint results after a file-editing tool completes, or record when an AdaL turn ends.
Hook commands run with your user account and inherited environment. Only add commands you trust, and do not load hook configuration from untrusted repositories.
Quick Start
Add a hooks object to ~/.adal/settings.json.
This example blocks every bash tool call:
{
"hooks": {
"PreToolUse": [
{
"matcher": "bash",
"hooks": [
{
"type": "command",
"command": "echo 'Shell commands are disabled by local policy' >&2; exit 2"
}
]
}
]
}
}
Restart AdaL after changing the file. When the agent tries to call bash, the hook exits with code 2, so AdaL skips the tool call and shows the hook's message.
Hook Events
AdaL supports these fixed lifecycle events:
| Event | When it runs | Can block? | Can add context? |
|---|---|---|---|
PreToolUse | Before a tool runs, before its permission prompt | Yes | No |
PostToolUse | After a tool succeeds | No | Yes |
PostToolUseFailure | After a tool returns an error | No | No |
UserPromptSubmit | Before AdaL processes your prompt | Yes | Yes |
PermissionRequest | When AdaL is waiting for approval to use a tool | No | No |
Stop | At the end of a turn, including interrupted turns | No | No |
Only PreToolUse and UserPromptSubmit can prevent an action. A block decision from any other event is ignored because the relevant action has either already occurred or is controlled by AdaL's permission system.
Configuration Reference
Each event maps to a list of matcher groups. A group contains an optional matcher and one or more command hooks.
{
"hooks": {
"EventName": [
{
"matcher": "ToolName|AnotherTool",
"hooks": [
{
"type": "command",
"command": "your-command-here",
"timeout": 60
}
]
}
]
}
}
| Field | Required | Description |
|---|---|---|
hooks | No | Top-level object containing lifecycle hook configuration. |
EventName | Yes | One of the six supported event names listed above. Event names are case-sensitive. |
matcher | No | Limits a group to matching tool names for tool-scoped events. Omit it or use "*" to match every tool. |
hooks | Yes | A non-empty list of command hook definitions. |
type | Yes | Must be "command". |
command | Yes | Command passed to your platform shell. |
timeout | No | Positive timeout in seconds. Defaults to 60. |
AdaL rejects an unknown event name or invalid hook shape during startup so a typo cannot silently disable a guardrail. Unknown optional fields are ignored with a warning for forward compatibility.
Match Tools with matcher
Matchers apply only to tool-scoped events:
PreToolUsePostToolUsePostToolUseFailurePermissionRequest
A matcher on UserPromptSubmit or Stop has no effect; those events do not have a tool name and therefore match every configured group.
| Matcher | Meaning |
|---|---|
Omitted, null, or "*" | Match every tool |
"bash" | Exact tool-name match, case-insensitive |
| `"bash | read_file"` |
"re:^mcp_" | Case-sensitive Python regular expression |
Plain and pipe-separated matchers are exact matches, not substrings. For example, "edit" does not match edit_file. Use the re: prefix when you intentionally need regular-expression matching.
Input Sent to Your Command
AdaL writes one JSON object to each hook command's standard input. Every event contains:
{
"session_id": "current-session-id",
"cwd": "/absolute/project/path",
"hook_event_name": "PreToolUse",
"transcript_path": "/path/to/conversation.jsonl"
}
transcript_path is included when a transcript is available.
Event-specific fields are:
| Event | Additional fields |
|---|---|
PreToolUse | tool_name, tool_input |
PostToolUse | tool_name, tool_input, tool_response |
PostToolUseFailure | tool_name, tool_input, error |
UserPromptSubmit | prompt |
PermissionRequest | tool_name, tool_input |
Stop | is_interrupt, optionally last_assistant_message |
Commands run with the agent working directory as their current directory.
Example: inspect a pending tool call
This PreToolUse hook calls a script that blocks a bash command containing rm -rf:
{
"hooks": {
"PreToolUse": [
{
"matcher": "bash",
"hooks": [
{
"type": "command",
"command": "./hooks/block-destructive-command.py"
}
]
}
]
}
}
./hooks/block-destructive-command.py:
#!/usr/bin/env python3
import json
import sys
hook = json.load(sys.stdin)
command = hook.get("tool_input", {}).get("command", "")
if "rm -rf" in command:
print("Destructive deletion is blocked by local policy", file=sys.stderr)
raise SystemExit(2)
Return Values
A command controls its result through its exit code or standard output.
| Result | AdaL behavior |
|---|---|
Exit 0 with ordinary stdout | AdaL treats stdout as additional context. |
Exit 0 with protocol JSON | AdaL reads decision, reason, and additionalContext. |
Exit 2 | AdaL blocks a blockable event. Standard error becomes the reason. |
| Any other exit code | AdaL logs a non-fatal error and continues. |
| Timeout or command-launch failure | AdaL logs a non-fatal error and continues. |
For advanced output, print JSON such as:
{
"decision": "block",
"reason": "This operation violates the repository policy",
"additionalContext": "Use the approved deployment workflow instead."
}
additionalContext from PostToolUse is appended to the tool result that AdaL sees. For UserPromptSubmit, it is prepended to the submitted prompt. This is useful for inserting project-specific instructions immediately before AdaL works.
Claude Code Compatibility
AdaL uses the same event names as the Claude Code hooks ecosystem and accepts the hookSpecificOutput envelope used by compatible command hooks. This lets a hook script share its output shape across supported tools while retaining AdaL's lifecycle behavior.
For a PreToolUse guardrail, a compatible script can return:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "This command is blocked by local policy"
}
}
When the command exits with 0, AdaL treats permissionDecision: "deny" as a block for PreToolUse. You can also return context through either additionalContext at the top level or inside hookSpecificOutput:
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Run the focused test suite after this edit."
}
}
Exit code 2 always takes precedence over JSON output. For a blockable event, it blocks the action and uses standard error as the reason.
Current Differences from Claude Code
AdaL supports a focused command-hook subset today:
- Only
type: "command"is supported.http,mcp_tool,prompt, andagenthook types are not supported. - Hooks are awaited. AdaL does not support
async,asyncRewake, or background hook execution. - AdaL does not support an
iffield. Put argument-level policy checks in your command by reading the JSON sent on standard input. - AdaL does not provide environment persistence files or terminal-sequence output fields.
- AdaL currently loads hooks from user settings at
~/.adal/settings.json; project and plugin hook scopes are not available.
Examples
Add context after a successful tool call
The following hook adds a reminder after successful edit_file calls:
{
"hooks": {
"PostToolUse": [
{
"matcher": "edit_file",
"hooks": [
{
"type": "command",
"command": "echo 'After editing, run the focused test suite before considering the task complete.'"
}
]
}
]
}
}
Add context to every prompt
This hook adds local project guidance before each submitted prompt:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "echo 'Project reminder: preserve public API compatibility unless the user explicitly requests a breaking change.'"
}
]
}
]
}
}
Notify when a turn ends
This Stop hook calls a script that receives whether the turn was interrupted:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./hooks/notify-turn-end.py"
}
]
}
]
}
}
./hooks/notify-turn-end.py:
#!/usr/bin/env python3
import json
import sys
hook = json.load(sys.stdin)
print("AdaL turn ended; interrupted={}".format(hook["is_interrupt"]))
Execution Behavior and Limitations
- Matching hooks for an event run in parallel.
- For blockable events, the first blocking result wins. If any hook blocks, additional context from the other hooks is discarded.
- Hooks cannot modify a tool's input before it runs.
- Script failures and timeouts are intentionally fail-open: they do not block the agent. Use a reliable command that exits with
2when you require enforcement. - A
PostToolUsehook runs after a streaming tool has completed and its final response is available. - AdaL uses
cmd.exe /con Windows and your configured shell (or/bin/sh) on Unix-like systems.
Hooks vs. Webhooks, Git Hooks, and React Hooks
Lifecycle Hooks are local AdaL commands that run during an agent turn. They are unrelated to:
- Webhooks, which send HTTP callbacks between services.
- Git hooks, which run on Git client events such as commits.
- React hooks, which manage state and effects in React components.