Skip to content

Add experimental Grok Build (grok) host - #148

Merged
Saksham Uboweja (SakshamUboweja) merged 4 commits into
microsoft:mainfrom
cagdasyurekli:feat/grok-host
Nov 6, 2026
Merged

Saksham Uboweja (SakshamUboweja) merged 4 commits into
microsoft:mainfrom
cagdasyurekli:feat/grok-host

Conversation

@cagdasyurekli

Copy link
Copy Markdown
Contributor

Closes #145.

Grok 1.0.70 loads Claude Code's installed plugins and runs their hooks. So TokenEconomizer installed for Claude Code already runs inside grok sessions, where hosts.detect treated them as Claude Code: the wrong session log, tool names and result shapes, and side calls through claude -p. This PR adds a grok host and makes detect recognise grok first. Grok alone sends hookEventName, and its sessions live under ~/.grok/sessions/. Environment variables decide only when the payload has no transcript path, so a Claude Code session started from a grok shell stays Claude Code. Supported: micro-skills, the loop guard and the session ledger. Not supported: tool pruning.

Grok behaviours the host handles (verified live on 1.0.70)

  • A Bash tool_response.output is a list of byte ints. The host reads the text from output_for_prompt and the exit code from exit_code.
  • An extra observe-only Stop (reason shutdown, no promptId) fires after SessionEnd. It is ignored. StopCancelled and StopFailure close the turn as not normal.
  • promptId is on UserPromptSubmit and Stop but not on tool events.
  • SessionStart context never reaches the model. Only a person-facing systemMessage is read there, which I verified with a codeword. PostToolUse additionalContext does reach the model, so the start context is delivered once, with the session's first tool call.
  • Usage comes from the session's usage.json, where inputTokens includes cached reads, so fresh input is inputTokens - cachedReadTokens. xAI ids aren't in pricing.py, so the ledger uses its labelled fallback.

Side calls

grok -p … --output-format json runs in a throwaway directory with TE_SIDECALL=1. grok -p runs in auto permission mode, so the call uses a tool allowlist: --tools read_file, with grok's MCP dispatchers removed and deny rules for Bash/Edit/Write/Web/Task behind it.

A deny list does not fail closed here. Live, --disallowed-tools run_terminal_cmd (the name in grok's docs example) removed nothing and the shell command ran; grok's real shell tool is run_terminal_command. --tools "" is read as no filter at all. With the allowlist, the side call's own tool_definitions.json lists only read_file, and a prompt asking for a shell command and a file write left its directory empty.

Verification

  • python -m pytest tests experiments/tests: 1119 passed, 18 skipped (28 new tests in tests/test_grok.py, fixtures built in code).
  • A live grok -p run with the plugin's Claude Code hook file (no --host) installed as project hooks, with an isolated TOKENECONOMIZER_HOME:
    • The session was detected as grok and the turn recorded (2 tool calls, ended normally; the shutdown Stop was ignored).
    • The folded ledger row matches grok's own report exactly (10,408 fresh input, 30,208 cached, 318 output tokens, grok-4.7-build).
  • grok_call was run live and returned a verdict. grok plugin validate plugin passes.
  • Not verified live:
    • grok plugin marketplace add plus grok plugin install tokeneconomizer --trust, which follows grok's docs; I didn't install into a real grok config.
    • A redirect in a real stuck turn, which needs a learned envelope. It is covered through hook.main in the tests.

Note for review

This PR and the agy host PR (#147) each add a small render seam to hook.run with different signatures. Whichever lands second needs a small rebase.

🤖 Generated with Claude Code

grok 1.0.70 runs Claude Code's installed plugins and sends Claude's
snake_case hook keys, so TokenEconomizer already ran inside grok sessions
and was misdetected as Claude Code (wrong session log, tool names and side
calls). detect() now recognises grok first (it alone sends hookEventName;
its sessions live under ~/.grok/sessions).

Verified live on grok 1.0.70:
- Bash results carry `output` as a list of byte ints; the adapter reads
  output_for_prompt and exit_code instead.
- An extra observe-only Stop (reason "shutdown", no promptId) follows
  SessionEnd and is ignored; StopCancelled/StopFailure close the turn as
  not normal.
- SessionStart context never reaches the model (only a person-facing
  systemMessage is read there), so the start context is delivered once with
  the session's first PostToolUse additionalContext, which does.
- Usage comes from the session's usage.json (inputTokens includes cached
  reads); side calls run through `grok -p --output-format json`.

Micro-skills, the loop guard and the session ledger are supported; tool
pruning is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deny list naming grok's shell as run_terminal_cmd removed nothing (its
shell is run_terminal_command), and grok -p runs in auto permission mode,
so a side call whose prompt quotes session commands could run a shell
command. Verified on 1.0.70 that the allowlist leaves only read_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SakshamUboweja

Copy link
Copy Markdown
Collaborator

Thanks, this looks good and I'm happy to merge it. A few small things first:

The sidecall.py module docstring still says the Grok side call removes tools with --disallowed-tools. It now uses the --tools read_file allowlist, so please update it. docs/PLUGIN.md also lists the side call's flags as not exercised, but 4f9af7f says they were verified on 1.0.70.
Please add grok.py to tokeneconomizer/trace/README.md.
Please rebase on main. There's a small README conflict with the Cursor update from #149.

…nto feat/grok-host

# Conflicts:
#	README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cagdasyurekli

Copy link
Copy Markdown
Contributor Author

Thanks! Addressed in 84418df:

  • The sidecall.py docstring now describes the --tools read_file allowlist (plus --disallowed-tools for the MCP dispatchers and --deny rules).
  • docs/PLUGIN.md moves the side call's flags out of "Not exercised" and says what was verified on 1.0.70: the run's tool list held only read_file, and a prompt asking it to create a file left none.
  • Added grok.py to tokeneconomizer/trace/README.md.
  • main is merged in (252f2ac) and the README conflict with Cursor: count prompts and close turns in headless (-p) runs #149 is resolved. I merged instead of rebasing so there's no force-push, but I can rebase if you prefer.

@SakshamUboweja
Saksham Uboweja (SakshamUboweja) merged commit ce9cf65 into microsoft:main Nov 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Grok Build CLI (grok)

2 participants