Working with Autohand
Hooks and Events
Hooks and events let you extend Autohand Code without changing the agent. Run shell commands when files change, tools run, sessions end, or errors occur. Use them to enforce quality gates, notify your team, and build audit trails.
Rate-limit notifications and session retries
CLI builds containing the rate-limit update (revision d0c05a6f or later with these changes) emit a dedicated rate-limit hook when a provider reports a rate or quota limit. The ordinary session-error notification still fires, but the session-retry loop does not automatically retry that failure.
In JSON-RPC mode, the notification method is autohand.hook.rateLimit. Updated TypeScript SDK builds expose the validated payload as hook_rate_limit, including optional provider, model, HTTP status, and retry timing. See the SDK rate-limit event contract before implementing application retries.
What hooks and events are
Hooks are shell commands that Autohand Code runs in response to events. Events are discrete moments in the agent lifecycle, such as a file change, a tool call, or the end of a session.
You configure hooks in JSON as an array of definitions, each naming an event and a command. When an event fires, the agent selects the definitions whose filter or matcher match, substitutes template variables, sets the $HOOK_* environment variables, writes the event context to stdin, and runs the command. Synchronous hooks block until they finish; hooks marked async run in the background.
Event categories
Autohand Code emits events across the whole session. The most common categories are listed below; the Hooks Reference lists every event.
| Category | Example events | Use case |
|---|---|---|
| Session | session-start, session-end, pre-clear | Initialize context and send wrap-up notifications. |
| Prompt and turn | pre-prompt, stop | Screen instructions and monitor token usage per turn. |
| Tool | pre-tool, post-tool | Log tool usage, gate shell commands, and validate results. |
| File | file-modified | Run linters, formatters, and related tests. |
| Permission and notification | permission-request, permission-denied, notification | Automate approval decisions and surface system alerts. |
| Rate limit and error | session-error, rate-limit | Alert operators and write failure logs. |
| Auto-mode | automode:start, automode:iteration, automode:complete | Track autonomous loops and iteration counts. |
| Sub-agent and team | subagent-start, subagent-stop, task-completed | Coordinate multi-agent workflows. |
| Review, mode, and context | review:completed, mode-change, context:compact | Record review outcomes and context lifecycle changes. |
The original on_*, before_*, and after_* names (for example on_file_change and before_tool_call) remain supported and are rewritten onto these events; see Legacy event names in the Hooks Reference for the full mapping.
Tip: Run /hooks in a session to browse every event with its installed and active hook counts, or /hooks list to print the table.
Configure hooks
Hooks live under the hooks key of ~/.autohand/config.json (or the file passed with --config). Each entry in hooks.hooks names an event, a command, and optional filters.
{
"hooks": {
"enabled": true,
"hooks": [
{
"event": "file-modified",
"command": "eslint {{file}} --fix && prettier --write {{file}}",
"description": "Lint and format changed files",
"filter": {
"path": [
"**/*.ts",
"**/*.tsx"
]
}
},
{
"event": "post-tool",
"command": "echo \"$(date -u +%FT%TZ) $HOOK_TOOL ${HOOK_DURATION}ms\" >> ~/.autohand/audit.log",
"description": "Audit tool calls"
},
{
"event": "session-end",
"command": "./scripts/notify.sh \"$HOOK_SESSION_ID\" \"$HOOK_DURATION\"",
"description": "Notify when the session ends"
}
]
}
}Definition options
Every definition accepts description, enabled, timeout (milliseconds, default 5000), async, a regex matcher, and a filter with tool and path arrays. Use them to scope a hook and to keep slow work off the agent's critical path.
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "npm test -- --findRelatedTests {{file}}",
"filter": {
"path": [
"src/**/*"
]
},
"timeout": 30000
},
{
"event": "file-modified",
"command": "npm run build",
"filter": {
"path": [
"src/**/*"
]
},
"async": true
}
]
}
}Template variables
Double curly braces insert event data into commands before they run. Values that are not a single plain word are single-quoted for the shell, and unknown variables become empty strings.
| Variable | Description | Events |
|---|---|---|
{{file}} | Path to the affected file | file-modified, permission events |
{{tool}} | Tool name | pre-tool, post-tool, permission events |
{{command}} | Shell command text | pre-tool and post-tool for shell tools, permission events |
{{session_id}} | Current session identifier | All events |
{{duration}} | Execution time in milliseconds | post-tool, stop, session-end, subagent-stop |
{{exit_code}} | 0 when the tool succeeded, 1 when it failed | post-tool |
{{error}} | Error message | session-error, rate-limit, subagent-stop, review:failed |
{{timestamp}} | ISO 8601 timestamp | All events |
Tip: The same context is available as $HOOK_* environment variables and as JSON on stdin. Prefix secrets and URLs with $ instead of embedding them in config.json. See the environment variable reference and the template variable table.
Common patterns
Use the same file-modified event across stacks, scope it with a filter.path glob, then tune the commands for the project language. The curl tab shows the notification pattern you can reuse for Slack, Discord, or any webhook receiver.
JavaScript
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "npx eslint {{file}} --fix && npx prettier --write {{file}}",
"description": "Lint and format JavaScript",
"filter": {
"path": [
"**/*.js",
"**/*.jsx"
]
}
}
]
}
}
TypeScript
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "npx eslint {{file}} --fix && npx prettier --write {{file}}",
"description": "Lint and format TypeScript",
"filter": {
"path": [
"**/*.ts",
"**/*.tsx"
]
}
},
{
"event": "file-modified",
"command": "npm test -- --findRelatedTests {{file}}",
"description": "Run related tests",
"filter": {
"path": [
"**/*.ts",
"**/*.tsx"
]
},
"timeout": 30000
}
]
}
}
Python
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "ruff check --fix {{file}} && ruff format {{file}}",
"description": "Lint and format Python",
"filter": {
"path": [
"**/*.py"
]
}
},
{
"event": "file-modified",
"command": "pytest -q",
"description": "Run tests",
"filter": {
"path": [
"**/*.py"
]
},
"timeout": 60000
}
]
}
}
Go
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "gofmt -w {{file}} && go test ./...",
"description": "Format and test Go",
"filter": {
"path": [
"**/*.go"
]
},
"timeout": 60000
}
]
}
}
Java
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "./mvnw -q test",
"description": "Run Maven tests",
"filter": {
"path": [
"**/*.java"
]
},
"timeout": 120000
}
]
}
}
Swift
{
"hooks": {
"hooks": [
{
"event": "file-modified",
"command": "swift test",
"description": "Run Swift tests",
"filter": {
"path": [
"**/*.swift"
]
},
"timeout": 120000
}
]
}
}
curl
{
"hooks": {
"hooks": [
{
"event": "session-end",
"command": "curl -X POST \"$SLACK_WEBHOOK\" -H 'Content-type: application/json' -d \"{\\\"text\\\": \\\"Autohand session ended after ${HOOK_DURATION}ms\\\"}\"",
"description": "Slack notification on session end",
"async": true
}
]
}
}Block dangerous commands
A synchronous pre-tool hook sees every shell command before it runs. Print {"decision": "deny"} to reject it without prompting, or exit with code 2 to stop execution with the stderr message. Exit codes other than 0 and 2 are logged and do not block.
{
"hooks": {
"hooks": [
{
"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
exit 0Best practices
- Keep hooks fast. Long-running work should be marked
asyncor moved to an external script, and given atimeoutthat fits. - Make hooks idempotent. The agent may retry an action or emit multiple file events for one logical change.
- Exit with code 2 only when you want to block the underlying action. Blocking applies to pre-tool and pre-prompt hooks; other non-zero exits are logged and execution continues.
- Use environment variables for secrets and webhook URLs.
- Test hooks outside Autohand first by running the script with the
HOOK_*variables set and a sample JSON payload on stdin.