# Custom Hooks Guide Hooks let you run custom scripts or HTTP requests at key moments during a Grok session β€” for example, before or after a tool runs, when a session starts or ends, or when the agent sends a notification. They are perfect for automation, safety checks, logging, notifications, and integrating with your own tools. ## Why Use Hooks? Common use cases: - **Safety guards**: Block dangerous commands like `rm -rf /` before they execute. - **Audit logging**: Record every tool use or session to a file or external service. - **Notifications**: Send a Slack/Discord message when a long-running task finishes. - **Auto-formatting**: Run `cargo fmt` or `prettier` automatically after edits. - **Environment setup**: Export secrets or set variables at session start. - **Custom workflows**: Trigger builds, tests, or deployments on specific events. ## Quick Start 1. Create the hooks directory: ```sh mkdir -p ~/.grok/hooks ``` 2. Create a simple hook file, e.g. `~/.grok/hooks/session-start.json`: ```json { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo \"πŸš€ Grok session started in $(pwd)\"" } ] } ] } } ``` 3. Start (or restart) a Grok session. The hook runs automatically on `SessionStart`. Try it: press `Ctrl+L` on non–VS Code family (or run `/hooks` anywhere β€” preferred on VS Code / Cursor / Windsurf / Zed) and check the Hooks tab to confirm it's loaded. ## Hook Locations Hooks are discovered from several places (all are merged): | Scope | Path | Trusted? | Notes | |-----------|-----------------------------------|--------------|-------| | Global | `~/.grok/hooks/*.json` | Always | Best for personal hooks | | Global | `~/.claude/settings.json` | Always | Claude Code compatibility | | Project | `/.grok/hooks/*.json` | Requires trust | Per-repo automation | | Project | `/.claude/settings.json` | Requires trust | Claude compatibility | | Config | `config.toml`, `managed_config.toml`, `requirements.toml` | Always | Hooks shipped in your (or your organization's) config | | Plugin | Bundled inside installed plugins | Per-plugin | Shared team hooks | Config-file hooks use the same schema in TOML form; see the [Hooks user guide](user-guide/10-hooks.md#hooks-in-config-files) for details. **Trusting a project**: Open the hooks modal (`Ctrl+L` on non–VS Code family, or `/hooks` on any terminal including VS Code family) or run `/hooks-trust` (the same folder-trust gate as `--trust`, recorded in `~/.grok/trusted_folders.toml`) the first time you open a project with hooks. This prevents untrusted repos from running arbitrary code. ## The Hook JSON Format Each `.json` file can define multiple hooks: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bin/safety-check.sh", "timeout": 10 } ] } ], "PostToolUse": [ { "hooks": [ { "type": "command", "command": "bin/log-activity.sh" } ] } ] } } ``` Key fields: - **Event name** (top-level key): `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `Notification`, `SessionEnd`, etc. - **matcher** (optional): Regex tested against the event's match value β€” the tool name on tool events, and per-event values elsewhere (see the user guide's Hooks chapter). Empty = match everything. - **type**: `"command"` (run a script or shell one-liner) or `"http"` (POST the event to a URL). - **command**: Path to executable (relative to the JSON file) or inline shell command. - **timeout**: Seconds before killing the hook (default: 5, or 600 for `Stop`/`SubagentStop` gates). Hooks fail open on timeout. **Tool name aliases**: Claude-style names like `Bash`, `Edit`, `Read` automatically match Grok's internal names (`run_terminal_cmd`, `search_replace`, `read_file`). ## Writing Hook Scripts ### Input The full event is sent as JSON on **stdin**. Example for a `PreToolUse` hook: ```json { "hookEventName": "pre_tool_use", "sessionId": "abc-123", "cwd": "/Users/you/project", "workspaceRoot": "/Users/you/project", "toolName": "run_terminal_cmd", "toolInput": { "command": "npm test" }, "timestamp": "2026-04-14T12:00:00Z" } ``` ### Output (for blocking hooks like PreToolUse) Write JSON to **stdout**: - Allow: `{"decision": "allow"}` - Deny: `{"decision": "deny", "reason": "Unsafe command detected"}` **Exit codes** (behavior differs by hook type): - `0` β€” success / allow (for blocking hooks) - `2` β€” explicit deny (`PreToolUse`) or block-stop with stderr as feedback (`Stop`/`SubagentStop`; see Stop Decision Control in the user guide) - Any other (including timeout/crash/missing env var) β€” **fail-open**: the failure is logged and shown in the hook scrollback, but the tool call is not blocked. To block a tool call, return JSON `{"decision":"deny","reason":"..."}` on stdout. ### Passive hooks For events like `SessionStart` or `PostToolUse`, stdout is ignored. Just exit 0 on success. ### Useful Environment Variables Grok injects the following variables into every hook process: - `GROK_HOOK_EVENT` β€” the event name (e.g. `pre_tool_use`, `session_start`, `post_tool_use`) - `GROK_HOOK_NAME` β€” the full configured name of this hook - `GROK_SESSION_ID` β€” the current session identifier - `GROK_WORKSPACE_ROOT` β€” absolute path to the workspace root For hooks provided by plugins, the following are also set: - `GROK_PLUGIN_ROOT` β€” absolute path to the plugin's installation directory - `GROK_PLUGIN_DATA` β€” absolute path to the plugin's writable data directory These runner- and plugin-injected variables always take precedence. Attempts to override the reserved runner keys via the `env` field are stripped at load time (with a warning logged). For plugin hooks, `GROK_PLUGIN_ROOT` and `GROK_PLUGIN_DATA` similarly override any user-supplied values for those keys. ### Custom Environment Variables (`env` field) Each handler can declare additional env vars to inject into the child process: ```json { "type": "command", "command": "bin/check.sh", "env": { "MY_API_TOKEN": "secret-here", "LOG_LEVEL": "debug" } } ``` Values must be **strings** β€” JSON numbers and bools currently fail to parse (wrap them in quotes if you need them). For plugin hooks, the plugin adapter additionally injects `GROK_PLUGIN_ROOT` and `GROK_PLUGIN_DATA`. These keys override any user-declared values for the same names (the plugin contract is non-negotiable). ### Variable Substitution `command` and `url` strings support `$VAR` and `${VAR}` substitution at config-load time: ```json { "type": "command", "command": "${HOME}/.config/grok-hooks/check.sh" } ``` Lookup order for each reference: 1. The handler's own `env` map. 2. The current process environment (the env Grok itself sees). If a reference is unset in both, it's **preserved verbatim** (e.g. `${UNSET}` stays as the literal string). The runtime `sh -c` branch may resolve it later if the var becomes set; otherwise the runner refuses to spawn with a clear "required env var(s) not set" error. For HTTP hooks specifically, `url` is also re-expanded **at request time** (immediately before SSRF validation), so plugin-injected vars like `${GROK_PLUGIN_ROOT}/check` resolve against the plugin's actual path. #### Parameter-expansion modifiers POSIX parameter-expansion forms β€” `${VAR:-default}`, `${VAR-default}`, `${VAR:=x}`, `${VAR:?msg}`, `${VAR:+x}`, `${VAR%pat}`, `${VAR#pat}`, `${VAR/pat/repl}`, `${VAR:N:M}` β€” are **never** expanded at load time and are left verbatim for the runtime `sh -c` branch to handle. This avoids subtle divergences between the load-time expander and POSIX shell semantics (notably, the empty-string behaviour of `:-`). If your hook command contains shell metacharacters (spaces, pipes, `&&`, redirects, `$`, etc.), the runner routes it through `sh -c` and you get full shell-expansion semantics. If your command is a bare path with no metachars, the runner spawns it directly β€” but `$VAR` / `${VAR}` references in the path are still resolved at load time so direct-exec paths like `${HOME}/bin/check.sh` work without needing to be wrapped in `sh -c`. #### What is NOT expanded - **`matcher`** is a regex (`$` is the regex anchor for end-of-line). It is never env-expanded β€” substituting `$VAR` would silently change the regex's semantics and likely produce an invalid pattern. If you need a dynamic matcher, generate the JSON file at write time. - **`timeout`** is numeric, so there is nothing to expand. - **The values of the `env` map itself** β€” these are stored verbatim and passed to the child as-is, so `"BAR": "${HOME}/x"` injects the literal string `${HOME}/x` into the child's environment. ## Managing Hooks in the TUI Press `Ctrl+L` on non–VS Code family (or run `/hooks` anywhere) to open the Hooks & Plugins modal. In the **Hooks** tab you can: - `l` β€” Reload all hooks - `a` β€” Add a custom hook by path (great for testing) - `e` β€” Enable/disable - `r` β€” Remove - `Space` β€” Expand groups Hooks from `~/.grok/hooks/` appear under **Global**, project ones under **Project**, etc. ## HTTP Hooks Instead of a local script, call a remote endpoint: ```json { "type": "http", "url": "https://hooks.example.com/grok-event", "timeout": 15 } ``` The full event envelope is POSTed as JSON. Useful for webhooks, analytics, or serverless functions. ## Best Practices 1. **Keep hooks fast** β€” long-running hooks block the UI (use background `&` or async where possible). 2. **Use explicit `deny` to block** β€” hooks fail-open on any error (timeout, crash, missing env var, etc.), so a hook that crashes will not block the tool call. To enforce policy, your hook must run to completion and emit `{"decision":"deny","reason":"..."}` on stdout. 3. **Use absolute paths or relative to hook file** β€” scripts in `bin/` next to the JSON are portable. 4. **Test with `Ctrl+L` (non–VS Code family) / `/hooks`** β€” verify loading and matching before relying on them. 5. **Version control project hooks** β€” commit `.grok/hooks/` (but never secrets). ## Security Notes - Global hooks (`~/.grok/...`) run with your user permissions β€” treat them like shell scripts. - Project hooks require explicit trust (run `/hooks-trust` or use the modal) to prevent supply-chain attacks from malicious repos. - HTTP hooks send session data β€” only use trusted endpoints. ## Troubleshooting - **Hook not running?** β†’ Press `Ctrl+L` on non–VS Code family (or run `/hooks` anywhere) to see if it's loaded and matched. - **Project hooks ignored?** β†’ Trust the project first. - **Script not found?** β†’ Check the path is relative to the `.json` file and executable (`chmod +x`). - **See errors?** β†’ Check the pager logs (usually in the tracing pane or `~/.grok/logs`). ## More Examples See the built-in examples in the `xai-grok-hooks` crate: - [Safe Shell Guard](../../../xai-grok-hooks/examples/hooks/safe-shell.json) - [No Recursive Grep](../../../xai-grok-hooks/examples/hooks/no-recursive-grep.json) β€” hard-blocks `grep -r`/`grep -R`/`rgrep` (OOM guard) - [Session Audit Log](../../../xai-grok-hooks/examples/hooks/session-log.json) - [Tool Activity Logger](../../../xai-grok-hooks/examples/hooks/tool-logger.json) Copy them to `~/.grok/hooks/` and customize. ## Full Reference For the complete event list, matcher semantics, trust model, and advanced details, see the [Hooks user guide](user-guide/10-hooks.md). --- *Happy hooking!* If you build something cool, consider sharing it as a plugin.