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
      }
    ]
  }
}
FieldTypeDefaultDescription
enabledbooleantrueEnable or disable all hooks globally
hooksarray[]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

PropertyTypeRequiredDescription
eventstringYesEvent to hook into (see Hook events)
commandstringYesShell command to execute
descriptionstringNoDescription shown in the /hooks display
enabledbooleanNoWhether the hook is active (default: true)
timeoutnumberNoTimeout in milliseconds (default: 5000)
asyncbooleanNoRun without blocking the agent (default: false)
matcherstringNoRegex pattern to filter events
filterobjectNoFilter to specific tools or paths
importedFromobjectNoSource 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:

EventMatcher matches against
pre-tool, post-toolTool name
permission-requestTool name
notificationNotification type
session-startSession type (startup, resume, clear)
session-endEnd reason (quit, clear, exit, error)
subagent-start, subagent-progress, subagent-message, subagent-cancel-requested, subagent-stopSubagent type
automode:*Event-specific auto-mode prompt, iteration, or reason
review:*Event-specific review path, scope, instructions, or error
team-created, team-shutdownTeam name
teammate-spawned, teammate-idleTeam name, teammate name, or teammate agent name
task-assigned, task-completedTask 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

EventWhen firedContext
session-startWhen a session beginssession type (startup/resume/clear)
session-endWhen a session endsreason (quit/clear/exit/error), duration
pre-clearBefore memory extraction on /clear or /newsession id, cwd

Prompt & turn

EventWhen firedContext
pre-promptBefore sending the instruction to the LLMinstruction, mentioned files
stopAfter the agent finishes responding (turn complete)tokens used, tool call count, duration
post-responseAlias for stop for backward compatibilitytokens used, tool call count, duration

Tool

EventWhen firedContext
pre-toolBefore a tool begins executiontool name, args, toolCallId
post-toolAfter a tool completestool name, success, duration, output

File

EventWhen firedContext
file-modifiedWhen a file is created, modified, or deletedfile path, change type

Subagent

EventWhen firedContext
subagent-startBefore a worker begins its taskrun id, parent id, source, workspace, task, name, type
subagent-progressWhen a worker's actual activity changesrun identity, status, activity, usage
subagent-messageWhen a message is queued for a workerrun identity, queued message
subagent-cancel-requestedWhen a worker stop is requestedrun identity, status
subagent-stopWhen a worker completes, fails, or is cancelledrun 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

EventWhen firedContext
permission-requestBefore showing the permission dialogtool, path, permission type
permission-deniedAfter the user refuses a permission requesttool, path, command, refusing decision
notificationWhen a notification is sent to the usernotification type, message

Rate limit & errors

EventWhen firedContext
session-errorWhen an error occurserror message, code, context
rate-limitWhen a provider rate limit ends the turnerror 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

EventWhen firedContext
automode:startWhen auto-mode startsauto-mode session id, prompt, max iterations
automode:iterationOn each auto-mode iterationiteration, actions, files created/modified, cost
automode:checkpointWhen auto-mode creates a checkpointiteration, checkpoint commit
automode:pauseWhen auto-mode pausesauto-mode session id, iteration
automode:resumeWhen auto-mode resumesauto-mode session id, iteration
automode:cancelWhen auto-mode is cancelledcancel reason, iteration, cost
automode:completeWhen auto-mode completes successfullyiterations, actions, files changed, cost
automode:errorWhen auto-mode encounters an errorerror message, iteration

Auto-research

EventWhen firedContext
autoresearch:startWhen an auto-research session starts or resumesgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:pauseWhen an auto-research session is pausedgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:initWhen init_experiment configures the sessiongoal, active state, iteration, subcommand, attempt id, decision
autoresearch:beforeBefore run_experiment starts an iterationgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:runWhen run_experiment executes the benchmarkgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:afterAfter run_experiment finishes an iterationgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:logWhen log_experiment records a resultgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:decisionWhen the deterministic experiment decision is persistedgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:replayWhen an isolated candidate replay completesgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:rescoreWhen stored measurements are rescored with the current policygoal, active state, iteration, subcommand, attempt id, decision
autoresearch:pruneWhen artifact retention is previewed or appliedgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:completeWhen the auto-research loop completesgoal, active state, iteration, subcommand, attempt id, decision
autoresearch:errorWhen auto-research encounters an errorgoal, active state, iteration, subcommand, attempt id, decision

Learn & goals

EventWhen firedContext
pre-learnBefore a learn operation beginsinstruction, cwd
post-learnAfter a learn operation completesinstruction, duration, success
goal-written:completedAfter a goal objective is createdgoal id, objective, source

Teams

EventWhen firedContext
team-createdWhen a team is createdteam name, member count
teammate-spawnedWhen a teammate process startsteam name, teammate name, agent name, pid
teammate-idleWhen a teammate becomes idleteam name, teammate name
task-assignedWhen a task is assigned to a teammatetask id, owner, teammate name
task-completedWhen a task is marked completetask id, owner, result
team-shutdownWhen team cleanup completesteam name, completed task count, total task count

Review

EventWhen firedContext
review:startWhen a code review beginsreview path, scope, instructions
review:endWhen a code review session endsreview path, scope, duration
review:pausedWhen a code review pausesreview path, scope
review:failedWhen a code review failsreview path, scope, review error
review:completedWhen a code review completes successfullyreview path, scope, duration

Mode & context

EventWhen firedContext
mode-changeWhen the permission mode changesprevious mode, current mode
context:compactWhen context is compactedcontext lifecycle details
context:overflowWhen context overflow is detectedcontext lifecycle details
context:warningWhen context usage crosses the warning thresholdcontext lifecycle details
context:criticalWhen context usage crosses the critical thresholdcontext lifecycle details

Environment variables

When a hook command executes, these variables are set in its environment. Availability depends on the event.

VariableDescriptionAvailable in
HOOK_EVENTEvent name (e.g. pre-tool)All events
HOOK_WORKSPACEWorkspace root pathAll events
HOOK_SESSION_IDCurrent session IDAll events
HOOK_TOOLTool namepre-tool, post-tool, permission-request, permission-denied
HOOK_TOOL_CALL_IDUnique tool call IDpre-tool, post-tool
HOOK_ARGSJSON-encoded tool argumentspre-tool, post-tool
HOOK_SUCCESStrue or falsepost-tool
HOOK_OUTPUTTool output/resultpost-tool
HOOK_DURATIONExecution time in mspost-tool, stop, session-end
HOOK_PATHFile pathfile-modified, permission-request, permission-denied
HOOK_CHANGE_TYPEcreate, modify, or deletefile-modified
HOOK_INSTRUCTIONUser instructionpre-prompt
HOOK_MENTIONED_FILESJSON array of mentioned filespre-prompt
HOOK_TOKENSTokens usedstop
HOOK_TOOL_CALLS_COUNTNumber of tool callsstop
HOOK_TURN_TOOL_CALLSTool calls in the current turnstop
HOOK_TURN_DURATIONTurn duration in msstop
HOOK_ERRORError messagesession-error, rate-limit
HOOK_ERROR_CODEError codesession-error, rate-limit
HOOK_RETRY_AFTER_MSProvider-advertised retry delay in ms (only when sent)rate-limit
HOOK_HTTP_STATUSHTTP status that produced the rate limitrate-limit
HOOK_MODELModel that was rate limitedrate-limit
HOOK_PROVIDERProvider that reported the rate limitrate-limit
HOOK_SESSION_TYPEstartup, resume, or clearsession-start
HOOK_SESSION_END_REASONquit, clear, exit, or errorsession-end
HOOK_PREVIOUS_MODEPrevious permission modemode-change
HOOK_MODECurrent permission modemode-change
HOOK_SUBAGENT_IDExact worker run IDsubagent events
HOOK_SUBAGENT_NAMESubagent namesubagent events
HOOK_SUBAGENT_TYPESubagent typesubagent events
HOOK_SUBAGENT_PARENT_IDParent worker run ID, when nestedsubagent events
HOOK_SUBAGENT_SOURCEdelegate or teamsubagent events
HOOK_SUBAGENT_STATUSCurrent worker statussubagent events
HOOK_SUBAGENT_WORKSPACESelected execution workspacesubagent events
HOOK_SUBAGENT_ACTIVITYActual model/tool activity, when availablesubagent-progress
HOOK_SUBAGENT_SUCCESStrue or falsesubagent-stop
HOOK_SUBAGENT_ERRORError message if failedsubagent-stop
HOOK_SUBAGENT_DURATIONDuration in mssubagent-stop
HOOK_PERMISSION_TYPEPermission type being requested, or the refusing decision (deny_once, deny_session, ...)permission-request, permission-denied
HOOK_NOTIFICATION_TYPEType of notificationnotification
HOOK_NOTIFICATION_MSGNotification messagenotification
HOOK_AUTOMODE_SESSION_IDAuto-mode session IDautomode:*
HOOK_AUTOMODE_PROMPTAuto-mode prompt/taskautomode:start, automode:iteration
HOOK_AUTOMODE_ITERATIONCurrent auto-mode iterationautomode:*
HOOK_AUTOMODE_MAX_ITERATIONSMaximum auto-mode iterationsautomode:start, automode:iteration
HOOK_AUTOMODE_ACTIONSJSON array of actionsautomode:iteration, automode:complete
HOOK_AUTOMODE_FILES_CREATEDNumber of files createdautomode:*
HOOK_AUTOMODE_FILES_MODIFIEDNumber of files modifiedautomode:*
HOOK_AUTOMODE_CANCEL_REASONCancellation reasonautomode:cancel
HOOK_AUTOMODE_CHECKPOINTCheckpoint commit hashautomode:checkpoint
HOOK_AUTOMODE_COSTTotal auto-mode costautomode:*
HOOK_REVIEW_PATHReview target pathreview:*
HOOK_REVIEW_SCOPEReview scopereview:*
HOOK_REVIEW_ERRORReview error messagereview:failed
HOOK_REVIEW_INSTRUCTIONSReview instructions/focusreview:*
HOOK_GOAL_IDGoal IDgoal-written:completed
HOOK_GOAL_OBJECTIVEGoal objective textgoal-written:completed
HOOK_GOAL_SOURCESource that created the goalgoal-written:completed
HOOK_TEAM_NAMETeam nameteam-created, teammate-spawned, teammate-idle, task-assigned, task-completed, team-shutdown
HOOK_TEAMMATE_NAMETeammate nameteammate-spawned, teammate-idle, task-assigned, task-completed
HOOK_TEAMMATE_AGENTTeammate agent definitionteammate-spawned
HOOK_TEAMMATE_PIDTeammate process IDteammate-spawned
HOOK_TEAM_TASK_IDTeam task IDtask-assigned, task-completed
HOOK_TEAM_TASK_OWNERTeam task ownertask-assigned, task-completed
HOOK_TEAM_TASK_RESULTTeam task resulttask-completed
HOOK_TEAM_MEMBER_COUNTNumber of team membersteam-created, teammate-spawned, teammate-idle, team-shutdown
HOOK_TEAM_TASKS_COMPLETEDCompleted task countteammate-idle, task-assigned, task-completed, team-shutdown
HOOK_TEAM_TASKS_TOTALTotal task countteammate-idle, task-assigned, task-completed, team-shutdown
HOOK_ADDITIONAL_WORKSPACESJSON array of additional workspacesAll 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
}
FieldTypeDescription
decisionstringallow, deny, ask, or block
reasonstringReason for the decision (shown to the agent)
continuebooleanWhether to continue execution
stopReasonstringMessage shown when continue is false
updatedInputobjectModified tool input
additionalContextstringAdditional context to add to the conversation
DecisionEffect
allowApprove the action without prompting the user
denyReject the action without prompting the user
askContinue with the normal user prompt
blockBlock 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 codeMeaning
0Success. A JSON response on stdout is parsed if present.
2Blocking error. Execution stops and the stderr message is reported.
OtherNon-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"] }
}
VariableValueSource
{{file}}, {{path}}, {{resource}}File pathHOOK_PATH
{{action}}Change type (create, modify, delete) or permission decisionHOOK_CHANGE_TYPE, HOOK_PERMISSION_TYPE
{{tool}}Tool nameHOOK_TOOL
{{args}}JSON-encoded tool argumentsHOOK_ARGS
{{command}}Shell command being run or approvedHOOK_ARGS (command), permission context
{{cwd}}, {{project}}Workspace rootHOOK_WORKSPACE
{{session_id}}Session IDHOOK_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 outputHOOK_OUTPUT
{{exit_code}}0 when the tool succeeded, 1 when it failedHOOK_SUCCESS
{{error}}Error messageHOOK_ERROR, HOOK_SUBAGENT_ERROR, HOOK_REVIEW_ERROR
{{context}}Error codeHOOK_ERROR_CODE
{{message}}User instruction, notification message, or queued subagent messageHOOK_INSTRUCTION, HOOK_NOTIFICATION_MSG
{{tokens}}Tokens used in the turnHOOK_TOKENS
{{level}}Notification typeHOOK_NOTIFICATION_TYPE
{{agent}}Subagent name or typeHOOK_SUBAGENT_NAME, HOOK_SUBAGENT_TYPE
{{task}}Subagent task or auto-mode promptHOOK_AUTOMODE_PROMPT
{{iteration}}, {{iterations}}Current auto-mode or auto-research iterationHOOK_AUTOMODE_ITERATION
{{total}}, {{max_iterations}}Maximum iterationsHOOK_AUTOMODE_MAX_ITERATIONS
{{reason}}Cancel reason, context reason, or session end reasonHOOK_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 nameFires onOnly when
on_session_startsession-start—
on_session_endsession-end—
on_session_resumesession-startsession type is resume
before_tool_callpre-tool—
after_tool_callpost-tool—
on_tool_errorpost-toolthe tool failed
on_file_changefile-modified—
on_file_createfile-modifiedchange type is create
on_file_deletefile-modifiedchange type is delete
on_file_readpost-tooltool is read_file
before_commandpre-tooltool is run_command, shell, or custom_command
after_commandpost-tooltool is run_command, shell, or custom_command
on_user_messagepre-prompt—
on_agent_responsestop—
on_errorsession-error—
on_permission_deniedpermission-denied—
on_automode_startautomode:start—
on_automode_stopautomode:complete, automode:cancel, automode:error—
on_automode_iterationautomode:iteration—
on_subagent_startsubagent-start—
on_subagent_stopsubagent-stop—
on_permission_requestpermission-request—
on_notificationnotification—

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

IssueSolution
Hook not runningCheck 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 outRaise timeout (default 5000ms) or mark the hook async.
Hook blocks the agentUse async: true for slow, non-critical work.
Decision ignoredControl-flow JSON is only honored from synchronous hooks with exit code 0, and only on events that accept control fields.
Permission deniedMake 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.