Reverse-engineering Grok Build from one stripped binary
10 Jul 2026I started this investigation with a simple assumption: if the Grok
terminal client was distributed as JavaScript, then understanding it
might be mostly a matter of unpacking a bundle, finding source maps, and
following module imports. That would have been the easy route. The
installation looked small from the outside: two commands,
grok and agent, both placed in
~/.grok/bin/. But the first few terminal commands changed
the direction of the investigation completely.
Grok Build 0.2.93 is not a packaged Node.js application. It is a 153 MiB stripped, statically linked Rust executable containing the TUI, ACP server, session engine, tool runtime, networking, Git integration, sandboxing, and subagent coordination in one binary. There is no JavaScript module graph to unfold. There is, however, a surprising amount of architecture still visible if we stop treating decompilation as the only way to learn from a binary.
This article follows that path: identify the artifact, recover its Rust structure from ELF metadata and embedded tracing paths, interrogate its public ACP boundary, reconstruct the agent loop, and decide where modification is actually useful. The work here was performed on my own local installation, without changing the installed executable or sending an inference request.
Table of Contents
1. Start with
the artifact, not the interface
2. Proving that it is native
Rust
3. What stripped really
removes
4. Recovering the internal
crate map
5. Reconstructing the agent
loop
6. ACP is the cleanest
observation point
7. How subagents are
represented
8. Persistence
makes the runtime inspectable
9. Modification
without patching machine code
10. When native
decompilation is still useful
11. A practical research
workflow
12. What this tells us
about coding agents
Start with the artifact, not the interface
The installer creates four visible paths, but only one real executable:
~/.local/bin/grok -> ~/.grok/bin/grok
~/.local/bin/agent -> ~/.grok/bin/agent
~/.grok/bin/grok -> ../downloads/grok-linux-x86_64
~/.grok/bin/agent -> ../downloads/grok-linux-x86_64
That is worth checking before doing anything more sophisticated.
Alternate command names sometimes activate different behavior through
argv[0], as BusyBox does. In this build, invoking the
target through agent produces the same top-level CLI as
grok. The actual non-TUI agent interface is a
subcommand:
grok agent stdio
grok agent serve
grok agent headlessThe first useful inspection is deliberately boring:
target="$HOME/.grok/downloads/grok-linux-x86_64"
file -L "$target"
sha256sum "$target"
readelf -hW "$target"
readelf -SW "$target"
readelf -dW "$target"The filesystem relationship can be reduced to one small map:
command typed installed entry real artifact
grok -----------------------> ~/.grok/bin/grok -----+
|
+--> grok-linux-x86_64
| 159,465,672 bytes
agent -----------------------> ~/.grok/bin/agent -----+ one ELF process
"agent" is not a second implementation.
"grok agent ..." is a subcommand handled inside the shared executable.
For version 0.2.93, the result was:
| Property | Observed value |
|---|---|
| Format | ELF 64-bit, x86-64 |
| ELF type | DYN, used here as a position-independent
executable |
| Linking | Static PIE |
| Symbols | Stripped |
| Size | 159,465,672 bytes |
| Build ID | b2a926bda144f1fa29d85f30e4814e47c467c4ad |
| CLI build | f00f96316d |
| SHA-256 | 4e0738d3b5550f3c842bc0ae69f468815c6329c008a110d0c27a694dc3401135 |
Static PIE explains several later constraints. Address-space layout
randomization still applies, but there are effectively no normal
shared-library boundaries to hook. ldd reports the
executable as statically linked. Techniques built around replacing
dynamically linked functions with LD_PRELOAD are therefore
poor fits.
Proving that it is native Rust
Searching for words like node_modules, bun,
or Node.js is not enough. A large native application may
contain those terms because it understands JavaScript projects, invokes
package managers, or ships a web-development tool. The compiler metadata
gives a much stronger answer:
readelf -p .comment "$target"
objdump -s -j .debug_gdb_scripts "$target"The .comment section identifies:
rustc version 1.92.0 (ded5c06cf 2025-12-08)
GCC: (GNU) 9.4.0
GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
The .debug_gdb_scripts section asks GDB to load Rust
pretty-printers. Dependency paths embedded in the read-only data also
name Rust crates with exact versions, including tokio,
reqwest, rustls, ratatui,
rusqlite, git2, gix,
tonic, prost, rmcp, and
agent-client-protocol.
This is not a thin Rust launcher around a hidden JavaScript payload. The native code section alone is more than 100 MiB. The crate inventory matches the features visible in the product: async orchestration, HTTP streaming, a terminal UI, SQLite search, Git worktrees, protobuf messages, MCP, and ACP.
The binary also contains a .gnu_debuglink entry:
xai-grok-pager.debug
That filename is important. It means the release process produced or expected a separate debug-symbol artifact. The symbol file was not installed and I did not find it in the public distribution, but the exact matching file would be far more valuable than guessing function boundaries in an optimized release. GDB, Ghidra, IDA, or Binary Ninja could use it to recover names, types, and source-line associations.
What stripped really removes
"Stripped" often gets treated as if it means "opaque." It does not. Stripping primarily removes the convenient symbol and debug tables. It does not automatically remove every string literal, serialization field, tracing event, panic location, protocol name, error message, or source path that the program needs at runtime.
Rust applications are especially generous when they use structured
tracing. A call such as tracing::event! can preserve module
and source-file metadata through file!() and
module_path!(). Serde-generated code leaks structure names
and field names. Clap retains command names, argument descriptions, and
possible values. Protobuf and JSON-RPC layers retain method and message
identifiers.
A broad strings dump is noisy, so I narrowed it to
source-shaped patterns:
strings -a "$target" \
| rg -o 'crates/codegen/[A-Za-z0-9_./-]+\.rs' \
| sort -uThe result is not source code, but it is close to an architectural index. This is the key distinction: decompilation tries to reconstruct implementation. Metadata recovery reconstructs responsibility and boundaries. For learning how an agent works, the second one often gets us to the useful questions faster.
Recovering the internal crate map
The first-party crate names form a fairly clean system map:
| Crate | Likely responsibility, supported by module paths |
|---|---|
xai-grok-pager |
Full-screen terminal UI and subagent views |
xai-grok-shell |
CLI, ACP sessions, orchestration, persistence, permissions |
xai-chat-state |
Conversation state and construction of model requests |
xai-grok-sampler |
Streaming inference and retries |
xai-grok-tools |
Built-in tool definitions and schemas |
xai-tool-runtime |
Tool execution protocol and progress events |
xai-grok-mcp |
MCP discovery and dispatch |
xai-grok-hooks |
Lifecycle hooks around prompts and tools |
xai-grok-sandbox |
Landlock/Seatbelt execution isolation |
xai-grok-subagent-resolution |
Child role, persona, model, and capability resolution |
xai-grok-compaction |
Context-window compression |
xai-grok-memory |
Cross-session memory |
xai-hunk-tracker |
File changes, diffs, rewind, and review state |
The main shell crate exposes even more specific modules:
session/acp_session_impl/prompt_build.rs
session/acp_session_impl/run_loop.rs
session/acp_session_impl/sampler_turn.rs
session/acp_session_impl/tool_calls.rs
session/acp_session_impl/tool_dispatch.rs
session/acp_session_impl/turn_end.rs
session/acp_session_impl/reminders.rs
session/acp_session_impl/laziness_classifier.rs
session/persistence.rs
session/storage/jsonl.rs
agent/mvp_agent/subagent_coordinator.rs
agent/subagent.rs
Even without function bodies, those names expose a sequence. A session is set up, its prompt is built, a sampler turn streams model output, tool calls are collected and dispatched, results are folded back into chat state, and the turn is finalized and persisted.
There are also separate goal-related modules: planner, strategist, summarizer, classifier, tracker, verifier, and stop detector. One embedded prompt belongs to a strict JSON classifier that decides whether the main agent is stalled. It looks for narration that claims an action without a matching tool call, or a completion claim unsupported by tool evidence. This is a useful reminder that what feels like one agent in the UI can actually include several narrow model-assisted control passes around the primary conversation.
Reconstructing the agent loop
Putting together the crate names, protocol fields, local documentation, and session format produces this high-level loop:
ONE AGENT TURN
+-------------+ +----------------------+ +-------------------+
| user prompt | ----> | build model request | ----> | inference backend |
+-------------+ +----------------------+ +---------+---------+
| |
| collects | streams
v v
+------------------+ +---------------+
| system prompt | | text |
| AGENTS.md | | reasoning |
| skills + memory | | tool calls |
| prior messages | | final status |
| tool schemas | +-------+-------+
+------------------+ |
v
+----------+----------+
| classify the output |
+----+-----------+----+
| |
text / | | tool call
final | |
v v
+-----------+ +----------------+
| ACP / TUI | | tool dispatcher|
| updates | +-------+--------+
+-----+-----+ |
| v
| +------------------+
| | PreToolUse hooks |
| +--------+---------+
| |
| v
| +------------------+
| | permission rules |
| +--------+---------+
| |
| v
| +------------------+
| | sandbox boundary |
| +--------+---------+
| |
| v
| +------------------+
| | file / shell / |
| | Git / MCP / web |
| | subagent runtime |
| +--------+---------+
| |
| v
| +------------------+
| | structured result|
| +--------+---------+
| |
| +----+
| |
v v
+------------------------------+
| append to conversation state |
+---------------+--------------+
|
another model pass | or turn end
+------------+------------+
| |
v v
inference backend persist + summarize
"Build request context" is doing a lot of work here. It can include
the base system prompt, selected agent definition, project instruction
files such as AGENTS.md, skill descriptions, memory, MCP
tools, file references, prior conversation, plan state, reminders, and
the current tool schemas. The xai-chat-state request
builder then serializes that state for the selected backend.
The model is not directly executing shell commands. It emits a structured request. The client decides whether the tool exists, whether its arguments deserialize, whether a hook denies it, whether permission policy allows it, and whether the operating-system sandbox permits the underlying action. This separation matters because "the model can use Bash" really means "the harness exposed a Bash-shaped schema and agreed to execute a validated request under several local policy layers."
The ordering documented by Grok Build is:
- Blocking
PreToolUsehooks. - Explicit permission rules, with deny taking precedence.
- Built-in fast paths for read-only or recognized-safe operations.
- The active prompt policy, such as asking the user or always approving.
- The OS sandbox, which remains a separate enforcement boundary.
The official enterprise documentation confirms that the Linux sandbox
uses Landlock and that network communication uses rustls,
not OpenSSL.1 The dependency inventory in the
binary independently matches both claims.
ACP is the cleanest observation point
Native disassembly is not the only interface. Grok Build implements the Agent Client Protocol, a JSON-RPC protocol designed for editors and other agent clients.2 Its standard input mode can be queried without opening the interactive TUI:
grok agent --no-leader stdioI sent a single initialize request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": 1,
"clientCapabilities": {
"fs": {
"readTextFile": true,
"writeTextFile": true
},
"terminal": true
},
"clientInfo": {
"name": "local-analysis",
"version": "0"
}
}
}The response advertised session loading, embedded context, MCP over
HTTP and SSE, filesystem notifications, blocking pre-tool hooks, model
state, and built-in commands including compact,
context, session-info, and goal.
No inference request was needed to learn this.
ACP gives us a stable seam between presentation and orchestration:
presentation process Grok Build process
+--------------------------+ +----------------------------------+
| editor / IDE extension | | grok agent stdio |
| custom terminal client | <---> | |
| transparent log proxy | JSON | initialize |
+--------------------------+ RPC | session/new or session/load |
| session/prompt |
| session/request_permission |
| session/update |
+----------------+-----------------+
|
+--------------------+--------------------+
| | |
v v v
+-------------+ +-------------+ +-------------+
| session | | agent + tool| | persistence |
| registry | | run loop | | and restore |
+-------------+ +------+------+ +-------------+
|
+--------------------+--------------------+
| | |
v v v
+-------------+ +-------------+ +-------------+
| model | | local tools | | MCP servers |
| backend | | and Git | | and apps |
+-------------+ +-------------+ +-------------+
The client sees state transitions, not Rust function calls. That is usually the
better abstraction for learning why an agent asked, executed, waited, or stopped.
A transparent ACP proxy is therefore one of the best research tools for this application. It can log method names, session updates, reasoning chunks exposed by the protocol, permission requests, tool-call status, plans, and final messages. It will not reveal every internal Rust call, but it reveals the state transitions that actually matter to an editor or orchestration client.
The product itself officially supports interactive, headless, and ACP modes.3 That makes ACP observation less brittle than hooking private functions whose addresses may change in every release.
How subagents are represented
Subagents are not threads sharing one prompt buffer. They are child
sessions with their own conversation context. The parent model requests
one through a spawn_subagent tool, passing a task
description and an agent type. Resolution then combines the selected
role with optional model overrides, persona instructions, capability
restrictions, and isolation settings.
The built-in roles are:
| Type | Default purpose |
|---|---|
general-purpose |
Full-capability implementation and general work |
explore |
Read/search/execute investigation without file editing |
plan |
Codebase exploration followed by an implementation plan |
Capabilities can be restricted independently to read-only, read-write, execute, or all. Isolation is either the shared working directory or a separate Git worktree. A background child returns immediately with an ID; the parent later retrieves its progress or result. A blocking child holds the parent call until completion.
+-----------------------------------------------------------------------+
| PARENT SESSION |
| |
| model decides that a bounded task can be delegated |
+-----------------------------------+-----------------------------------+
|
| spawn_subagent {
| prompt, description, type,
| background, capability, isolation
| }
v
+---------------------------+
| subagent resolution |
| |
| 1. choose agent role |
| 2. apply persona |
| 3. resolve model |
| 4. restrict tools |
| 5. choose cwd/worktree |
+-------------+-------------+
|
v
+-----------------------------------------------------------------------+
| CHILD SESSION |
| |
| separate context window separate transcript normal run loop|
| |
| workspace choice: |
| +--------------------+ +-------------------------+ |
| | same working tree | OR | isolated Git worktree | |
| +--------------------+ +-------------------------+ |
+-----------------------------------+-----------------------------------+
|
progress -----+----- completion
|
v
+---------------------------+
| compact result / summary |
+-------------+-------------+
|
v
+-----------------------------------------------------------------------+
| PARENT CONTINUES |
| full child transcript stays separate; only the result enters context |
+-----------------------------------------------------------------------+
Depth limit: parent -> child. A child cannot create a grandchild.
The nesting depth is intentionally one: only the top-level session can spawn subagents. This keeps the graph flat and prevents recursive fan-out. A resumed child can inherit a completed child transcript and tool state, but its current system prompt and tools are rendered again from the active agent definition.
This architecture explains why subagents save context in the parent. The parent does not need every search result and failed command in its own history. It receives a compressed result while the complete child transcript remains separately inspectable.
Persistence makes the runtime inspectable
Every mode writes the same general session model under
~/.grok/sessions/. A session directory contains files such
as:
summary.json
updates.jsonl
chat_history.jsonl
plan.json
rewind_points.jsonl
signals.json
feedback.jsonl
compaction_checkpoints/
subagents/
updates.jsonl is the ACP-style event stream used for
restoration. chat_history.jsonl contains the raw
conversation messages used by the model-facing state. Smaller JSON files
track plans, usage signals, summaries, and rewind state. Session search
additionally maintains a SQLite FTS index.
This is more useful than it first appears. To understand an agent turn, we can correlate:
- the user message in
chat_history.jsonl; - emitted reasoning and tool events in
updates.jsonl; - the tool result attached to the next model request;
- turn and token counters in
signals.json; - child metadata under
subagents/; - debug logs enabled with
--debug-file.
That creates a behavioral trace without touching the ELF. It also separates two commonly confused histories: what the user interface rendered and what the model actually received after normalization, pruning, repair, or compaction.
Embedded log messages show that the chat-state layer repairs duplicate results and dangling tool calls after crashes. Compaction has its own checkpoints and transcript transformation code. This means a resumed session is not simply replaying a text file; it is reconstructing a consistent state machine from persisted events.
Modification without patching machine code
If the goal is to learn how the agent behaves, patching instructions
in .text should be near the bottom of the list. Grok Build
already exposes several modification layers with much better
stability.
Replace or extend the prompt
The CLI supports both:
grok --rules "Additional session-specific rules"
grok --system-prompt-override "A complete replacement system prompt"Project AGENTS.md files, custom agent definitions under
.grok/agents/, personas, and role prompt files offer more
durable variants. These are ideal for testing whether a behavior comes
from prompting or from harness logic.
Interpose at the lifecycle boundary
Hooks can observe session start, user prompts, tool requests,
results, permission denial, compaction, subagent start/stop, and turn
completion. PreToolUse hooks can deny an operation. Passive
hooks can log a structured event stream.
This is the right layer for experiments such as:
- recording every tool and its arguments;
- blocking a command family;
- measuring time between model output and tool completion;
- comparing main-agent and subagent tool patterns;
- injecting controlled environment state at session start.
Wrap ACP
A small proxy can launch grok agent stdio, forward
newline-delimited JSON in both directions, and write a timestamped copy.
Because it works at the documented protocol boundary, it should survive
internal Rust refactors better than an address-based hook.
Capture the model-facing request locally
Grok supports custom OpenAI-compatible model endpoints. In an
isolated lab configuration, a local endpoint can capture the request
body and return a canned response. The important safety rules are to use
a temporary GROK_HOME, a dummy API key, and an empty test
workspace so no real credentials, project instructions, memory, or
private source code enter the capture.
A minimal model definition looks conceptually like this:
[models]
default = "capture"
[model.capture]
model = "capture"
base_url = "http://127.0.0.1:8080/v1"
api_key = "dummy"
context_window = 128000This experiment can reveal the exact rendered system prompt, message ordering, tool schemas, reminders, and provider API dialect. For studying an agent harness, that is usually more informative than pseudocode reconstructed from optimized assembly.
When native decompilation is still useful
There are questions the public boundaries cannot answer. Native analysis becomes useful when we need the precise precedence of undocumented flags, the implementation of a parser, a hidden state transition, the source of a crash, or the location of an internal constant.
The practical route would be:
- Preserve the original hash and work on a copy.
- Import the ELF as x86-64 little-endian in Ghidra, IDA, or Binary Ninja.
- Mark the image as PIE and use the ELF program headers rather than guessing a base address.
- Apply Rust demangling and known library signatures.
- Use embedded source paths and tracing strings to label functions by responsibility.
- Locate cross-references to distinctive errors or protocol method strings.
- Compare behavior across two releases to separate stable subsystems from compiler noise.
- Load
xai-grok-pager.debugif an exact matching symbol artifact becomes available.
The best initial targets are not main. Optimized Rust
startup mostly leads into runtime and argument-parsing machinery. Better
anchors are distinctive strings associated with:
session/prompt
session/request_permission
spawn_subagent
tool.call
tool.cancel
compact_conversation
active_sessions.json
updates.jsonl
From each string, follow read-only-data references into code, then expand the call graph. Serde and protobuf-generated functions can be large and repetitive, so they should be labeled early to avoid mistaking serialization machinery for business logic.
Binary patching is possible, but the useful cases are narrow. A
same-length string replacement in .rodata is manageable.
Changing string length requires relocating data or rewriting references.
Control-flow patches must account for optimized code, RIP-relative
addressing, unwinding metadata, and ASLR. The internal updater will
replace the modified artifact on the next update.
Because the binary is statically linked, LD_PRELOAD will
not give normal interposition points. Because HTTPS is implemented with
rustls, OpenSSL function hooks will not expose plaintext.
Explicit protocol proxies, custom endpoints, ACP wrappers, hooks,
ptrace, or uprobes are more appropriate depending on the
question.
A practical research workflow
If I continued this project, I would avoid starting with a full decompile. I would build the evidence in layers:
Phase one: inventory
file -L "$target"
readelf -hW "$target"
readelf -SW "$target"
readelf -p .comment "$target"
readelf -x .gnu_debuglink "$target"
strings -a "$target" | rg 'crates/codegen|session/prompt|spawn_subagent'Record the version, build ID, compiler, hash, sections, source paths, dependencies, endpoints, environment variables, and protocol identifiers.
Phase two: observe public behavior
Run the ACP initialization handshake, enumerate CLI help recursively,
inspect discovered configuration with grok inspect, and
read the locally installed user guide. None of these steps requires a
paid model turn.
Phase three: trace one controlled session
Use an empty repository and a harmless prompt. Save ACP traffic, debug logs, session JSONL, hooks, and filesystem changes. Correlate events by session ID and tool-call ID.
Phase four: isolate one question
Examples:
- What exactly is sent to a subagent?
- When is a stalled turn automatically continued?
- How does permission precedence handle conflicting rules?
- Which content survives compaction?
- Does a resumed child inherit the old prompt or re-render the current one?
Use the least invasive boundary that answers the question. Only move to native decompilation if ACP, hooks, logs, persistence, and a controlled endpoint cannot resolve it.
Phase five: patch a disposable copy
If a machine-code modification is genuinely necessary, redirect a separate launcher to the copy and disable auto-update for the experiment. Never edit the symlink target without a rollback path. Verify the copy's hash before and after every change and document file offsets separately from runtime virtual addresses.
What this tells us about coding agents
The most interesting result is not that Grok Build is written in Rust. It is how much of the "agent" lives outside the model.
The model proposes text and structured actions, but the harness constructs its context, defines the available actions, validates arguments, asks for permission, executes tools, stores results, repairs history, compacts old context, coordinates child sessions, detects stalls, manages worktrees, and decides when a turn has actually ended. The visible personality may come from a prompt and a model, but reliability comes from a state machine surrounding them.
That state machine is also the most practical place to learn. ACP shows the session boundary. Hooks show policy decisions. JSONL shows history. A local model endpoint shows the rendered request. The ELF fills in the internal subsystem names and helps answer the remaining implementation questions.
So the original JavaScript hypothesis was wrong, but the conclusion is better: we do not need source maps to understand an agent harness. We need to identify its boundaries, choose the right observation point, and separate evidence from inference. Native code makes the last ten percent harder. It does not hide the shape of the system.
At the time of this investigation, xAI describes Grok Build as an early-beta coding agent supporting TUI, headless automation, ACP, skills, plugins, hooks, MCP, and subagents.4 The public xAI GitHub organization did not list the CLI source itself, so the local artifact and its documented protocols remain the primary material for this kind of study.5
xAI, "Enterprise Deployments", including network, configuration, authentication, sandbox, permission, and TLS details.↩︎
Agent Client Protocol, the JSON-RPC protocol used by
grok agent stdio.↩︎xAI, "Grok Build", official product and CLI overview.↩︎
xAI, "Introducing Grok Build", May 25, 2026.↩︎
xAI's public GitHub repositories, checked during the investigation on July 10, 2026.↩︎