# Hooks

> Lifecycle hooks (PreToolUse, PostToolUse, SessionStart, and peers): JSON discovery, shell vs HTTP runners, deny/allow results, environment variables, and safety patterns.

- Repository: xai-org/grok-build
- GitHub: https://github.com/xai-org/grok-build
- Human docs: https://grok-wiki.com/public/docs/xai-org-grok-build-90205de50458
- Complete Markdown: https://grok-wiki.com/public/docs/xai-org-grok-build-90205de50458/llms-full.txt

## Source Files

- `crates/codegen/xai-grok-pager/docs/user-guide/10-hooks.md`
- `crates/codegen/xai-grok-hooks/src/discovery.rs`
- `crates/codegen/xai-grok-hooks/src/dispatcher.rs`
- `crates/codegen/xai-grok-hooks/src/runner`
- `crates/codegen/xai-grok-hooks/src/event.rs`
- `crates/codegen/xai-grok-shell/src/util/hooks.rs`
- `crates/codegen/xai-grok-pager/docs/custom-hooks.md`

---

---
title: "Hooks"
description: "Lifecycle hooks (PreToolUse, PostToolUse, SessionStart, and peers): JSON discovery, shell vs HTTP runners, deny/allow results, environment variables, and safety patterns."
---

Grok Build’s hooks runtime (`xai-grok-hooks`) discovers JSON hook definitions from global, project, plugin, and custom paths, indexes them by lifecycle event, and runs matching handlers as either local shell/command processes or HTTPS POST requests. Only **`PreToolUse`** is blocking (explicit `allow` / `deny`); every other event is passive and fail-open. Project hooks are gated by the same folder-trust store as repo-local MCP and LSP (`~/.grok/trusted_folders.toml`).

## What hooks do

A hook is a command or HTTPS endpoint Grok invokes when a session lifecycle event fires. Typical uses:

| Goal | Event | Pattern |
|------|--------|---------|
| Block dangerous tool calls | `PreToolUse` | Matcher on tool name; emit `{"decision":"deny",...}` |
| Log / notify after tools | `PostToolUse`, `PostToolUseFailure` | Passive script or HTTP webhook |
| Session setup / teardown | `SessionStart`, `SessionEnd` | Env setup, audit lines |
| Turn and agent boundaries | `Stop`, `SubagentStart`, `SubagentStop` | Metrics, cleanup |
| Compaction | `PreCompact`, `PostCompact` | Archive or react to context shrink |

Hooks sit **before** the permission pipeline’s final tool execution for `PreToolUse` denials. They complement allow/deny rules and sandbox profiles rather than replacing them — see [Permissions and safety](/permissions-and-safety) and [Sandbox profiles](/sandbox).

## Quick start

```sh
mkdir -p ~/.grok/hooks
```

`~/.grok/hooks/session-start.json`:

```json
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "echo \"Grok session started in $(pwd)\"" }
        ]
      }
    ]
  }
}
```

Restart or start a session, then open the Hooks UI (`/hooks`, or `Ctrl+L` on non–VS Code family terminals) to confirm load. Built-in examples live under `crates/codegen/xai-grok-hooks/examples/hooks/` (`safe-shell.json`, `no-recursive-grep.json`, `session-log.json`, `tool-logger.json`).

## Discovery and merge order

Session startup and mid-session reload both go through shell discovery (`discover_hooks` / `discover_hook_source_paths`), then `load_hooks_from_sources`.

| Scope | Typical paths | Trust |
|-------|----------------|-------|
| Global | `~/.grok/hooks/*.json` | Always trusted |
| Global (compat) | `~/.claude/settings.json`, `settings.local.json` | Always trusted when scanned |
| Global (compat) | `~/.cursor/hooks.json` | Always trusted when scanned |
| Global (custom) | Paths listed in `~/.grok/hooks-paths` (one path per line) | Always trusted |
| Project | `<git-root>/.grok/hooks/*.json` | Requires folder trust |
| Project (compat) | `<git-root>/.claude/settings.json` (+ local), `.cursor/hooks.json` | Requires folder trust |
| Plugin | Bundled with installed plugins | Per-plugin trust / install model |

**Merge rules:**

- Sources are **additive**. Global specs load first, then project; names are prefixed `global/…` or `project/…`.
- Dedup key: `(event, command_raw, url_raw, configured_matcher)`. Same content + matcher from a later source is skipped; earlier (global) wins.
- Claude/Cursor vendor trees are on by default. Disable with `[compat.claude] hooks = false` or `[compat.cursor] hooks = false` (or matching env) in config — see [Configure Grok](/configure-grok) and [Configuration reference](/configuration-reference).
- After a Claude import mark, live scanning of `.claude/settings*.json` is skipped; native `~/.grok/hooks/` still loads.
- Project sources are omitted entirely when the workspace is untrusted (`include_project: false`).

### Folder trust

`/hooks-trust` or `--trust` writes `~/.grok/trusted_folders.toml` and unlocks **hooks, MCP, and LSP** for that folder (and subdirectories). `/hooks-untrust` revokes. Disabling folder-trust globally (`GROK_FOLDER_TRUST=0` or `[folder_trust] enabled = false`) also ungates project hooks. Legacy `trusted-hook-projects` files are migration-only.

## Events

Config keys accept PascalCase (`PreToolUse`), snake_case (`pre_tool_use`), and Cursor-style camelCase / operation names. Wire JSON on stdin uses **snake_case** event names (e.g. `pre_tool_use`).

| Event | When | Blocking? |
|-------|------|-----------|
| `SessionStart` | Session starts | No |
| `UserPromptSubmit` | User submits a prompt | No |
| `PreToolUse` | Tool about to run | **Yes** — can deny |
| `PostToolUse` | Tool completed successfully | No |
| `PostToolUseFailure` | Tool threw / failed | No |
| `PermissionDenied` | Permission system denied the call | No |
| `Stop` | Agent turn ends (ok / cancel / error) | No |
| `StopFailure` | Turn ends on API error | No |
| `Notification` | Agent notification (permission prompt, idle, …) | No |
| `SubagentStart` / `SubagentStop` | Subagent spawn / finish | No |
| `SubagentEnd` | Alias of `SubagentStop` | No |
| `PreCompact` / `PostCompact` | Before / after compaction | No |
| `SessionEnd` | Session ends | No |

Unrecognized top-level event keys in shared Claude/Cursor files are skipped so the rest of the file still loads.

### Cursor event name map

| Cursor name | Maps to |
|-------------|---------|
| `sessionStart`, `sessionEnd` | `SessionStart`, `SessionEnd` |
| `preToolUse`, `postToolUse`, `postToolUseFailure` | `PreToolUse`, `PostToolUse`, `PostToolUseFailure` |
| `beforeShellExecution`, `beforeMCPExecution`, `beforeReadFile` | `PreToolUse` |
| `afterShellExecution`, `afterMCPExecution`, `afterFileEdit`, `afterAgentResponse`, `afterAgentThought` | `PostToolUse` |
| `beforeSubmitPrompt` | `UserPromptSubmit` |
| `subagentStart`, `subagentStop` | `SubagentStart`, `SubagentStop` |
| `preCompact`, `stop` | `PreCompact`, `Stop` |

Per-operation Cursor hooks become generic tool events; filter with `matcher` or the envelope’s `toolName`.

## JSON format

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bin/safety-check.sh", "timeout": 10 }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "bin/log-activity.sh" }
        ]
      }
    ]
  }
}
```

| Field | Role |
|-------|------|
| Event key | One of the events above |
| `matcher` | Optional filter (see below). Empty / omitted / `"*"` → match all |
| `type` | `"command"` or `"http"` |
| `command` | Executable path (relative to the JSON file) or shell one-liner |
| `url` | HTTPS endpoint for HTTP handlers |
| `timeout` | Seconds (default **5** → `timeout_ms` 5000) |
| `env` | Extra string env vars for the child (values must be JSON strings) |

Unsupported handler types fail at parse/run with a clear error; they do not block the session.

## Matchers

Matchers apply to:

- Tool events: `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied` — tested against **resolved tool name**
- `Notification` — tested against `notificationType`

**Lifecycle events** that reject a matcher at load: `SessionStart`, `SessionEnd`, `Stop`, `UserPromptSubmit`. Other non-tool events ignore matchers.

### Matching rules

1. Empty pattern or `"*"` → match everything.
2. **Simple form** (only `[A-Za-z0-9_|]`): exact match per `|`-term, with Claude→Grok alias expansion (e.g. `Bash` also matches `run_terminal_command`).
3. Otherwise: **unanchored regex**, also tried against external alias names (so `^Bash$` still hits Grok’s shell tool).

MCP tools dispatched through the internal meta-tool appear as the qualified `server__tool` name (e.g. `linear__save_issue`), not the dispatcher name — match the qualified name.

Common aliases in matchers: `Bash` → `run_terminal_command`; `Read` → `read_file`; `Edit` / `Write` / `MultiEdit` → `search_replace`; `Grep` → `grep`; `Glob` / `ListDir` → `list_dir`; `WebSearch` → `web_search`; `Task` → `spawn_subagent`.

**Not env-expanded:** `matcher` is never substituted (`$` is a regex anchor). Generate dynamic matchers at write time.

## Command runner

For `type: "command"`:

1. Envelope JSON is written to **stdin**.
2. If `command` contains shell metacharacters (space, `|`, `&`, `;`, redirects, `$`) or starts with `~`, the runner uses `sh -c`; otherwise it direct-execs, resolving relative paths against the hook JSON’s directory.
3. Unresolved bare `$VAR` / `${VAR}` (no parameter-expansion modifier) refuse to spawn with a clear “required env var(s) not set” error (fail-open for the tool).
4. Timeout kills the process; stdout/stderr capture is capped (~64 KB).
5. `GROK_HOOK_DEBUG=1` enables trace logging of stdin payload sizes.

Default timeout: **5 seconds**.

### Blocking output (`PreToolUse`)

Stdout JSON:

```json
{"decision": "allow"}
```

```json
{"decision": "deny", "reason": "Unsafe command detected"}
```

| Exit code | Meaning |
|-----------|---------|
| `0` | Success / allow (if no deny JSON) |
| `2` | Explicit deny (blocking hooks) |
| Other | Fail-open for tool policy (logged in scrollback) |

JSON `{"decision":"deny",...}` **wins over exit code** (including exit 0). Passive-open applies to timeouts, crashes, missing commands, malformed output, and spawn refusals. **Only an explicit deny decision blocks the tool.**

Passive events ignore decision JSON; prefer exit 0.

### Example: safe shell guard

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bin/safe-shell-guard.sh", "timeout": 5 }
        ]
      }
    ]
  }
}
```

The example script reads stdin, pattern-matches destructive shell lines (`rm -rf /`, `mkfs`, …), prints deny JSON + exit `2` on hit, otherwise allow + exit `0`.

## HTTP runner

```json
{ "type": "http", "url": "https://hooks.example.com/grok-event", "timeout": 15 }
```

- POSTs the full event envelope as JSON.
- Same allow/deny JSON body for blocking hooks.
- **SSRF controls (CWE-918):** only **`https://`**; DNS-resolved addresses must not be private RFC1918, link-local/metadata (`169.254/16`), CGNAT (`100.64/10`), unspecified, or IPv6 ULA/link-local. **Loopback is allowed** for local dev (`127.0.0.0/8`, `::1`).
- URL is env-expanded at request time (after plugin env injection), then validated — SSRF checks the expanded URL.
- Display surfaces prefer pre-expansion `url_raw` so secrets in `${TOKEN}` do not leak into UI logs.

## Event envelope (stdin / POST body)

Common fields (camelCase wire format):

| Field | Description |
|-------|-------------|
| `hookEventName` | Snake_case event id |
| `sessionId` | Current session id |
| `cwd` | Working directory |
| `workspaceRoot` | Workspace root |
| `timestamp` | ISO-style timestamp |
| `transcriptPath`, `clientIdentifier`, `promptId` | Optional context |

Event-specific fields are flattened into the same object. Tool events include:

- `toolName`, `toolUseId`, `toolInput`, `toolInputTruncated`
- `PostToolUse` also: `toolResult`, `toolResultTruncated`, `durationMs`, `isBackgrounded`, optional `subagentType`
- Large `toolInput` / `toolResult` payloads are truncated at **128 KB** serialized size (`MAX_PAYLOAD_SIZE`) with `*Truncated: true`

`PreToolUse` example:

```json
{
  "hookEventName": "pre_tool_use",
  "sessionId": "abc-123",
  "cwd": "/Users/you/project",
  "workspaceRoot": "/Users/you/project",
  "toolName": "run_terminal_command",
  "toolUseId": "...",
  "toolInput": { "command": "npm test" },
  "toolInputTruncated": false,
  "timestamp": "2026-04-14T12:00:00Z"
}
```

## Environment variables

### Runner-injected (always; reserved)

| Variable | Meaning |
|----------|---------|
| `GROK_HOOK_EVENT` | Snake_case event name |
| `GROK_HOOK_NAME` | Configured hook name (includes source/plugin prefix) |
| `GROK_SESSION_ID` | Session id |
| `GROK_WORKSPACE_ROOT` | Workspace root |
| `CLAUDE_PROJECT_DIR` | Same as workspace root (Claude Code compat) |

User `env` entries for these keys are **stripped at load** (warning logged). The runner re-injects real values **after** `extra_env` so they always win.

### Plugin hooks

| Variable | Meaning |
|----------|---------|
| `GROK_PLUGIN_ROOT` / `CLAUDE_PLUGIN_ROOT` | Plugin install directory |
| `GROK_PLUGIN_DATA` / `CLAUDE_PLUGIN_DATA` | Writable plugin data directory |

Plugin adapter values override user `env` for those keys. See [Plugins](/plugins).

### Handler `env` map

```json
{
  "type": "command",
  "command": "bin/check.sh",
  "env": {
    "MY_API_TOKEN": "secret-here",
    "LOG_LEVEL": "debug"
  }
}
```

Values must be strings. **`env` map values are not expanded** (`"BAR": "${HOME}/x"` injects the literal). Use expansion on `command` / `url` instead.

### `$VAR` / `${VAR}` in `command` and `url`

Load-time expansion order: handler `env` map, then process environment. Unset plain references stay literal for the shell path. POSIX modifiers (`${VAR:-default}`, `${VAR%pat}`, …) are **not** expanded at load time — left for `sh -c`. HTTP `url` is re-expanded at request time; command paths that direct-exec are **not** re-expanded mid-session.

## Dispatch semantics

- Hooks for an event run **sequentially** in registry order.
- Disabled hooks (`spec.enabled == false` or listed in `~/.grok/disabled-hooks`) are skipped.
- `PreToolUse`: first **explicit deny** short-circuits; later hooks do not run. Any earlier allow + later deny → **deny** (stricter filter wins).
- Non-blocking dispatch never denies; failures are recorded for scrollback only.
- Fail-open is intentional: timeout/crash of a security hook does **not** block the tool — write robust scripts that always emit an explicit deny when policy requires it.

## Manage hooks in the TUI

| Action | How |
|--------|-----|
| Open Hooks tab | `/hooks` (any terminal); `Ctrl+L` opens Extensions on non–VS Code family |
| Reload from disk | `r` in Hooks tab |
| Add custom path | `a` or `/hooks-add <path>` |
| Remove | `x` or `/hooks-remove <path>` |
| Enable / disable | `Space` (persists via `~/.grok/disabled-hooks`) |
| Filter | `f` — All / Enabled / Disabled |
| List (headless-ish) | `/hooks-list` |
| Trust / untrust project | `/hooks-trust`, `/hooks-untrust` |

Groups: **Global**, **Project**, **Plugin**, **Custom**. Hook run outcomes appear as scrollback annotations when the plugins UI is enabled (default).

Related slash surfaces: [Slash commands](/slash-commands).

## Safety patterns

1. **Fail-open** — never rely on exit-non-zero alone; emit `{"decision":"deny","reason":"..."}` to block.
2. **Keep hooks short** — they run on the tool path; long work should background or use HTTP async consumers.
3. **Trust gates** — do not commit secrets; project hooks need explicit folder trust.
4. **HTTP** — only HTTPS public (or loopback) endpoints; envelope includes session and tool data.
5. **Relative scripts** — `bin/…` next to the JSON file is portable across clones.
6. **Matchers** — prefer simple names or careful regex; invalid regex falls back to match-all at recompile (fail-open).
7. **Debug** — `RUST_LOG=debug GROK_LOG_FILE=/tmp/grok.log grok` and/or `GROK_HOOK_DEBUG=1`.

### Interaction with other safety layers

| Layer | Role vs hooks |
|-------|----------------|
| Permission rules / modes | Separate allow/deny/ask pipeline; hooks can deny earlier on `PreToolUse` |
| Safe-bash / remembered grants | Still apply after hooks allow |
| Sandbox profiles | OS isolation of the tool process; hooks themselves run as the user outside that sandbox |

## Troubleshooting

| Symptom | Check |
|---------|--------|
| Hook never runs | `/hooks` loaded? Matcher vs real `toolName`? Disabled list? |
| Project hooks missing | Folder trust (`/hooks-trust` / `--trust`) |
| Script not found | Path relative to JSON; `chmod +x`; metacharacters force `sh -c` |
| Deny not blocking | Must be `PreToolUse` + stdout JSON deny (or exit 2 with deny path); failures fail-open |
| HTTP hook fails | HTTPS only; not private IP; DNS resolution; timeout |
| Compat file ignored | `[compat.*.hooks]`, Claude import mark, or trust |

## Implementation map

| Area | Location |
|------|----------|
| Events, envelope, truncation | `crates/codegen/xai-grok-hooks/src/event.rs` |
| Discovery / registry | `…/discovery.rs` |
| Parse / HookSpec / defaults | `…/config.rs` |
| Dispatch allow/deny | `…/dispatcher.rs` |
| Matchers + aliases | `…/matcher.rs` |
| Command / HTTP runners | `…/runner/command.rs`, `…/runner/http.rs` |
| Decisions / run results | `…/result.rs` |
| Disable list / legacy trust | `…/trust.rs` |
| Path discovery + trust gate | `crates/codegen/xai-grok-shell/src/util/hooks.rs` |
| User-facing guides | `crates/codegen/xai-grok-pager/docs/user-guide/10-hooks.md`, `custom-hooks.md` |

## Next

<CardGroup cols={2}>
  <Card title="Permissions and safety" href="/permissions-and-safety">
    Tool authorization pipeline, permission modes, and how PreToolUse denials interact with rules.
  </Card>
  <Card title="Plugins" href="/plugins">
    Bundle hooks with skills and MCP; GROK_PLUGIN_ROOT and GROK_PLUGIN_DATA.
  </Card>
  <Card title="Configure Grok" href="/configure-grok">
    config.toml precedence, compat vendor toggles, and inspect resolved settings.
  </Card>
  <Card title="Slash commands" href="/slash-commands">
    /hooks modal, trust, and related interactive commands.
  </Card>
</CardGroup>

## Related pages

- [Sandbox profiles](/sandbox) — OS isolation after hooks allow a tool
- [Subagents and personas](/subagents) — SubagentStart / SubagentStop payloads
- [MCP servers](/mcp-servers) — Match qualified `server__tool` names
- [Headless mode](/headless-mode) — CI runs still load hooks under the same discovery rules
- [Troubleshooting](/troubleshooting) — Logs, terminal, and diagnostics
