Synced from monorepo Changes: - Report invalid MCP server config instead of failing startup - Keep completed terminal output when the gateway connection is lost - Show a duration-only detail view for single-task task output - Don't let a stale registry turn counter hide local sessions - Raise the file-descriptor soft limit on Linux and log effective limits at startup - Stop aborting when HTTP client construction fails - Make session thread and runtime spawn failures recoverable - Fix main-prompt paste parity in the question freeform input - Fire SessionEnd hooks on /exit and headless quit - Embed the deployment-config signing public key - Repaint paste-chip background on inline panel inputs - Security: prevent acceptEdits from auto-approving agent writes into the always-trusted global hook root - Fix stacked "Worked for" markers so parks render as status and turns close with exactly one marker - Parse hooks from config files - Add a remote kill-switch for managed-config signature verification - Security: fix workspace file-reference resolution bypassing workspace filesystem confinement Source-Revision: d02693a856a54f1030695b36b91d276e96b30b23
12 KiB
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 fmtorprettierautomatically after edits. - Environment setup: Export secrets or set variables at session start.
- Custom workflows: Trigger builds, tests, or deployments on specific events.
Quick Start
-
Create the hooks directory:
mkdir -p ~/.grok/hooks -
Create a simple hook file, e.g.
~/.grok/hooks/session-start.json:{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "echo \"🚀 Grok session started in $(pwd)\"" } ] } ] } } -
Start (or restart) a Grok session. The hook runs automatically on
SessionStart.Try it: press
Ctrl+Lon non–VS Code family (or run/hooksanywhere — 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 | <project>/.grok/hooks/*.json |
Requires trust | Per-repo automation |
| Project | <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 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:
{
"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/SubagentStopgates). 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:
{
"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 hookGROK_SESSION_ID— the current session identifierGROK_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 directoryGROK_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:
{
"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:
{
"type": "command",
"command": "${HOME}/.config/grok-hooks/check.sh"
}
Lookup order for each reference:
- The handler's own
envmap. - 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
matcheris a regex ($is the regex anchor for end-of-line). It is never env-expanded — substituting$VARwould 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.timeoutis numeric, so there is nothing to expand.- The values of the
envmap itself — these are stored verbatim and passed to the child as-is, so"BAR": "${HOME}/x"injects the literal string${HOME}/xinto 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 hooksa— Add a custom hook by path (great for testing)e— Enable/disabler— RemoveSpace— 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:
{ "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
- Keep hooks fast — long-running hooks block the UI (use background
&or async where possible). - Use explicit
denyto 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. - Use absolute paths or relative to hook file — scripts in
bin/next to the JSON are portable. - Test with
Ctrl+L(non–VS Code family) //hooks— verify loading and matching before relying on them. - 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-trustor 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+Lon non–VS Code family (or run/hooksanywhere) 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
.jsonfile 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
- No Recursive Grep — hard-blocks
grep -r/grep -R/rgrep(OOM guard) - Session Audit Log
- Tool Activity Logger
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.
Happy hooking! If you build something cool, consider sharing it as a plugin.