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

# Hooks System

> Automate actions with lifecycle hooks for events like tool execution, session start/end, and prompt submission

The ChatCLI **Hooks System** lets you execute automatic actions in response to application lifecycle events. With hooks, you can auto-format code after edits, send notifications, block dangerous commands, log audit trails, and much more.

<Info>
  Hooks are **additive**: global and workspace configurations are merged. Workspace hooks complement global ones -- they never replace them.
</Info>

***

## Configuration

Hooks are defined in JSON files at two levels:

| Level | File | Scope |
| :- | :- | :- |
| **Global** | `~/.chatcli/hooks.json` | All projects and sessions |
| **Workspace** | `.chatcli/hooks.json` | Current project only |

<Tip>
  Workspace hooks are **additive** -- they combine with global hooks. If the same event has hooks at both levels, all are executed (global first, then workspace).
</Tip>

### File Structure

```json theme={"system"}
{
  "hooks": [
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "gofmt -w {{.FilePath}}",
      "toolPattern": "write|patch",
      "description": "Auto-format Go files after edits"
    },
    {
      "event": "SessionEnd",
      "type": "http",
      "url": "https://hooks.example.com/chatcli",
      "description": "Notify team of session end"
    }
  ]
}
```

***

## Available Events

ChatCLI emits 8 lifecycle events that hooks can bind to:

| Event | When it fires | Blocking |
| :- | :- | :- |
| `SessionStart` | When a new ChatCLI session starts | No |
| `SessionEnd` | When the session ends (exit/quit) | No |
| `UserPromptSubmit` | When the user submits a prompt | No |
| `PreToolUse` | **Before** executing a tool | **Yes** |
| `PostToolUse` | **After** a tool executes successfully | No |
| `PostToolUseFailure` | When a tool fails | No |
| `PreCompact` | **Before** any history compaction — automatic (chat, agent/coder, one-shot), `/compact`, or context-overflow recovery | No |
| `PostCompact` | After the compacted history is in place | No |

Compaction events carry a `trigger` field (`auto`, `manual`, `recovery`) and, on `PostCompact`, an `outcome` (`applied` when the history changed, `skipped` when nothing changed: no-op, rejected summary, failure) in the JSON payload and in `CHATCLI_HOOK_TRIGGER` / `CHATCLI_HOOK_OUTCOME`; every `PreCompact` is paired with exactly one `PostCompact`, so a hook can snapshot the transcript before an automatic rewrite, log manual `/compact` runs separately, or alert on overflow recoveries. `PreCompact` runs synchronously before the history changes; `PostCompact` runs detached from the turn.

<Warning>
  The `PreToolUse` event is **blocking**: if the hook returns exit code 2, the tool execution is **blocked**. This allows you to create guardrails that prevent dangerous operations.
</Warning>

***

## Hook Types

<Tabs>
  <Tab title="Command (Shell)">
    Executes a shell command on the operating system. The command has access to environment variables with event context.

    ```json theme={"system"}
    {
      "event": "PostToolUse",
      "type": "command",
      "command": "gofmt -w {{.FilePath}}",
      "toolPattern": "write|patch",
      "description": "Auto-format Go files after write/patch"
    }
    ```

    **Exit codes**:

    * `0` -- Success (execution continues normally)
    * `1` -- Error (logged, but does not block)
    * `2` -- **Blocks the operation** (only for `PreToolUse`)

    <Info>The command is executed via `sh -c` on Linux/macOS and `cmd /c` on Windows.</Info>
  </Tab>

  <Tab title="HTTP (Webhook)">
    Sends an HTTP POST request to the specified URL. The request body contains a JSON payload with the event data.

    ```json theme={"system"}
    {
      "event": "SessionEnd",
      "type": "http",
      "url": "https://hooks.example.com/chatcli/events",
      "description": "Send session end notification"
    }
    ```

    **JSON payload sent**:

    ```json theme={"system"}
    {
      "event": "SessionEnd",
      "timestamp": "2026-03-29T14:30:00Z",
      "session_id": "abc123",
      "tool": "",
      "data": {
        "duration": "45m12s",
        "messages": 32
      }
    }
    ```

    <Tip>Webhooks are fired asynchronously and do not block ChatCLI execution.</Tip>
  </Tab>
</Tabs>

***

## ToolPattern -- Tool Filtering

The `toolPattern` field lets you filter which tools trigger the hook. It accepts **glob** patterns:

| Pattern | Matched tools |
| :- | :- |
| `write` | Only `write` |
| `write\|patch` | `write` or `patch` |
| `exec*` | `exec`, `exec_background`, etc. |
| `mcp_*` | All MCP tools |
| `*` | All tools (default if omitted) |

```json theme={"system"}
{
  "event": "PreToolUse",
  "type": "command",
  "command": "echo 'BLOCKED: exec not allowed' && exit 2",
  "toolPattern": "exec*",
  "description": "Block all exec commands"
}
```

***

## Environment Variables

Command hooks receive environment variables with event context:

| Variable | Description | Example |
| :- | :- | :- |
| `CHATCLI_HOOK_EVENT` | Name of the event that triggered the hook | `PostToolUse` |
| `CHATCLI_HOOK_TOOL` | Tool name (if applicable) | `write` |
| `CHATCLI_HOOK_SESSION` | Current session ID | `session_abc123` |
| `CHATCLI_HOOK_TRIGGER` | Compaction trigger (`PreCompact`/`PostCompact` only) | `auto` |
| `CHATCLI_HOOK_OUTCOME` | Compaction outcome (`PostCompact` only): `applied` or `skipped` | `applied` |

Everything else about the event — tool arguments and output, the user prompt, the working directory, the error message — arrives as the JSON payload on **stdin** (`toolArgs`, `toolOutput`, `userPrompt`, `workingDir`, `error`, `trigger`); read it with `jq` when a hook needs more than the four variables above.

***

## Turning Hooks Off

| Variable | Effect |
| :- | :- |
| `CHATCLI_HOOKS_ENABLED=false` | Stops every hook in the process (`0`, `off` and `no` work too). Unset means on. `/config integrations` shows the setting |

Hooks also **never fire inside a [`chatcli eval`](/agents/harness/evals) run**, whatever that variable says. Each eval candidate is a real chatcli process, so without this rule a hook that runs an eval would fire again inside every candidate the eval spawns, without end, and a `UserPromptSubmit` hook that injects context would change the result being measured. The eval harness also sets `CHATCLI_HOOKS_ENABLED=false` on each candidate.

***

## Complete Examples

<Tabs>
  <Tab title="Auto-Format">
    Auto-format Go files after any edit:

    ```json theme={"system"}
    {
      "hooks": [
        {
          "event": "PostToolUse",
          "type": "command",
          "command": "f=$(jq -r '.toolArgs' | grep -oE '[^ \"]+\\.go' | head -1); if [ -n \"$f\" ]; then gofmt -w \"$f\"; fi",
          "toolPattern": "write|patch",
          "description": "Auto-format Go files"
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Notifications">
    Send a Slack notification at the end of each session:

    ```json theme={"system"}
    {
      "hooks": [
        {
          "event": "SessionEnd",
          "type": "http",
          "url": "https://hooks.slack.com/services/T00/B00/xxx",
          "description": "Notify Slack on session end"
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Block Commands">
    Block `rm -rf` and `DROP TABLE` in production:

    ```json theme={"system"}
    {
      "hooks": [
        {
          "event": "PreToolUse",
          "type": "command",
          "command": "if jq -r '.toolArgs' | grep -qE 'rm -rf|DROP TABLE'; then echo 'BLOCKED: dangerous command' >&2; exit 2; fi",
          "toolPattern": "exec*",
          "description": "Block dangerous shell commands"
        }
      ]
    }
    ```
  </Tab>

  <Tab title="Audit">
    Log all tool executions:

    ```json theme={"system"}
    {
      "hooks": [
        {
          "event": "PostToolUse",
          "type": "command",
          "command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) $CHATCLI_HOOK_EVENT $CHATCLI_HOOK_TOOL $CHATCLI_HOOK_TRIGGER\" >> ~/.chatcli/audit.log",
          "description": "Audit log for all tool executions"
        }
      ]
    }
    ```
  </Tab>
</Tabs>

***

## Use Cases

<CardGroup cols={2}>
  <Card title="Auto-Format" icon="wand-magic-sparkles">
    Run formatters (gofmt, prettier, black) automatically after file edits.
  </Card>

  <Card title="Notifications" icon="bell">
    Send alerts to Slack, Discord, or email at the end of sessions or on errors.
  </Card>

  <Card title="Guardrails" icon="shield-halved">
    Block dangerous commands (rm -rf, DROP TABLE, force push) with PreToolUse.
  </Card>

  <Card title="Audit" icon="clipboard-list">
    Log all agent actions to files for compliance.
  </Card>

  <Card title="Auto-Test" icon="flask-vial">
    Run tests automatically after every code edit.
  </Card>

  <Card title="Linting" icon="broom">
    Run linters (golangci-lint, eslint) after every write/patch.
  </Card>
</CardGroup>

***

## Running an Eval from a Hook

A hook can run an [eval suite](/agents/harness/evals), for example a cheap check when you leave the REPL that warns you only on a regression. An eval takes seconds to minutes and `SessionEnd` runs while ChatCLI shuts down, under the hook timeout (10 s by default). The hook therefore starts the eval **in the background** and returns at once, with its output going to a file so nothing waits on it.

```json ~/.chatcli/hooks.json theme={"system"}
{
  "hooks": [
    {
      "name": "eval-on-exit",
      "event": "SessionEnd",
      "type": "command",
      "timeout": 5000,
      "command": "sh ~/.chatcli/hooks/eval-smoke.sh"
    }
  ]
}
```

```sh ~/.chatcli/hooks/eval-smoke.sh theme={"system"}
#!/bin/sh
# One run at a time.
mkdir /tmp/chatcli-eval.lock 2>/dev/null || exit 0
cd ~/my-project || exit 0
nohup sh -c '
  chatcli eval run evals/ --filter tag:smoke --baseline runs/main.json \
    --out runs/last.json --max-cost 0.50 --quiet > runs/last.log 2>&1
  [ $? -eq 3 ] && osascript -e "display notification \"Eval regression\" with title \"chatcli\""
  rmdir /tmp/chatcli-eval.lock
' >/dev/null 2>&1 &
```

Exit code `3` means a regression against the baseline. Replace `osascript` (macOS) with `notify-send` on Linux, or with a curl to a chat webhook. To run the eval only when you change something that shapes behavior, such as a skill or a slash command, use `PostToolUse` with `"toolPattern": "@coder"` and have the script check `toolArgs` on stdin for `.chatcli/skills` or `.chatcli/commands`. You do not need a recursion guard: hooks do not fire inside the eval's own runs.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Coder Security" icon="shield-halved" href="/coder/coder-security">
    Security policies and approval for coder operations.
  </Card>

  <Card title="Security" icon="shield-halved" href="/security/overview">
    Understand the ChatCLI security model.
  </Card>

  <Card title="Compact UI" icon="minimize" href="/usage/compact-ui">
    Minimalist display mode for coder mode.
  </Card>

  <Card title="Coder Mode" icon="code" href="/usage/coder-mode">
    The full engineering cycle with integrated hooks.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.