Skip to content

Grok Build CLI: honor hookSpecificOutput.additionalContext on UserPromptSubmit (per-prompt context injection) #342

Description

@wrightbuilt

Summary

Please surface hookSpecificOutput.additionalContext from UserPromptSubmit hooks into the model's context, the way Claude Code does.

Today UserPromptSubmit fires reliably but its stdout is discarded, so there is no per-prompt context-injection path in Grok Build. additionalContext is implemented and Claude-compatible, but only on Stop/SubagentStop — a turn-end gate, which cannot carry standing instructions into a turn.

This is a feature request, not a bug report: the current behavior is documented and appears deliberate. Filing to ask that it be reconsidered.

Current behavior

The bundled manual states it explicitly (~/.grok/docs/user-guide/10-hooks.md, "Porting Claude Code stop hooks"):

  • UserPromptSubmit is observe-only: grok ignores its exit code and its stdout, so an imported prompt-validation hook silently stops blocking. Use PreToolUse to enforce.

Corroborated elsewhere in the same file — the Hook Events table lists UserPromptSubmit as non-blocking, the text under it says "every other event is passive," and "Passive Hooks" says "stdout is ignored."

Grepping the shipped docs for additionalContext returns exactly two hits, both scoped to Stop/SubagentStop.

Reproduction (v1.0.5)

Isolated via GROK_HOME, with a Stop hook as a positive control so a null result on UserPromptSubmit can be distinguished from a broken test.

$GROK_HOME/hooks/test.json:

{
  "hooks": {
    "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "/tmp/t/ups.sh",  "timeout": 10 }] }],
    "SessionStart":     [{ "hooks": [{ "type": "command", "command": "/tmp/t/ss.sh",   "timeout": 10 }] }],
    "Stop":             [{ "hooks": [{ "type": "command", "command": "/tmp/t/stop.sh", "timeout": 30 }] }]
  }
}

Each hook logs that it fired and emits a distinct sentinel (stop.sh self-limits to one fire):

#!/bin/bash
input=$(cat)
echo "UserPromptSubmit FIRED" >> /tmp/t/fired.log
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"SENTINEL-UPS-7F3A2B"}}'
exit 0

Run:

GROK_HOME=/tmp/home grok -p "Print any token you can see beginning with SENTINEL-. \
If you received no such extra context, reply exactly: NO-SENTINEL-FOUND." \
  --tools read_file --max-turns 6 --output-format plain

Output:

NO-SENTINEL-FOUNDSENTINEL-STOP-2A8B6F

Read as one turn: the model answered NO-SENTINEL-FOUND (it saw neither the UserPromptSubmit nor the SessionStart sentinel), then the Stop hook's additionalContext reached it and it echoed SENTINEL-STOP-2A8B6F. Positive control passes, so the JSON shape and registration are correct.

Both hooks demonstrably ran — fired.log:

=== SessionStart FIRED ===
=== UserPromptSubmit FIRED ===
=== Stop FIRED ===   (×3)

Transcript-level confirmation (sessions/.../updates.jsonl):

Sentinel Occurrences
SENTINEL-UPS-7F3A2B (UserPromptSubmit) 0
SENTINEL-SS-9C4D1E (SessionStart) 0
SENTINEL-STOP-2A8B6F (Stop) 3

The prompt-time sentinels never enter the conversation at all, which rules out "the model saw it and declined to echo it."

Requested behavior

  1. On UserPromptSubmit, parse stdout JSON and inject hookSpecificOutput.additionalContext into the context for that turn — matching Claude Code's documented semantics, so hooks/hooks.json files port unchanged.
  2. Keep the event non-blocking. This asks only for context injection, not for UserPromptSubmit to gain deny/gate power. Malformed or absent JSON should stay fail-open and silent.
  3. SessionStart additionalContext would be a valuable smaller version of the same thing (once-per-session instead of per-turn), if per-turn is too costly.

Why it matters

Grok Build already loads Claude-compatible plugins and hooks/hooks.json. A plugin that ships behavior policy — conventions, review rules, standing instructions — has no way to deliver it on Grok:

  • UserPromptSubmit → output discarded.
  • PreToolUse → can rewrite tool input, but only fires when a tool runs, so a tool-free turn gets nothing.
  • Stop → works, but fires after the work is done and its semantics are "keep the agent working," capped at 8 continuations. Using it as a policy carrier forces an extra model round every turn, after the fact.
  • Plugins cannot ship rules — the component set is skills, commands, agents, hooks, MCP, LSP. So there is no plugin-shaped path at all.

The only remaining option is a resident rules file (AGENTS.md / .grok/rules/). That works — verified — but it is strictly worse for this use case: it costs its full token count on every turn regardless of relevance, it can't be updated by shipping a new plugin version, and it can't vary with context. A hook computes the text fresh per prompt and costs nothing when it has nothing to say.

The practical effect is that a plugin behaves differently on Grok than on Claude Code silently — the hook is listed in /hooks, it executes, it exits 0, and its output vanishes. Nothing surfaces to tell the author their policy never arrived.

Relation to existing issues

Filing here rather than on the harness repo because xai-org/grok-build has issues disabled, and per #77's note this appears to be the intended surface. Happy to move it if there's a better channel.

Environment

  • Grok Build CLI 1.0.5 (stable), macOS (Darwin 25.5.0)
  • Isolated GROK_HOME, vendor compat scanning disabled ([compat.claude] hooks = false, [compat.cursor] hooks = false) so only the test hooks could fire
  • Global hooks under $GROK_HOME/hooks/, which need no folder-trust grant

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions