Skip to main content

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.

Security

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:

EventWhen it runsCan block?Can add context?
PreToolUseBefore a tool runs, before its permission promptYesNo
PostToolUseAfter a tool succeedsNoYes
PostToolUseFailureAfter a tool returns an errorNoNo
UserPromptSubmitBefore AdaL processes your promptYesYes
PermissionRequestWhen AdaL is waiting for approval to use a toolNoNo
StopAt the end of a turn, including interrupted turnsNoNo

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
}
]
}
]
}
}
FieldRequiredDescription
hooksNoTop-level object containing lifecycle hook configuration.
EventNameYesOne of the six supported event names listed above. Event names are case-sensitive.
matcherNoLimits a group to matching tool names for tool-scoped events. Omit it or use "*" to match every tool.
hooksYesA non-empty list of command hook definitions.
typeYesMust be "command".
commandYesCommand passed to your platform shell.
timeoutNoPositive 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:

  • PreToolUse
  • PostToolUse
  • PostToolUseFailure
  • PermissionRequest

A matcher on UserPromptSubmit or Stop has no effect; those events do not have a tool name and therefore match every configured group.

MatcherMeaning
Omitted, null, or "*"Match every tool
"bash"Exact tool-name match, case-insensitive
`"bashread_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:

EventAdditional fields
PreToolUsetool_name, tool_input
PostToolUsetool_name, tool_input, tool_response
PostToolUseFailuretool_name, tool_input, error
UserPromptSubmitprompt
PermissionRequesttool_name, tool_input
Stopis_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.

ResultAdaL behavior
Exit 0 with ordinary stdoutAdaL treats stdout as additional context.
Exit 0 with protocol JSONAdaL reads decision, reason, and additionalContext.
Exit 2AdaL blocks a blockable event. Standard error becomes the reason.
Any other exit codeAdaL logs a non-fatal error and continues.
Timeout or command-launch failureAdaL 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, and agent hook types are not supported.
  • Hooks are awaited. AdaL does not support async, asyncRewake, or background hook execution.
  • AdaL does not support an if field. 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 2 when you require enforcement.
  • A PostToolUse hook runs after a streaming tool has completed and its final response is available.
  • AdaL uses cmd.exe /c on 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.