Hooks System

May 8, 2026 · View on GitHub

Autohand's hooks system allows you to run custom shell commands in response to lifecycle events like tool execution, file modifications, session lifecycle, and LLM interactions. Hooks can be configured via config.json or managed interactively with the /hooks command.

Overview

Hooks are useful for:

  • Logging tool executions for debugging
  • Sending notifications when tasks complete
  • Triggering CI/CD pipelines when files change
  • Custom metrics and telemetry collection
  • Integrating with external tools and services
  • Automating permission decisions
  • Custom session management

Two Modes of Hook Integration

1. Config-Based Hooks (CLI)

Define shell commands in your ~/.autohand/config.json that run automatically on lifecycle events. These hooks run in your local shell environment.

2. JSON-RPC 2.0 Notifications (IDE Integration)

When running in RPC mode (VS Code, Zed, etc.), hook events are also emitted as JSON-RPC 2.0 notifications that IDE extensions can subscribe to.


Hook Events

EventWhen FiredContext Available
pre-toolBefore a tool begins executiontool name, args, toolCallId
post-toolAfter a tool completestool name, success, duration, output
file-modifiedWhen a file is created, modified, or deletedfile path, change type
pre-promptBefore sending instruction to LLMinstruction, mentioned files
stopAfter agent finishes responding (turn complete)tokens used, tool calls count, duration
session-startWhen a session beginssession type (startup/resume/clear)
session-endWhen a session endsreason (quit/clear/exit/error), duration
session-errorWhen an error occurserror message, code, context
subagent-stopWhen a subagent finishes executionsubagent id, name, type, success, duration
permission-requestBefore showing permission dialogtool, path, permission type
notificationWhen a notification is sent to usernotification type, message

Note: post-response is an alias for stop for backward compatibility.


Configuration

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
      }
    ]
  }
}

Hook Definition Properties

PropertyTypeRequiredDescription
eventstringYesEvent to hook into (see events table)
commandstringYesShell command to execute
descriptionstringNoDescription shown in /hooks display
enabledbooleanNoWhether hook is active (default: true)
timeoutnumberNoTimeout in ms (default: 5000)
asyncbooleanNoRun without blocking (default: false)
matcherstringNoRegex pattern to filter events
filterobjectNoFilter to specific tools or paths

Filter Object

Limit when a hook fires using filters:

{
  "filter": {
    "tool": ["run_command", "write_file"],
    "path": ["src/**/*.ts", "lib/**/*.js"]
  }
}
  • tool: Array of tool names. Hook only fires for these tools.
  • path: Array of glob patterns. Hook only fires for matching file paths.

Matcher (Regex Filtering)

Use the matcher property to filter events using regex patterns:

{
  "event": "pre-tool",
  "command": "./log-dangerous.sh",
  "matcher": "^(run_command|delete_path)$",
  "description": "Log only dangerous tool calls"
}

What the matcher matches against depends on the event type:

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-stopSubagent type

JSON Input (stdin)

Hooks receive context as JSON via stdin, in addition to environment variables. This allows for more complex data handling:

#!/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"

JSON Input Structure

{
  "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,
  "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
}

Control Flow Responses

Hooks can return JSON to control agent behavior. This is useful for:

  • Automating permission decisions
  • Blocking dangerous operations
  • Modifying tool inputs

Response Format

{
  "decision": "allow",
  "reason": "Approved by automation",
  "continue": true,
  "stopReason": null,
  "updatedInput": null,
  "additionalContext": null
}

Response Fields

FieldTypeDescription
decisionstringallow, deny, ask, or block
reasonstringReason for decision (shown to agent)
continuebooleanWhether to continue execution
stopReasonstringMessage shown when continue is false
updatedInputobjectModified tool input
additionalContextstringAdditional context to add to conversation

Decision Values

DecisionEffect
allowApprove the action without prompting user
denyReject the action without prompting user
askContinue with normal user prompt
blockBlock execution entirely

Example: Auto-approve safe commands

#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# Auto-approve git status and git diff
if [[ "$TOOL" == "run_command" && "$COMMAND" =~ ^git\ (status|diff) ]]; then
  echo '{"decision": "allow", "reason": "Safe git command"}'
  exit 0
fi

# Ask for everything else
echo '{"decision": "ask"}'

Exit Codes

Hook exit codes have special meaning:

Exit CodeMeaning
0Success - JSON response parsed if present
2Blocking error - stops execution with stderr message
OtherNon-blocking error - logged but execution continues

Example: Block dangerous operations

#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')

# Block rm -rf /
if [[ "$COMMAND" =~ rm.*-rf.*/ ]]; then
  echo "Blocked dangerous rm command: $COMMAND" >&2
  exit 2
fi

exit 0

Environment Variables

When your hook command executes, these environment variables are available:

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
HOOK_TOOL_CALL_IDUnique tool call IDpre-tool, post-tool
HOOK_ARGSJSON-encoded tool argumentspre-tool, post-tool
HOOK_SUCCESS"true" or "false"post-tool
HOOK_OUTPUTTool output/resultpost-tool
HOOK_DURATIONExecution time in mspost-tool, stop, session-end
HOOK_PATHFile pathfile-modified, permission-request
HOOK_CHANGE_TYPE"create", "modify", or "delete"file-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 current turnstop
HOOK_TURN_DURATIONTurn duration in msstop
HOOK_ERRORError messagesession-error
HOOK_ERROR_CODEError codesession-error
HOOK_SESSION_TYPEstartup, resume, or clearsession-start
HOOK_SESSION_END_REASONquit, clear, exit, or errorsession-end
HOOK_SUBAGENT_IDSubagent task IDsubagent-stop
HOOK_SUBAGENT_NAMESubagent namesubagent-stop
HOOK_SUBAGENT_TYPESubagent typesubagent-stop
HOOK_SUBAGENT_SUCCESS"true" or "false"subagent-stop
HOOK_SUBAGENT_ERRORError message if failedsubagent-stop
HOOK_SUBAGENT_DURATIONDuration in mssubagent-stop
HOOK_PERMISSION_TYPEPermission type being requestedpermission-request
HOOK_NOTIFICATION_TYPEType of notificationnotification
HOOK_NOTIFICATION_MSGNotification messagenotification

Examples

Log All Tool Executions

{
  "event": "pre-tool",
  "command": "echo \"$(date) - Tool: $HOOK_TOOL\" >> ~/.autohand/tool.log",
  "description": "Log tool usage"
}

Notify on File Changes

{
  "event": "file-modified",
  "command": "osascript -e 'display notification \"File changed: '$HOOK_PATH'\" with title \"Autohand\"'",
  "description": "macOS notification on file change",
  "filter": {
    "path": ["src/**/*.ts"]
  }
}

Track Token Usage

{
  "event": "stop",
  "command": "curl -X POST https://api.example.com/metrics -d '{\"tokens\": '$HOOK_TOKENS'}'",
  "description": "Send token metrics",
  "async": true
}

Run Linter on Modified TypeScript Files

{
  "event": "file-modified",
  "command": "eslint \"$HOOK_PATH\" --fix",
  "description": "Auto-lint TypeScript",
  "filter": {
    "path": ["**/*.ts"]
  }
}

Auto-approve Read Operations

{
  "event": "permission-request",
  "command": "./auto-approve-reads.sh",
  "matcher": "^read_file$",
  "description": "Auto-approve file reads"
}

With auto-approve-reads.sh:

#!/bin/bash
echo '{"decision": "allow", "reason": "Read operations are safe"}'

Log Session Lifecycle

{
  "event": "session-start",
  "command": "echo \"Session started: $HOOK_SESSION_TYPE at $(date)\" >> ~/.autohand/sessions.log",
  "description": "Log session starts"
}
{
  "event": "session-end",
  "command": "echo \"Session ended: $HOOK_SESSION_END_REASON after ${HOOK_DURATION}ms\" >> ~/.autohand/sessions.log",
  "description": "Log session ends"
}

Track Subagent Performance

{
  "event": "subagent-stop",
  "command": "echo \"Subagent $HOOK_SUBAGENT_NAME ($HOOK_SUBAGENT_TYPE): $HOOK_SUBAGENT_SUCCESS in ${HOOK_SUBAGENT_DURATION}ms\" >> ~/.autohand/subagents.log",
  "description": "Track subagent performance"
}

Managing Hooks with /hooks

Use the /hooks slash command to interactively:

  • View all registered hooks grouped by event
  • Add new hooks
  • Enable/disable individual hooks
  • Remove hooks
  • Test hooks with sample context
  • Toggle hooks globally

Display Example

Hooks
──────────────────────────────────────────────────
Mode: enabled

pre-tool (2/2 enabled)
  1. [enabled] echo "Running tool: $HOOK_TOOL" - Log tool usage
  2. [enabled] ./notify.sh - Notify slack

post-tool (1/1 enabled)
  1. [enabled] ./metrics.sh - Track metrics

stop (1/1 enabled)
  1. [enabled] ./track-tokens.sh - Track token usage

session-start (1/1 enabled)
  1. [enabled] ./log-session.sh - Log sessions

──────────────────────────────────────────────────
Total: 5 hooks (5 enabled, 0 disabled)

JSON-RPC 2.0 Hook Notifications

When running in RPC mode (IDE integration), hook events are emitted as JSON-RPC 2.0 notifications that clients can subscribe to.

Notification Types

NotificationMethod
Pre-Toolautohand.hook.preTool
Post-Toolautohand.hook.postTool
File Modifiedautohand.hook.fileModified
Pre-Promptautohand.hook.prePrompt
Stopautohand.hook.stop
Post-Responseautohand.hook.postResponse (alias for stop)
Session Startautohand.hook.sessionStart
Session Endautohand.hook.sessionEnd
Session Errorautohand.hook.sessionError
Subagent Stopautohand.hook.subagentStop
Permission Requestautohand.hook.permissionRequest
Notificationautohand.hook.notification

Example: VS Code Extension

// Subscribe to hook notifications
rpcClient.onNotification('autohand.hook.preTool', (params) => {
  outputChannel.appendLine(`[Hook] Pre-tool: ${params.toolName}`);
  vscode.window.setStatusBarMessage(`Running ${params.toolName}...`);
});

rpcClient.onNotification('autohand.hook.postTool', (params) => {
  const status = params.success ? 'success' : 'failed';
  outputChannel.appendLine(`[Hook] Post-tool: ${params.toolName} (${status}, ${params.duration}ms)`);
});

rpcClient.onNotification('autohand.hook.stop', (params) => {
  outputChannel.appendLine(`[Hook] Turn complete: ${params.tokensUsed} tokens, ${params.toolCallsCount} tool calls`);
});

rpcClient.onNotification('autohand.hook.sessionStart', (params) => {
  outputChannel.appendLine(`[Hook] Session started: ${params.sessionType}`);
});

rpcClient.onNotification('autohand.hook.sessionEnd', (params) => {
  outputChannel.appendLine(`[Hook] Session ended: ${params.reason} after ${params.duration}ms`);
});

rpcClient.onNotification('autohand.hook.subagentStop', (params) => {
  const status = params.success ? 'completed' : 'failed';
  outputChannel.appendLine(`[Hook] Subagent ${params.subagentName} ${status} in ${params.duration}ms`);
});

Notification Parameters

autohand.hook.preTool

{
  toolId: string;
  toolName: string;
  args: Record<string, unknown>;
  timestamp: string;
}

autohand.hook.postTool

{
  toolId: string;
  toolName: string;
  success: boolean;
  duration: number;
  output?: string;
  timestamp: string;
}

autohand.hook.fileModified

{
  filePath: string;
  changeType: 'create' | 'modify' | 'delete';
  toolId: string;
  timestamp: string;
}

autohand.hook.prePrompt

{
  instruction: string;
  mentionedFiles: string[];
  timestamp: string;
}

autohand.hook.stop

{
  tokensUsed: number;
  tokensUsageStatus?: "actual" | "unavailable";
  toolCallsCount: number;
  duration: number;
  timestamp: string;
}

autohand.hook.sessionStart

{
  sessionType: 'startup' | 'resume' | 'clear';
  timestamp: string;
}

autohand.hook.sessionEnd

{
  reason: 'quit' | 'clear' | 'exit' | 'error';
  duration: number;
  timestamp: string;
}

autohand.hook.sessionError

{
  error: string;
  code?: string;
  context?: Record<string, unknown>;
  timestamp: string;
}

autohand.hook.subagentStop

{
  subagentId: string;
  subagentName: string;
  subagentType: string;
  success: boolean;
  duration: number;
  error?: string;
  timestamp: string;
}

autohand.hook.permissionRequest

{
  tool: string;
  path?: string;
  command?: string;
  args?: Record<string, unknown>;
  timestamp: string;
}

autohand.hook.notification

{
  notificationType: string;
  message: string;
  timestamp: string;
}

Built-in Hooks

Autohand ships with default hooks that are installed on first run. All hooks are disabled by default and can be enabled via /hooks or by editing your config.

Logging Hooks

Simple hooks for logging events:

EventDescription
session-startLog when session starts
session-endLog when session ends with duration
stopLog turn completion with token/tool stats
file-modifiedLog file changes (filtered to src/**/* and lib/**/*)

Sound Alert Hook

Plays a system sound when a task completes. Cross-platform support for macOS, Linux, and Windows.

{
  "event": "stop",
  "command": "~/.autohand/hooks/sound-alert.sh",
  "description": "Play sound when task completes",
  "enabled": true,
  "async": true
}

Platform support:

  • macOS: Uses afplay with system sounds (Glass.aiff for success)
  • Linux: Uses paplay, aplay, or speaker-test
  • Windows: Uses PowerShell [console]::beep()

Auto-Format Hook

Automatically formats changed files using prettier, eslint, or biome.

{
  "event": "file-modified",
  "command": "~/.autohand/hooks/auto-format.sh",
  "description": "Auto-format changed files",
  "enabled": true,
  "filter": {
    "path": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.json", "**/*.css", "**/*.md"]
  }
}

Formatter priority:

  1. Prettier (if available in project)
  2. ESLint --fix (for JS/TS files)
  3. Biome format

Slack Notification Hook

Sends a Slack notification when tasks complete. Requires SLACK_WEBHOOK_URL environment variable.

{
  "event": "stop",
  "command": "~/.autohand/hooks/slack-notify.sh",
  "description": "Send Slack notification when task completes",
  "enabled": true,
  "async": true
}

Setup:

  1. Create a Slack Incoming Webhook at https://api.slack.com/messaging/webhooks
  2. Set the environment variable:
    export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ"
    

Message includes:

  • Project name
  • Duration (human readable)
  • Tokens used
  • Tool calls count

Git Auto-Stage Hook

Automatically stages modified files to git.

{
  "event": "file-modified",
  "command": "~/.autohand/hooks/git-auto-stage.sh",
  "description": "Auto-stage modified files to git",
  "enabled": true,
  "filter": {
    "path": ["src/**/*", "lib/**/*", "tests/**/*"]
  }
}

Automatically skips:

  • .env* files
  • *.log, *.tmp, *.swp, *.bak files
  • node_modules/, .git/, dist/, build/, coverage/ directories

Security Guard Hook

Blocks dangerous commands and operations before they execute. Uses exit code 2 to block.

{
  "event": "pre-tool",
  "command": "~/.autohand/hooks/security-guard.sh",
  "description": "Block dangerous commands and operations",
  "enabled": true,
  "matcher": "^(run_command|delete_path|write_file)$"
}

Blocked commands:

  • rm -rf /, rm -rf ~, rm -rf .
  • sudo rm
  • chmod 777, chmod -R 777
  • mkfs, dd if=
  • Fork bombs
  • curl | bash, wget | sh (piped to shell)

Protected files:

  • .env, .env.local, .env.production
  • SSH keys (id_rsa, id_ed25519, *.pem, *.key)
  • Credentials (credentials.json, secrets.json, .npmrc, .pypirc)

Smart Commit Hook

Automatically runs lint, test, and creates a commit with an LLM-generated message.

{
  "event": "stop",
  "command": "~/.autohand/hooks/smart-commit.sh",
  "description": "Auto lint, test, and commit with LLM message",
  "enabled": false,
  "async": true
}

Note: This hook is disabled by default. Enable it only if you want automatic commits after each agent turn.

Enabling Built-in Hooks

Use /hooks and select "Enable/disable hooks" to toggle individual hooks:

› /hooks
? Hook action: Enable/disable hooks
? Select hook to toggle:
  ❯ [disabled] session-start - Log session start
    [disabled] sound-alert - Play sound when task completes
    [disabled] auto-format - Auto-format changed files
    [disabled] slack-notify - Send Slack notification
    [disabled] git-auto-stage - Auto-stage modified files
    [disabled] security-guard - Block dangerous operations

Or manually edit your ~/.autohand/config.json to enable specific hooks.


Best Practices

Timeout Guidelines

  • Default timeout is 5000ms (5 seconds)
  • For quick logging operations, 1000-2000ms is sufficient
  • For network operations, consider 10000-30000ms
  • For long-running operations, set async: true

Sync vs Async

  • Sync (default): Blocks agent until hook completes. Use for critical operations that must complete before continuing.
  • Async: Runs in background without blocking. Use for logging, metrics, or non-critical notifications.

Error Handling

  • Hook failures do not crash the agent
  • Errors are logged but execution continues
  • Exit code 2 blocks execution with the stderr message
  • Test hooks with the /hooks command before relying on them

Security Considerations

  • Hook commands run in your shell with your permissions
  • Be careful with hooks that receive user input (potential for injection)
  • Avoid running hooks from untrusted config files
  • Consider sanitizing environment variables in your hook scripts

Control Flow Best Practices

  • Use decision: "allow" sparingly - only for operations you're certain are safe
  • Use decision: "ask" as the default fallback
  • Use decision: "block" with exit code 2 for truly dangerous operations
  • Always provide a reason for allow/deny decisions for auditability