Before you start

Make sure you have Autohand Code installed and know where your config lives: ~/.autohand/config.json by default, or the file passed with --config. Hook definitions go in the hooks.hooks array; each names an event and a command. Every event is listed in the Hooks Reference.

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.

Step 1: lint changed files

Add a file-modified hook that runs the formatter, linter, or test command for the language used in the changed file. Scope it with a filter.path glob so it only fires for that language.

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

Step 2: run related tests

Run tests that are related to the changed file. This keeps feedback fast. Test runs usually exceed the 5-second default, so set a timeout.

{
  "hooks": {
    "hooks": [
      {
        "event": "file-modified",
        "command": "npm test -- --findRelatedTests {{file}}",
        "description": "Run related tests",
        "filter": {
          "path": [
            "src/**/*"
          ]
        },
        "timeout": 30000
      }
    ]
  }
}

Step 3: notify on session end

Send a Slack message when the session finishes. Store the webhook URL in an environment variable and mark the hook async so it never delays shutdown.

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

Step 4: block dangerous commands

Use a synchronous pre-tool hook as a guardrail. Print {"decision": "deny"} to reject the command without prompting, or exit with code 2 to stop it with the stderr message.

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

Step 5: audit tool usage

Log every tool call with a timestamp, outcome, and duration for compliance.

{
  "hooks": {
    "hooks": [
      {
        "event": "post-tool",
        "command": "echo \"$(date -u +%FT%TZ) | $HOOK_TOOL | success=$HOOK_SUCCESS | ${HOOK_DURATION}ms\" >> ~/.autohand/audit.log",
        "description": "Audit tool calls"
      }
    ]
  }
}

Next steps

Explore the Hooks Reference for the full event list, environment variables, and control-flow responses, and see Building Event-Driven Agents to trigger agents from external systems.