Grok Build Hooks Reference — PreToolUse in Every Mode, Fail-Open by Design
Running Grok Build? See and control every tool call it makes — allow/ask/deny before each call, the reason on every row, one installer:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Full Grok Build guide → · see your first checked call → · free up to 5 agents
Just want the answer?
Grok Build's PreToolUse hook fires in every permission mode, --always-approve included, and reads deny rules from your ~/.grok/hooks/acp.json. Install it with:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Full walkthrough: Grok Build install guide. Native control model: Grok Build permissions, explained.
Grok Build is xAI’s terminal coding agent, and its hook system is the most Claude-compatible of the harnesses in this reference series — it reads your real Claude Code settings files, aliases Claude’s tool names, and even accepts Claude’s JSON output shape “for forward-compatibility.” None of that compatibility extends to holding by default: Grok Build’s own hook runner is fail-open, and the field it actually parses for a decision isn’t the one most Claude-lineage hooks write. This is the reference for what fires, what it returns, and what a control hook has to bring on its own to hold in every mode Grok Build ships.
Where hooks live
Grok Build reads hook registrations from three places, in ascending order of trust required:
~/.grok/hooks/*.json— always trusted, no prompt.<project>/.grok/hooks/*.json— only after the folder is trusted once.- Inline
[[hooks]]blocks inconfig.toml.
And, on by default: Grok Build also discovers hooks in your existing ~/.claude/settings.json, ~/.claude/settings.local.json, and ~/.cursor/hooks.json. If you already run Claude Code or Cursor hooks, Grok Build is already invoking them — a real compatibility surface, not a marketing line.
Five permission modes, and where PreToolUse sits
| Mode | What it means |
|---|---|
default |
Prompts for anything not pre-approved |
plan |
Read-only; no edits or shell commands run |
acceptEdits |
File edits auto-approved; shell commands still prompt |
always-approve (--yolo) |
Every native prompt answered yes automatically |
dontAsk (headless) |
No prompts at all — unmatched calls are silently denied |
The two worth not confusing: always-approve is the escape hatch that answers yes to a human-facing prompt, and dontAsk is the strict headless posture that denies anything a rule doesn’t explicitly cover. Both matter for hooks for the same reason: deny rules and PreToolUse hooks fire in every mode, always-approve included. The flag removes the confirmation dialog; it does not remove the hook.
The event surface — only one event blocks
Grok Build fires SessionStart, SessionEnd, UserPromptSubmit (observe-only — unlike Claude Code, it cannot deny here), PreToolUse, PostToolUse, PostToolUseFailure, PermissionDenied, Stop, SubagentStop, and a compaction event. Of all of these, only PreToolUse can block a call before it runs.
The output contract: a top-level field, not hookSpecificOutput
A Grok Build hook script writes JSON to stdout. The field the runner actually parses is a top-level decision:
{ "decision": "deny", "reason": "Blocked by policy" }
"allow" or an absent field lets the call proceed; exit code 2 is an alternate deny path read from stderr. Here’s the trap: Claude Code hooks write their verdict under hookSpecificOutput.permissionDecision, and Grok Build’s docs describe accepting that shape too — “for forward-compatibility.” In practice, a hook that only sets hookSpecificOutput.permissionDecision: "deny" and leaves the top-level decision field unset parses as decision-absent, which is allow. A hook can run, correctly compute a deny, and still let the call through because it wrote the verdict in the wrong place. Any control hook targeting Grok Build has to write the flat decision field to actually block anything.
Fail-open by design — and why a control hook can’t inherit it
Grok Build’s default hook timeout is 5 seconds. A timeout, a crash, or malformed stdout all resolve to allow — only an explicit "decision": "deny" stops a call. That’s a defensible choice for the harness itself: a broken chime shouldn’t brick your coding session. It also means anything built on the same hook contract inherits “proceed” as its failure mode unless it brings its own posture on top.
The ACP hook for Grok Build answers this directly, and it doesn’t fail open uniformly: in interactive/auto sessions it fails open loud (a [ACP] ⚠ UNGOVERNED warning to stderr plus a durable line in ~/.acp/lapse.log), while unattended tiers (headless, CI, dontAsk) fail closed — nobody’s watching the scrollback to notice a silent gap, so the safer failure there is to stop.
The missing “ask,” and how it resolves
Grok Build’s own hook contract has no ask outcome — only allow or deny. The ACP hook resolves this by mode: in default/plan, an ask verdict defers to Grok Build’s own native confirmation prompt, which still stands after the hook returns; in auto/always-approve/headless modes, where there’s no prompt to defer to, ask becomes a deny with a link to resolve it in the console. An explicit local allow rule outranks an ask in attended modes; a deny always holds regardless of mode.
Installing it: ~/.grok/hooks/acp.json
curl -sf https://agenticcontrolplane.com/install.sh | bash
Manually:
mkdir -p ~/.acp/hooks/grok-build ~/.grok/hooks
curl -fsSL https://agenticcontrolplane.com/hooks/grok-build/hook.mjs \
-o ~/.acp/hooks/grok-build/hook.mjs
curl -fsSL https://agenticcontrolplane.com/hooks/grok-build/acp.json \
-o ~/.grok/hooks/acp.json
The registration file points Grok Build at the local hook script; the hook script itself calls the gateway, not the other way around, so rotating your ACP key is a credential-file overwrite, not a reinstall.
--local isn’t wired for Grok Build, and why
Run the installer’s own source and you’ll find Grok Build explicitly excluded under --local: the step is skipped with a message that its hook needs a workspace to check a call against, and local decisions aren’t wired there yet. The reasoning: Claude Code’s and Cursor’s hooks call an on-device policy engine (~/.acp/policy.json plus a local decide script) that --local mode seeds for exactly this purpose. Grok Build’s hook script has no equivalent local path yet — it always calls the hosted /govern/tool-use endpoint, so running it without a workspace key means it has nothing to check against. The installer skips the integration outright rather than register a hook that would always no-op. The same skip applies to Muse Code, Antigravity, dsh, pi, Prime Agent, and opencode. OpenClaw is the one exception among the harnesses in this series — see its own hooks reference for why.
Known limitations
- Hooks see tool calls, not tokens: model spend isn’t metered for Grok Build today — no proxy or transcript path ships for it, so tool-call rows (allow/deny, with the native tool name preserved) are what’s recorded, not per-call token cost.
- A hook can only judge what it’s shown at the moment it’s called — the same limit as every hook-based control layer, Grok Build’s included.
- Grok Build’s own headless session log (
~/.grok/sessions) has no documented query surface of its own; the ACP activity log is the queryable record of what the hook decided.
Comparing to Claude Code hooks
The shapes are close but not identical. Claude Code’s hook contract parses hookSpecificOutput.permissionDecision and has a real ask outcome baked in; Grok Build parses a flatter top-level decision and only has allow/deny. Both fire in their respective “auto-approve everything” modes — Claude Code’s bypassPermissions and Grok Build’s --always-approve — which is exactly why both need a hook that fails closed for unattended use rather than trusting the harness’s own default. See the Claude Code hooks reference for the comparison in the other direction.
Frequently asked questions
Does a Grok Build PreToolUse hook fire under --always-approve?
Yes. Grok Build applies deny rules and PreToolUse hooks in every permission mode, always-approve (–yolo) included. The flag answers yes to Grok’s own prompts; it doesn’t stop the hook from running or from returning a deny that blocks the call.
What does a Grok Build hook return to block a call?
A top-level decision field: “deny” (with a reason string) blocks the call, “allow” or an absent field lets it proceed. Exit code 2 is an alternate deny channel. There is no ask value — Grok Build’s hook contract only has two outcomes.
What happens if a Grok Build hook times out or crashes?
The call runs. Grok Build’s hook runner is fail-open by design — a timeout (5 seconds by default), a crash, or malformed JSON output all get logged to the scrollback, and the tool call proceeds. Only an explicit deny blocks it.
Can I run Grok Build's ACP hook in --local mode with no account?
Not today. The installer’s –local mode explicitly skips Grok Build: the hook needs a workspace to check a call against, and the on-device policy engine Claude Code’s and Cursor’s hooks use locally isn’t wired into Grok Build’s hook yet.
Where to read more
- Grok Build permissions, explained
- Grok Build install guide
- Claude Code hooks reference
- Codex hooks reference
- grok-build-acp-plugin source