Grok Build runs your Claude Code hooks — and ignores every verdict they return
Run agents? ACP lets you see, control, and price every tool call they make — allow/ask/deny policy, per-session cost, and a full audit log for Claude Code, Cursor, Codex, and OpenClaw, in one command:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Getting started → · see your first governed call → · free up to 5 agents · prefer fully on-device? add --local
Grok Build — xAI’s terminal coding agent, 1.0 as of August 7 — now has ACP support via @agenticcontrolplane/grok-build: every tool call policy-checked before it runs, in every permission mode, with an audit record and a session receipt.
Building it surfaced two things worth writing down. One is a trap in Grok Build’s Claude Code compatibility that will bite anyone who assumes their existing hooks carry over. The other is a bug the live verification found in our own gateway — filed, fixed at the client, and written up below, because a control plane that publishes other harnesses’ silent failures should publish its own.
The verified contract
Grok Build open-sourced in July, so none of this is inferred: it’s read out of the hook runner’s source and the in-repo user guide.
The good news is genuinely good. PreToolUse hooks fire in every permission mode — including --always-approve. Deny rules too. That’s the single most control-friendly property a harness can have: yolo mode silences Grok’s own prompts, and the hook still stands on the call path. Hooks register user-globally in ~/.grok/hooks/*.json with no per-project trust prompt, there’s an updatedInput field for rewriting tool input before execution, and type:"http" hooks can POST the event envelope straight to a URL.
Two properties you have to design around:
It’s fail-open, everywhere. Default hook timeout is 5 seconds; a timed-out, crashed, or unparseable hook logs a line and the call proceeds. Only an explicit deny blocks. A policy hook that inherits this inherits “proceed” as its failure mode — so ours carries its own posture instead: attended sessions fail open with a loud UNGOVERNED warning and a durable lapse-log line, unattended tiers fail closed, and the registered timeout sits far above the hook’s internal 4-second budget so Grok’s timer never decides.
There is no ask. The gate parses a top-level decision field: deny blocks, allow or absent allows, anything else is an unknown-decision error. So an ACP ask resolves by mode — attended modes defer to Grok’s native prompt, which still stands after the hook; unattended modes turn the ask into a deny that carries the console link.
The compatibility trap
Grok Build reads hooks out of your real ~/.claude/settings.json by default, aliases Claude tool names in matchers, and injects CLAUDE_PROJECT_DIR. If you have Claude Code hooks installed, Grok Build is already running them.
It just isn’t listening to them. The runner doesn’t parse Claude’s hookSpecificOutput.permissionDecision — xAI’s own example emits that key “for forward-compatibility” only — and Claude’s legacy approve/block values are unknown-decision errors. A Claude-vocabulary deny parses as decision-absent. Decision-absent is an allow.
Sit with that for a second: the hook fires on every call, appears in the config, visibly executes — and every verdict it returns is discarded. Your session looks governed. It isn’t. This is the failure class we keep writing about: not controls that are missing, but controls that are silently not applying. The fix is a hook that speaks Grok’s vocabulary natively (ours emits Claude’s alongside, for the day that forward-compatibility note becomes parsing).
The bug the live test found — in our gateway
Standard launch discipline here is an end-to-end check against production before anything ships. First fixture: rm -rf / under Grok’s native shell tool name.
tool_name: "run_terminal_command" → allow
tool_name: "Bash" → deny (hardline floor)
Same command, same credential, same tier. Our hardline floor — the tier of policy that is supposed to hold unconditionally — keyed on the canonical tool name and never evaluated the unmapped one. Every previously shipped plugin passes its live floor test because its harness’s tool names happen to land in the recognized set. Grok’s don’t.
Filed as gatewaystack-connect#750: the durable fix is the gateway owning the vocabulary (or better, applying shell-content floors to any input shaped like a command — a floor that depends on a name isn’t a floor). The immediate fix ships in the plugin: Grok’s native names map to canonical before the check — the alias table comes from Grok’s own docs — and the native name rides along as client_tool_name in the audit record. The rm -rf / fixture now denies under the native name, exit code 2, and that exact check lives in the test suite.
We publish this for the same reason we publish everyone else’s gaps: the whole category’s failure mode is silence. A floor that quietly doesn’t apply is indistinguishable from a floor that held — right up until it matters. If your control layer never runs adversarial fixtures against its own production path, you don’t know which one you have.
Where Grok Build lands
On the coverage table: pre-call interception over everything (one hook pipeline, MCP tools included as server__tool), ask via mode-aware resolution, no inline result rewrite (only PreToolUse blocks; post-hoc flags surface loudly and land in the audit log). On the controls comparison: strongest always-approve story in the lineup, fail-open hook core, and a headless mode — dontAsk — that is the opposite of an escape hatch: it silently denies anything without an explicit allow rule. We’d earlier listed dontAsk as the escape hatch; the source says otherwise, and the deep page now has it right.
Install:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Detects Grok Build, registers the hook user-globally, provisions a workspace. Guide · source · what Grok Build ships natively.