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.

CategoryExample eventsUse case
Sessionsession-start, session-end, pre-clearInitialize context and send wrap-up notifications.
Prompt and turnpre-prompt, stopScreen instructions and monitor token usage per turn.
Toolpre-tool, post-toolLog tool usage, gate shell commands, and validate results.
Filefile-modifiedRun linters, formatters, and related tests.
Permission and notificationpermission-request, permission-denied, notificationAutomate approval decisions and surface system alerts.
Rate limit and errorsession-error, rate-limitAlert operators and write failure logs.
Auto-modeautomode:start, automode:iteration, automode:completeTrack autonomous loops and iteration counts.
Sub-agent and teamsubagent-start, subagent-stop, task-completedCoordinate multi-agent workflows.
Review, mode, and contextreview:completed, mode-change, context:compactRecord 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.

VariableDescriptionEvents
{{file}}Path to the affected filefile-modified, permission events
{{tool}}Tool namepre-tool, post-tool, permission events
{{command}}Shell command textpre-tool and post-tool for shell tools, permission events
{{session_id}}Current session identifierAll events
{{duration}}Execution time in millisecondspost-tool, stop, session-end, subagent-stop
{{exit_code}}0 when the tool succeeded, 1 when it failedpost-tool
{{error}}Error messagesession-error, rate-limit, subagent-stop, review:failed
{{timestamp}}ISO 8601 timestampAll 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 0

Best practices

  • Keep hooks fast. Long-running work should be marked async or moved to an external script, and given a timeout that 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.