Working with Autohand
Hooks Reference
Hooks let you run custom scripts in response to agent events. Use them to enforce coding standards, run tests automatically, notify your team, or integrate with external tools.
Overview
Hooks are shell commands that Autohand runs at specific points in the agent lifecycle: before and after tool calls, when files change, when a turn or session ends, when permission is requested or refused, and across auto-mode, sub-agent, team, review, and context events. They let you automate around the agent without changing its behavior.
Common use cases:
- Run linters and formatters after file changes
- Execute related tests after code modifications
- Send notifications when a turn or session completes
- Block dangerous commands or auto-approve safe ones
- Log tool executions for audit trails and telemetry
- Trigger CI/CD pipelines and integrate with external services
Hooks can be defined in your config file, registered by enabled and trusted extensions through api.hooks.on(event, handler), and, when running in JSON-RPC mode for IDE integrations, emitted as JSON-RPC 2.0 notifications.
Manage hooks: Use /hooks during a session to browse every lifecycle event and create a hook in plain English, /hooks list to print the full event table, and /hooks manage to toggle, test, remove, or manually add config hooks.
Configuration
Hooks live under the hooks key of your Autohand config file (~/.autohand/config.json by default, or the file passed with --config or AUTOHAND_CONFIG). The hooks object holds a global enabled switch and an array of hook definitions.
Basic structure
{
"hooks": {
"enabled": true,
"hooks": [
{
"event": "pre-tool",
"command": "echo \"Running tool: $HOOK_TOOL\" >> ~/.autohand/hooks.log",
"description": "Log all tool executions",
"enabled": true
},
{
"event": "file-modified",
"command": "./scripts/on-file-change.sh",
"description": "Custom file change handler",
"filter": { "path": ["src/**/*.ts"] }
},
{
"event": "stop",
"command": "printf '%s\\n' \"$HOOK_TOKENS\" >> token-usage.log",
"description": "Track token usage",
"async": true
}
]
}
}
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable or disable all hooks globally |
hooks | array | [] | Array of hook definitions |
Older configs still work. The original event-keyed shape ("on_file_change": ["eslint {{file}} --fix"]) and its on_* / before_* / after_* event names are accepted and rewritten onto the events below. See Legacy event names.
Hook definition properties
| Property | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event to hook into (see Hook events) |
command | string | Yes | Shell command to execute |
description | string | No | Description shown in the /hooks display |
enabled | boolean | No | Whether the hook is active (default: true) |
timeout | number | No | Timeout in milliseconds (default: 5000) |
async | boolean | No | Run without blocking the agent (default: false) |
matcher | string | No | Regex pattern to filter events |
filter | object | No | Filter to specific tools or paths |
importedFrom | object | No | Source metadata written by autohand import; keep it on imported hooks |
Filter object
Limit when a hook fires with a filter object:
{
"filter": {
"tool": ["run_command", "write_file"],
"path": ["src/**/*.ts", "lib/**/*.js"]
}
}
tool: array of tool names. The hook only fires for these tools.path: array of glob patterns. The hook only fires for matching file paths.
Matcher (regex filtering)
Use the matcher property to filter events with a regular expression:
{
"event": "pre-tool",
"command": "./log-dangerous.sh",
"matcher": "^(run_command|delete_path)$",
"description": "Log only dangerous tool calls"
}
What the matcher is tested against depends on the event:
| Event | Matcher matches against |
|---|---|
pre-tool, post-tool | Tool name |
permission-request | Tool name |
notification | Notification type |
session-start | Session type (startup, resume, clear) |
session-end | End reason (quit, clear, exit, error) |
subagent-start, subagent-progress, subagent-message, subagent-cancel-requested, subagent-stop | Subagent type |
automode:* | Event-specific auto-mode prompt, iteration, or reason |
review:* | Event-specific review path, scope, instructions, or error |
team-created, team-shutdown | Team name |
teammate-spawned, teammate-idle | Team name, teammate name, or teammate agent name |
task-assigned, task-completed | Task id, task owner, or task result |
Hook events
Every event below is available to config hooks, extension hooks, and the /hooks browser. post-response is a backward-compatible alias for stop and is listed under stop in /hooks.
Session
| Event | When fired | Context |
|---|---|---|
session-start | When a session begins | session type (startup/resume/clear) |
session-end | When a session ends | reason (quit/clear/exit/error), duration |
pre-clear | Before memory extraction on /clear or /new | session id, cwd |
Prompt & turn
| Event | When fired | Context |
|---|---|---|
pre-prompt | Before sending the instruction to the LLM | instruction, mentioned files |
stop | After the agent finishes responding (turn complete) | tokens used, tool call count, duration |
post-response | Alias for stop for backward compatibility | tokens used, tool call count, duration |
Tool
| Event | When fired | Context |
|---|---|---|
pre-tool | Before a tool begins execution | tool name, args, toolCallId |
post-tool | After a tool completes | tool name, success, duration, output |
File
| Event | When fired | Context |
|---|---|---|
file-modified | When a file is created, modified, or deleted | file path, change type |
Subagent
| Event | When fired | Context |
|---|---|---|
subagent-start | Before a worker begins its task | run id, parent id, source, workspace, task, name, type |
subagent-progress | When a worker's actual activity changes | run identity, status, activity, usage |
subagent-message | When a message is queued for a worker | run identity, queued message |
subagent-cancel-requested | When a worker stop is requested | run identity, status |
subagent-stop | When a worker completes, fails, or is cancelled | run identity, status, success, duration, error |
Synchronous subagent-start and subagent-progress hooks can return additionalContext to queue context for that worker's next safe model step, or continue: false with a stopReason to stop only that worker. subagent-message, subagent-cancel-requested, and subagent-stop are observational, and any returned control fields are ignored.
Permission & notification
| Event | When fired | Context |
|---|---|---|
permission-request | Before showing the permission dialog | tool, path, permission type |
permission-denied | After the user refuses a permission request | tool, path, command, refusing decision |
notification | When a notification is sent to the user | notification type, message |
Rate limit & errors
| Event | When fired | Context |
|---|---|---|
session-error | When an error occurs | error message, code, context |
rate-limit | When a provider rate limit ends the turn | error message, code, retryAfterMs, httpStatus, model, provider |
Long-window quotas (5-hour, daily, weekly, or unscoped) are not retried within the turn. The turn ends immediately and both session-error and rate-limit fire once. HOOK_RETRY_AFTER_MS is set only when the provider advertised a Retry-After, so branch on its presence rather than assuming a value.
Auto-mode
| Event | When fired | Context |
|---|---|---|
automode:start | When auto-mode starts | auto-mode session id, prompt, max iterations |
automode:iteration | On each auto-mode iteration | iteration, actions, files created/modified, cost |
automode:checkpoint | When auto-mode creates a checkpoint | iteration, checkpoint commit |
automode:pause | When auto-mode pauses | auto-mode session id, iteration |
automode:resume | When auto-mode resumes | auto-mode session id, iteration |
automode:cancel | When auto-mode is cancelled | cancel reason, iteration, cost |
automode:complete | When auto-mode completes successfully | iterations, actions, files changed, cost |
automode:error | When auto-mode encounters an error | error message, iteration |
Auto-research
| Event | When fired | Context |
|---|---|---|
autoresearch:start | When an auto-research session starts or resumes | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:pause | When an auto-research session is paused | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:init | When init_experiment configures the session | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:before | Before run_experiment starts an iteration | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:run | When run_experiment executes the benchmark | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:after | After run_experiment finishes an iteration | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:log | When log_experiment records a result | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:decision | When the deterministic experiment decision is persisted | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:replay | When an isolated candidate replay completes | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:rescore | When stored measurements are rescored with the current policy | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:prune | When artifact retention is previewed or applied | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:complete | When the auto-research loop completes | goal, active state, iteration, subcommand, attempt id, decision |
autoresearch:error | When auto-research encounters an error | goal, active state, iteration, subcommand, attempt id, decision |
Learn & goals
| Event | When fired | Context |
|---|---|---|
pre-learn | Before a learn operation begins | instruction, cwd |
post-learn | After a learn operation completes | instruction, duration, success |
goal-written:completed | After a goal objective is created | goal id, objective, source |
Teams
| Event | When fired | Context |
|---|---|---|
team-created | When a team is created | team name, member count |
teammate-spawned | When a teammate process starts | team name, teammate name, agent name, pid |
teammate-idle | When a teammate becomes idle | team name, teammate name |
task-assigned | When a task is assigned to a teammate | task id, owner, teammate name |
task-completed | When a task is marked complete | task id, owner, result |
team-shutdown | When team cleanup completes | team name, completed task count, total task count |
Review
| Event | When fired | Context |
|---|---|---|
review:start | When a code review begins | review path, scope, instructions |
review:end | When a code review session ends | review path, scope, duration |
review:paused | When a code review pauses | review path, scope |
review:failed | When a code review fails | review path, scope, review error |
review:completed | When a code review completes successfully | review path, scope, duration |
Mode & context
| Event | When fired | Context |
|---|---|---|
mode-change | When the permission mode changes | previous mode, current mode |
context:compact | When context is compacted | context lifecycle details |
context:overflow | When context overflow is detected | context lifecycle details |
context:warning | When context usage crosses the warning threshold | context lifecycle details |
context:critical | When context usage crosses the critical threshold | context lifecycle details |
Environment variables
When a hook command executes, these variables are set in its environment. Availability depends on the event.
| Variable | Description | Available in |
|---|---|---|
HOOK_EVENT | Event name (e.g. pre-tool) | All events |
HOOK_WORKSPACE | Workspace root path | All events |
HOOK_SESSION_ID | Current session ID | All events |
HOOK_TOOL | Tool name | pre-tool, post-tool, permission-request, permission-denied |
HOOK_TOOL_CALL_ID | Unique tool call ID | pre-tool, post-tool |
HOOK_ARGS | JSON-encoded tool arguments | pre-tool, post-tool |
HOOK_SUCCESS | true or false | post-tool |
HOOK_OUTPUT | Tool output/result | post-tool |
HOOK_DURATION | Execution time in ms | post-tool, stop, session-end |
HOOK_PATH | File path | file-modified, permission-request, permission-denied |
HOOK_CHANGE_TYPE | create, modify, or delete | file-modified |
HOOK_INSTRUCTION | User instruction | pre-prompt |
HOOK_MENTIONED_FILES | JSON array of mentioned files | pre-prompt |
HOOK_TOKENS | Tokens used | stop |
HOOK_TOOL_CALLS_COUNT | Number of tool calls | stop |
HOOK_TURN_TOOL_CALLS | Tool calls in the current turn | stop |
HOOK_TURN_DURATION | Turn duration in ms | stop |
HOOK_ERROR | Error message | session-error, rate-limit |
HOOK_ERROR_CODE | Error code | session-error, rate-limit |
HOOK_RETRY_AFTER_MS | Provider-advertised retry delay in ms (only when sent) | rate-limit |
HOOK_HTTP_STATUS | HTTP status that produced the rate limit | rate-limit |
HOOK_MODEL | Model that was rate limited | rate-limit |
HOOK_PROVIDER | Provider that reported the rate limit | rate-limit |
HOOK_SESSION_TYPE | startup, resume, or clear | session-start |
HOOK_SESSION_END_REASON | quit, clear, exit, or error | session-end |
HOOK_PREVIOUS_MODE | Previous permission mode | mode-change |
HOOK_MODE | Current permission mode | mode-change |
HOOK_SUBAGENT_ID | Exact worker run ID | subagent events |
HOOK_SUBAGENT_NAME | Subagent name | subagent events |
HOOK_SUBAGENT_TYPE | Subagent type | subagent events |
HOOK_SUBAGENT_PARENT_ID | Parent worker run ID, when nested | subagent events |
HOOK_SUBAGENT_SOURCE | delegate or team | subagent events |
HOOK_SUBAGENT_STATUS | Current worker status | subagent events |
HOOK_SUBAGENT_WORKSPACE | Selected execution workspace | subagent events |
HOOK_SUBAGENT_ACTIVITY | Actual model/tool activity, when available | subagent-progress |
HOOK_SUBAGENT_SUCCESS | true or false | subagent-stop |
HOOK_SUBAGENT_ERROR | Error message if failed | subagent-stop |
HOOK_SUBAGENT_DURATION | Duration in ms | subagent-stop |
HOOK_PERMISSION_TYPE | Permission type being requested, or the refusing decision (deny_once, deny_session, ...) | permission-request, permission-denied |
HOOK_NOTIFICATION_TYPE | Type of notification | notification |
HOOK_NOTIFICATION_MSG | Notification message | notification |
HOOK_AUTOMODE_SESSION_ID | Auto-mode session ID | automode:* |
HOOK_AUTOMODE_PROMPT | Auto-mode prompt/task | automode:start, automode:iteration |
HOOK_AUTOMODE_ITERATION | Current auto-mode iteration | automode:* |
HOOK_AUTOMODE_MAX_ITERATIONS | Maximum auto-mode iterations | automode:start, automode:iteration |
HOOK_AUTOMODE_ACTIONS | JSON array of actions | automode:iteration, automode:complete |
HOOK_AUTOMODE_FILES_CREATED | Number of files created | automode:* |
HOOK_AUTOMODE_FILES_MODIFIED | Number of files modified | automode:* |
HOOK_AUTOMODE_CANCEL_REASON | Cancellation reason | automode:cancel |
HOOK_AUTOMODE_CHECKPOINT | Checkpoint commit hash | automode:checkpoint |
HOOK_AUTOMODE_COST | Total auto-mode cost | automode:* |
HOOK_REVIEW_PATH | Review target path | review:* |
HOOK_REVIEW_SCOPE | Review scope | review:* |
HOOK_REVIEW_ERROR | Review error message | review:failed |
HOOK_REVIEW_INSTRUCTIONS | Review instructions/focus | review:* |
HOOK_GOAL_ID | Goal ID | goal-written:completed |
HOOK_GOAL_OBJECTIVE | Goal objective text | goal-written:completed |
HOOK_GOAL_SOURCE | Source that created the goal | goal-written:completed |
HOOK_TEAM_NAME | Team name | team-created, teammate-spawned, teammate-idle, task-assigned, task-completed, team-shutdown |
HOOK_TEAMMATE_NAME | Teammate name | teammate-spawned, teammate-idle, task-assigned, task-completed |
HOOK_TEAMMATE_AGENT | Teammate agent definition | teammate-spawned |
HOOK_TEAMMATE_PID | Teammate process ID | teammate-spawned |
HOOK_TEAM_TASK_ID | Team task ID | task-assigned, task-completed |
HOOK_TEAM_TASK_OWNER | Team task owner | task-assigned, task-completed |
HOOK_TEAM_TASK_RESULT | Team task result | task-completed |
HOOK_TEAM_MEMBER_COUNT | Number of team members | team-created, teammate-spawned, teammate-idle, team-shutdown |
HOOK_TEAM_TASKS_COMPLETED | Completed task count | teammate-idle, task-assigned, task-completed, team-shutdown |
HOOK_TEAM_TASKS_TOTAL | Total task count | teammate-idle, task-assigned, task-completed, team-shutdown |
HOOK_ADDITIONAL_WORKSPACES | JSON array of additional workspaces | All events when configured |
JSON input and control-flow responses
JSON input (stdin)
In addition to environment variables, every hook receives its context as a JSON object on stdin. Read it once and parse the fields you need:
#!/bin/bash
# Hook script that reads JSON input
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
TOOL_ARGS=$(echo "$INPUT" | jq -r '.tool_input')
echo "Tool: $TOOL_NAME with args: $TOOL_ARGS"
Fields that do not apply to the current event are null. A pre-tool payload looks like this:
{
"session_id": "abc123",
"cwd": "/path/to/workspace",
"hook_event_name": "pre-tool",
"tool_name": "write_file",
"tool_input": { "path": "src/index.ts", "content": "..." },
"tool_use_id": "call_123",
"tool_response": null,
"tool_success": null,
"file_path": null,
"change_type": null,
"instruction": null,
"mentioned_files": null,
"tokens_used": null,
"tokens_usage_status": null,
"tool_calls_count": null,
"turn_tool_calls": null,
"turn_duration": null,
"duration": null,
"error": null,
"error_code": null,
"session_type": null,
"session_end_reason": null,
"subagent_id": null,
"subagent_name": null,
"subagent_type": null,
"subagent_success": null,
"subagent_error": null,
"subagent_duration": null,
"permission_type": null,
"notification_type": null,
"notification_message": null,
"automode_session_id": null,
"automode_prompt": null,
"automode_iteration": null,
"automode_max_iterations": null,
"automode_actions": null,
"automode_files_created": null,
"automode_files_modified": null,
"automode_cancel_reason": null,
"automode_checkpoint_commit": null,
"automode_total_cost": null,
"review_path": null,
"review_scope": null,
"review_instructions": null,
"review_error": null,
"team_name": null,
"teammate_name": null,
"teammate_agent_name": null,
"teammate_pid": null,
"team_task_id": null,
"team_task_owner": null,
"team_task_result": null,
"team_member_count": null,
"team_tasks_completed": null,
"team_tasks_total": null,
"additional_workspaces": null
}
Mode changes add previous_mode and mode. Subagent events add the task and queued message text as subagent_task and subagent_message.
Control-flow responses
A synchronous hook can print a JSON object on stdout to control what the agent does next. This is how you automate permission decisions, block dangerous operations, or modify tool inputs.
{
"decision": "allow",
"reason": "Approved by automation",
"continue": true,
"stopReason": null,
"updatedInput": null,
"additionalContext": null
}
| Field | Type | Description |
|---|---|---|
decision | string | allow, deny, ask, or block |
reason | string | Reason for the decision (shown to the agent) |
continue | boolean | Whether to continue execution |
stopReason | string | Message shown when continue is false |
updatedInput | object | Modified tool input |
additionalContext | string | Additional context to add to the conversation |
| Decision | Effect |
|---|---|
allow | Approve the action without prompting the user |
deny | Reject the action without prompting the user |
ask | Continue with the normal user prompt |
block | Block execution entirely |
A pre-prompt hook can prevent model work with a decision of deny or block, continue: false, or exit code 2.
Exit codes
| Exit code | Meaning |
|---|---|
0 | Success. A JSON response on stdout is parsed if present. |
2 | Blocking error. Execution stops and the stderr message is reported. |
| Other | Non-blocking error. Logged, but execution continues. |
Template variables
Any hook command may contain {{variable}} placeholders. They are replaced before the command runs, so they work alongside the $HOOK_* environment variables. Values that are not a single plain word are single-quoted for the shell, so eslint {{file}} is safe for paths with spaces. Unknown variables become empty strings.
{
"event": "file-modified",
"command": "eslint {{file}} --fix",
"filter": { "path": ["**/*.ts"] }
}
| Variable | Value | Source |
|---|---|---|
{{file}}, {{path}}, {{resource}} | File path | HOOK_PATH |
{{action}} | Change type (create, modify, delete) or permission decision | HOOK_CHANGE_TYPE, HOOK_PERMISSION_TYPE |
{{tool}} | Tool name | HOOK_TOOL |
{{args}} | JSON-encoded tool arguments | HOOK_ARGS |
{{command}} | Shell command being run or approved | HOOK_ARGS (command), permission context |
{{cwd}}, {{project}} | Workspace root | HOOK_WORKSPACE |
{{session_id}} | Session ID | HOOK_SESSION_ID |
{{timestamp}} | ISO timestamp at execution | — |
{{duration}} | Duration in ms (tool, turn, or subagent) | HOOK_DURATION, HOOK_TURN_DURATION, HOOK_SUBAGENT_DURATION |
{{result}}, {{output}}, {{response}} | Tool output | HOOK_OUTPUT |
{{exit_code}} | 0 when the tool succeeded, 1 when it failed | HOOK_SUCCESS |
{{error}} | Error message | HOOK_ERROR, HOOK_SUBAGENT_ERROR, HOOK_REVIEW_ERROR |
{{context}} | Error code | HOOK_ERROR_CODE |
{{message}} | User instruction, notification message, or queued subagent message | HOOK_INSTRUCTION, HOOK_NOTIFICATION_MSG |
{{tokens}} | Tokens used in the turn | HOOK_TOKENS |
{{level}} | Notification type | HOOK_NOTIFICATION_TYPE |
{{agent}} | Subagent name or type | HOOK_SUBAGENT_NAME, HOOK_SUBAGENT_TYPE |
{{task}} | Subagent task or auto-mode prompt | HOOK_AUTOMODE_PROMPT |
{{iteration}}, {{iterations}} | Current auto-mode or auto-research iteration | HOOK_AUTOMODE_ITERATION |
{{total}}, {{max_iterations}} | Maximum iterations | HOOK_AUTOMODE_MAX_ITERATIONS |
{{reason}} | Cancel reason, context reason, or session end reason | HOOK_AUTOMODE_CANCEL_REASON, HOOK_SESSION_END_REASON |
Legacy event names
The first hooks documentation described an event-keyed config shape, on_* / before_* / after_* event names, and {{variable}} placeholders. All three still work and are rewritten onto the lifecycle events above, so an older configuration keeps firing without changes. Legacy hooks receive the same environment variables and JSON input as the event they map to, their results are reported under that event, and /hooks lists them under the mapped event.
Legacy config shape
Commands may be listed directly under an event name. Each string (or object with a command) becomes a hook definition for that event, and the array form and the event-keyed form can be mixed. The next time hooks are saved from /hooks, the file is written in the array form.
{
"hooks": {
"on_file_change": [
"eslint {{file}} --fix",
{ "command": "prettier --write {{file}}", "async": true }
],
"on_session_end": ["notify-send \"Autohand session finished\""]
}
}
Legacy name mapping
| Legacy name | Fires on | Only when |
|---|---|---|
on_session_start | session-start | — |
on_session_end | session-end | — |
on_session_resume | session-start | session type is resume |
before_tool_call | pre-tool | — |
after_tool_call | post-tool | — |
on_tool_error | post-tool | the tool failed |
on_file_change | file-modified | — |
on_file_create | file-modified | change type is create |
on_file_delete | file-modified | change type is delete |
on_file_read | post-tool | tool is read_file |
before_command | pre-tool | tool is run_command, shell, or custom_command |
after_command | post-tool | tool is run_command, shell, or custom_command |
on_user_message | pre-prompt | — |
on_agent_response | stop | — |
on_error | session-error | — |
on_permission_denied | permission-denied | — |
on_automode_start | automode:start | — |
on_automode_stop | automode:complete, automode:cancel, automode:error | — |
on_automode_iteration | automode:iteration | — |
on_subagent_start | subagent-start | — |
on_subagent_stop | subagent-stop | — |
on_permission_request | permission-request | — |
on_notification | notification | — |
Examples
Each example is one entry for the hooks.hooks array. Scripts under ~/.autohand/hooks/ must be executable.
Auto-lint on file changes
Run ESLint and Prettier whenever the agent creates or modifies a TypeScript file:
{
"event": "file-modified",
"command": "eslint \"$HOOK_PATH\" --fix && prettier --write \"$HOOK_PATH\"",
"description": "Auto-lint TypeScript",
"filter": { "path": ["**/*.ts", "**/*.tsx"] }
}
Run tests after changes
Execute the tests related to the changed file. Test runs take longer than the 5-second default, so raise the timeout:
{
"event": "file-modified",
"command": "npm test -- --findRelatedTests {{file}}",
"description": "Run related tests",
"filter": { "path": ["src/**/*"] },
"timeout": 30000
}
Slack notifications
Post a message when a session ends. Keep the webhook URL in an environment variable, and run the hook asynchronously so it never blocks shutdown:
{
"event": "session-end",
"command": "curl -X POST \"$SLACK_WEBHOOK\" -H 'Content-type: application/json' -d \"{\\\"text\\\": \\\"Autohand session ended ($HOOK_SESSION_END_REASON) after ${HOOK_DURATION}ms\\\"}\"",
"description": "Slack notification on session end",
"async": true
}
Audit logging
Record every tool call with its outcome and duration for compliance:
{
"event": "post-tool",
"command": "echo \"$(date -u +%FT%TZ) | $HOOK_TOOL | success=$HOOK_SUCCESS | ${HOOK_DURATION}ms\" >> ~/.autohand/audit.log",
"description": "Audit tool calls"
}
Block dangerous commands
Use a synchronous pre-tool hook to inspect shell commands before they run. Return {"decision": "deny"} to reject the call without prompting, or exit with code 2 to stop execution with the stderr message:
{
"event": "pre-tool",
"command": "~/.autohand/hooks/block-dangerous.sh",
"description": "Block destructive shell commands",
"matcher": "^run_command$"
}
With ~/.autohand/hooks/block-dangerous.sh:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if [[ "$COMMAND" =~ rm.*-rf.*/ ]]; then
echo '{"decision": "deny", "reason": "Recursive delete of a root path is not allowed"}'
exit 0
fi
# Equivalent hard stop: report on stderr and exit 2
# echo "Blocked dangerous rm command: $COMMAND" >&2
# exit 2
exit 0
Type checking on TypeScript files
Run the type checker whenever a TypeScript file changes:
{
"event": "file-modified",
"command": "npx tsc --noEmit",
"description": "Type-check after TypeScript edits",
"filter": { "path": ["**/*.ts", "**/*.tsx"] },
"timeout": 60000
}
Git pre-commit style check
Gate git commit behind lint and type checks. A non-zero exit from the checks becomes exit code 2, which blocks the command:
{
"event": "pre-tool",
"command": "~/.autohand/hooks/commit-gate.sh",
"description": "Lint and type-check before git commit",
"matcher": "^run_command$",
"timeout": 120000
}
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if [[ "$COMMAND" == git\ commit* ]]; then
if ! npm run lint && npm run typecheck; then
echo "Commit blocked: lint or typecheck failed" >&2
exit 2
fi
fi
exit 0
Advanced configuration
Timeouts
Each hook has a timeout in milliseconds (default 5000). Quick logging needs 1000-2000ms, network calls usually need 10000-30000ms, and anything longer should run asynchronously.
{
"event": "file-modified",
"command": "npm test -- --findRelatedTests \"$HOOK_PATH\"",
"timeout": 30000
}
Async hooks
Set async: true to run a hook in the background without blocking the agent. Use it for logging, metrics, and notifications, not for hooks that return control-flow decisions, since their output is not waited for:
{
"event": "file-modified",
"command": "npm run build",
"async": true
}
External scripts
For anything beyond a one-liner, point command at a script. Scripts created from /hooks are saved under ~/.autohand/hooks/generated/, and the bundled examples live in ~/.autohand/hooks/. Scripts read the JSON payload from stdin and the $HOOK_* variables from the environment:
{
"hooks": {
"hooks": [
{ "event": "file-modified", "command": "~/.autohand/hooks/on-change.sh" },
{ "event": "session-end", "command": "~/.autohand/hooks/session-complete.sh" }
]
}
}
Conditional hooks
Prefer filter and matcher over shell conditionals so hooks that do not apply never spawn a process:
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "npm test -- \"$HOOK_PATH\"",
"filter": { "path": ["**/*.test.ts"] }
},
{
"event": "file-modified",
"command": "eslint \"$HOOK_PATH\"",
"filter": { "path": ["**/*.ts"] }
},
{
"event": "post-tool",
"command": "~/.autohand/hooks/shell-audit.sh",
"matcher": "^(run_command|custom_command)$"
}
]
}
}
Debugging hooks
View configured hooks
Open /hooks for the interactive event browser, which shows installed and active counts per event and lists each hook with its source. /hooks list prints the same table and also works without a TTY:
/hooks list
# Hooks — Lifecycle hooks from config and enabled plugins.
# Event Installed Active Description
# session-start 1 1 When a session begins
# pre-tool 2 2 Before a tool executes
# file-modified 1 0 When files are changed
# ...
An installed hook with an active count of zero is disabled, either individually or by the global switch. /hooks manage toggles hooks on and off, removes them, adds them manually, and can run a hook with sample context (only do this when you intend its side effects to occur).
Test hooks manually
Run the script outside Autohand with the same inputs the agent provides: the $HOOK_* variables in the environment and the JSON payload on stdin. Then check its exit code and stdout:
# Environment-variable style
HOOK_EVENT=file-modified HOOK_PATH=src/auth.ts HOOK_CHANGE_TYPE=modify \
HOOK_WORKSPACE="$PWD" HOOK_SESSION_ID=test \
~/.autohand/hooks/on-change.sh
# JSON-stdin style, checking a control-flow decision
echo '{"hook_event_name":"pre-tool","tool_name":"run_command","tool_input":{"command":"rm -rf /"}}' \
| ~/.autohand/hooks/block-dangerous.sh; echo "exit=$?"
Common issues
| Issue | Solution |
|---|---|
| Hook not running | Check the event name, that hooks.enabled and the hook's own enabled are not false, and that filter or matcher actually matches. /hooks shows a notice when hooks are globally disabled. |
| Variable is empty | $HOOK_* variables are event-specific (see the table above). Unknown {{variable}} placeholders expand to an empty string. |
| Hook timed out | Raise timeout (default 5000ms) or mark the hook async. |
| Hook blocks the agent | Use async: true for slow, non-critical work. |
| Decision ignored | Control-flow JSON is only honored from synchronous hooks with exit code 0, and only on events that accept control fields. |
| Permission denied | Make scripts executable with chmod +x. |
Best practices
Keep hooks fast
Synchronous hooks block the agent until they finish. Keep them short, and move long-running or non-critical work behind async: true.
Handle failures deliberately
Hook failures never crash the agent. A non-zero exit other than 2 is logged and execution continues, while exit code 2 blocks the current action with the stderr message. Exit 2 only when you mean to stop the agent.
Use control flow sparingly
Reserve decision: "allow" for operations you are certain are safe, fall back to decision: "ask", use decision: "block" or exit code 2 for truly dangerous operations, and always include a reason so allow and deny decisions are auditable.
Treat hook input as untrusted
Hooks run in your shell with your permissions and receive model-generated arguments. Quote $HOOK_* values, pass values as arguments rather than interpolating them into shell strings, keep secrets in environment variables, and avoid loading hooks from config files you do not trust.
Document your hooks
Give each hook a description so it is recognizable in /hooks, and explain team hooks in your AGENTS.md:
# AGENTS.md
## Hooks
The following hooks are configured:
- **file-modified**: Auto-formats code with Prettier and ESLint
- **session-end**: Posts a summary to the #dev-updates Slack channel
Frequently asked questions
What are hooks in Autohand CLI?
Hooks are shell commands that Autohand runs automatically in response to lifecycle events such as tool calls, file changes, turn completion, and session start or end. They automate workflows like running linters after file changes, sending Slack notifications, or blocking dangerous commands, and are configured under the hooks key of ~/.autohand/config.json or managed with the /hooks command.
What events can trigger hooks in Autohand?
Autohand emits more than fifty hook events grouped into session (session-start, session-end, pre-clear), prompt and turn (pre-prompt, stop), tool (pre-tool, post-tool), file (file-modified), subagent, permission and notification (permission-request, permission-denied, notification), rate limit and error, auto-mode, auto-research, learn and goal, team, review, and mode and context events. Legacy names such as on_file_change and before_tool_call are still accepted and mapped onto these events.
How do I create a hook in Autohand?
Run /hooks, select an event, and describe the automation in plain English, or add an object with an event and a command to the hooks.hooks array in config.json. Commands receive context through HOOK_* environment variables, JSON on stdin, and {{variable}} placeholders. For example, { "event": "file-modified", "command": "eslint --fix {{file}}" } runs ESLint every time the agent changes a file.