# Hook Authoring, Replay, and Troubleshooting

Runtime hooks let you intercept every tool call that Anteroom makes. A **PreToolUse** hook runs before the tool executes; a **PostToolUse** hook runs after. Hooks can allow, deny, or escalate to the approval flow.

## Quick start

```yaml
# ~/.anteroom/config.yaml
hooks:
  pre_tool:
    - id: audit-bash
      event: pre_tool
      matcher:
        tool_name: bash
      runner:
        type: command
        command: /usr/local/bin/anteroom-audit-hook
        timeout: 5
      message: Bash command blocked by audit hook
```

Reload requires a session restart — hook config is session-scoped and read once at startup.

## Hook config fields

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Stable identifier used for deduplication across config layers. |
| `event` | `pre_tool` \| `post_tool` | yes | Lifecycle event to hook. |
| `matcher.tool_name` | fnmatch pattern | no | Tool name pattern (default `*`). |
| `matcher.arguments` | `{key: value}` | no | Argument value conditions (all must match). |
| `runner.type` | `command` \| `webhook` | yes | How to invoke the hook. |
| `runner.command` | string | command only | Shell command to run. |
| `runner.url` | string | webhook only | HTTP endpoint to POST to. |
| `runner.timeout` | int (1–30) | no | Timeout in seconds (default 5). |
| `message` | string | no | Message shown when hook returns `deny` or `ask`. |

## Decision protocol

The hook's stdout (command) or response body (webhook) must be valid JSON:

```json
{"decision": "allow"}
{"decision": "deny", "message": "blocked: rm -rf is not permitted"}
{"decision": "ask", "message": "review required for this command"}
```

- **allow** — proceed normally (default on empty output, parse failure, or timeout)
- **deny** — block the tool call; `message` is shown to the user
- **ask** — route to the existing approval flow with `message` as context

Hooks fail-open: timeouts, subprocess errors, and parse failures all default to `allow`.

## Context passed to hooks

Command hooks receive context via environment variables:

| Variable | Content |
|---|---|
| `ANTEROOM_HOOK_EVENT` | `pre_tool` or `post_tool` |
| `ANTEROOM_HOOK_TOOL_NAME` | Name of the tool being called |
| `ANTEROOM_HOOK_ARGUMENTS` | JSON of the tool arguments |
| `ANTEROOM_HOOK_OUTPUT` | JSON of the tool output (post_tool only) |

Webhook hooks receive a JSON POST body:

```json
{
  "event": "pre_tool",
  "tool_name": "bash",
  "arguments": {"command": "ls -la"},
  "output": null
}
```

## Trust sources and executability

Hooks carry a `trust_source` derived from where they were declared:

| Source | Executable | Notes |
|---|---|---|
| `personal` | yes | Declared in `~/.anteroom/config.yaml` |
| `team` | yes | Declared in team config |
| `pack` | no | Pack-distributed hooks are parsed but not executed in phase 1; see [Behavior-Bearing Packs](../packs/behavior-bearing-packs.md) |

Non-executable hooks appear in `aroom hook list` and replay output, but are never dispatched at runtime.

## Developer tooling

### List configured hooks

```bash
aroom hook list
```

Shows all hooks in a table with their trust source and executability status.

### Validate hook config

```bash
aroom hook validate
```

Validates every hook in the resolved config snapshot:
- Empty `command` or `url` → error (hook skipped at runtime)
- Unknown `runner.type` → error
- `trust_source: pack` → warning (hook present but not executable)

Exits 0 if all hooks are valid, 1 if any error is found.

### Dry-run replay

```bash
aroom hook replay pre_tool --tool bash --args '{"command": "ls -la"}'
aroom hook replay post_tool --tool write_file --args '{"path": "/tmp/x"}' --output '{"result": "ok"}'
```

Replay evaluates all hooks against the provided event without executing anything for real:
- **No audit events are emitted** — replay does not write `hook.*` runtime events
- All hooks are evaluated (no short-circuit) so you can see the full decision sequence
- Non-executable pack hooks appear as `skipped (pack)` in the output
- The `Final decision` line shows what the production runtime would have returned

Replay **does** execute your hook commands and webhooks against the simulated event. Use it to test real hook scripts during development.

> **Tip:** Replay uses the resolved config snapshot — the same hook list and trust-source assignments that a live session would use. It does not re-read raw YAML; changes require a config reload before replay reflects them.

## Troubleshooting

### Hook not firing

1. Run `aroom hook list` — confirm the hook appears and shows `Executable: yes`.
2. Check the matcher: `tool_name` uses fnmatch, so `bash` matches only `bash`, while `b*` matches `bash`, `brew`, etc.
3. Check argument conditions in `matcher.arguments`: all key/value pairs must match exactly.

### Hook returns allow unexpectedly

1. Run `aroom hook replay pre_tool --tool <name>` — the `Decision` column shows the raw outcome.
2. If `Decision` shows `allow (timeout)`, increase `runner.timeout`.
3. If `Decision` shows `allow (exception)`, check that the command/URL is correct and accessible.
4. Enable debug logging: `AI_CHAT_LOG_LEVEL=DEBUG aroom chat` — hook dispatch logs warnings on errors.

### Pack hook not running

Pack-sourced hooks (`trust_source: pack`) are intentionally not executed in phase 1. See [Behavior-Bearing Packs](../packs/behavior-bearing-packs.md) for the current trust boundary and planned rollout.

### Config change not picked up

Hook config is session-scoped. Restart the session after changing `hooks:` in `config.yaml`.
