> ## Documentation index
> Fetch the complete documentation index at: https://haarness.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Run commands on chat events (hooks)

A hook runs a command of yours at a point in a chat: before the agent runs a tool, when you send a
message, when a turn ends. Use one to block a command you never want run, to add context to every
prompt, to approve routine requests, or to send yourself a notification. Hooks use the same format
as Claude Code's, so most hook scripts written for Claude Code work as they are.

## Add a hook

Put a `hooks` section in `~/.config/haarness/haarness.json`, or in a project's
`.haarness/haarness.json` to share it with the project:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/bin/check-command.sh", "timeout": 30 }]
      }
    ]
  }
}
```

This runs `check-command.sh` before every shell command the agent runs. Haarness picks up the
change on its own once no turn is running.

You can also create hooks without editing the file: `/hooks` → **Create hook** in the terminal UI,
or **Settings → Hooks → Add hook** in Desktop; see
[Run a command when something happens](https://haarness.ai/docs/desktop/settings.md#run-a-command-when-something-happens).

## What your command receives

The command runs through `sh -lc` in the project folder, with `HAARNESS_PROJECT_DIR` and
`CLAUDE_PROJECT_DIR` set to that folder. It gets one JSON document on standard input with
`hook_event_name`, `cwd`, the chat's `session_id` and the event's own fields, such as `tool_name`
and `tool_input` for a tool, or `prompt` for a message.

```sh
#!/bin/sh
# check-command.sh: refuse any command that pushes to main
if jq -e '.tool_input.command | test("git push.*main")' >/dev/null; then
  echo "Pushing to main is not allowed here; open a pull request." >&2
  exit 2
fi
```

## Block, allow or add context

- **Block:** exit with code 2. What the command wrote to standard error is the reason the agent
  reads. Printing `{"decision": "block", "reason": "…"}` does the same.
- **Allow or deny a tool call:** a `PreToolUse` hook prints
  `{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow"}}`
  (or `"deny"` or `"ask"`). `updatedInput` there changes the tool's input before it runs.
- **Add context:** `additionalContext` from a `UserPromptSubmit` hook reaches the model with your
  message.
- **Answer an approval:** a `PermissionRequest` hook runs just before you would be asked. It can
  answer with `"decision": {"behavior": "allow"}`, `"deny"`, or `"allow"` with `"always": true`.
  Always lasts until the server stops and is never written to disk.
- **Answer the agent's question:** a `PreToolUse` hook with the matcher `question` can hand back an
  answer, and nobody is asked. The chat shows **Answered by hook**.

Print nothing, or `{}`, to leave the decision to Haarness. Any other non-zero exit is logged and
the action goes on. A hook that runs longer than its `timeout` (60 seconds by default, at most
600) is stopped and changes nothing. Set `"async": true` for a hook that should never hold up the
chat.

A hook can approve only what you could approve with a rule: a deny rule still refuses, and
requests that always need a person, such as an edit to Haarness's own settings or a Cloud action
that spends money, still ask you.

## Events

| Event | Runs | Matcher compares |
| --- | --- | --- |
| `SessionStart` | A chat is created | `startup` |
| `UserPromptSubmit` | You send a message | — |
| `PreToolUse` | Before a tool runs | Tool name |
| `PermissionRequest` | Before you are asked to approve something | Permission name |
| `PostToolUse`, `PostToolUseFailure` | After a tool succeeds or fails | Tool name |
| `FileChanged` | After the agent writes or edits a file | Tool name |
| `Stop` | A turn completed | — |
| `TurnEnd` | After `Stop`, with the agent, model and finish reason | — |
| `StopFailure` | A turn failed for good | The error, such as `rate_limit` |
| `PostCompact` | A long chat was summarised | `manual` or `automatic` |
| `Notification` | An approval is waiting, or a turn completed | `permission_prompt` or `turn_completed` |
| `SessionEnd` | A chat was archived | `archived` |
| `ConfigChange` | Settings were reloaded | — |

A matcher is `*` (or empty) for everything, names separated by `|` such as `Bash|Edit`, or a
regular expression. Claude Code's tool names work. Events marked — run every hook. A hook whose
matcher can never match its event is marked **Never runs** in the terminal UI and Desktop.

## Before you add one

A hook runs as you, with your user's access, every time its event fires. Hooks in a project's
configuration load only while project configuration is on, so `haarness --disable-project-config`
skips them for a repository you do not trust. See
[What can a hook do?](https://haarness.ai/docs/security/trust-boundaries.md#what-can-a-hook-do).

## Related

- [Add a custom tool with an extension](https://haarness.ai/docs/cli/extensions.md), for hooks written in TypeScript
- [Permissions](https://haarness.ai/docs/security/permissions.md)
- [Configure Haarness](https://haarness.ai/docs/cli/configuration.md)
