Driving grok with an agent
Verified against grok 1.0.5 (5115b46bc9) [stable] (grok --version / grok version --json). Sanity-check flags against grok --help before relying on them — grok evolves.
This skill is engine mechanics only. Shared orchestration doctrine (lane scaffolding, turn prompts, verification, hand-patch policy) lives elsewhere.
Trap: a positional
PROMPTstarts the interactive TUI, even with--output-formatset. Headless requires-p/--single,--prompt-file, or--prompt-json.
1. Canonical unattended launch
cd <LANE_DIR> && grok -p "$(cat prompts/turns/03.md)" \
--output-format json \
--always-approve \
--permission-mode bypassPermissions \
--max-turns 40 \
--cwd <LANE_DIR> \
> build/turn-logs/03.log 2>&1; echo "EXIT $?" >> build/turn-logs/03.log
PowerShell:
Set-Location <LANE_DIR>
grok -p (Get-Content -Raw prompts\turns\03.md) --output-format json --always-approve --max-turns 40 --cwd <LANE_DIR> *>&1 | Tee-Object build\turn-logs\03.log
"EXIT $LASTEXITCODE" | Add-Content build\turn-logs\03.log
That is the one-shot agentic loop: one user prompt, full tool use, process exits when the turn ends. -p is labelled --single but it is not a no-tools completion — --max-turns caps agentic model rounds (tool loop), range 1..=4294967295.
Headless triggers vs TUI trap
| Invocation | Mode | Empirically |
|---|---|---|
grok -p "…" |
headless, exits | EXIT 0, JSON on stdout |
grok --prompt-file PATH |
headless, exits | same |
grok --prompt-json '[{…}]' |
headless, exits | same |
grok --output-format json "…" (positional PROMPT, no -p) |
TUI | alt-screen; does not exit |
grok --output-format json --max-turns 1 --always-approve "…" |
TUI | --max-turns ignored in TUI |
--output-format only formats a headless run. It does not select headless.
grok agent stdio is a long-lived ACP server (JSON-RPC on stdin/stdout) for IDEs/SDKs, not a polling one-shot. Use -p for bounded turns.
Approval / permission
Unattended runs must skip interactive approval:
| Flag | Effect |
|---|---|
--always-approve |
Auto-approve tool executions. Hidden alias --yolo is accepted by the parser but not listed in grok --help. |
--permission-mode bypassPermissions |
Same always-approve mode. Parser values: default, acceptEdits, auto, dontAsk, bypassPermissions, plan. |
--allow <RULE> / --deny <RULE> |
Repeatable permission rules (Bash(…), Edit(…), Write(…), Read(…), Grep(…), WebFetch(…), MCPTool(…)). Deny wins. Compat aliases: --allowedTools, --disallowedTools. |
--tools <IDS> |
Headless allowlist of built-in tools (comma-separated). |
--disallowed-tools <IDS> |
Headless denylist. Supports Agent / Agent(explore) to block subagents. |
--sandbox <PROFILE> |
Optional OS sandbox (GROK_SANDBOX). Help lists the flag; built-in names in the install docs: off (default), workspace, devbox, read-only, strict. Enforcement is Landlock (Linux) / Seatbelt (macOS). On this Windows host --sandbox workspace and even --sandbox notaprofile still ran EXIT 0 — do not assume Windows enforces. |
--always-approve still honors deny rules, hooks, and admin locks. auto in a non-interactive session blocks some calls and reports the block to the model rather than prompting — use always-approve for CI.
Tradeoff: always-approve is what makes a polling orchestrator possible; pair it with --deny / --disallowed-tools / --sandbox when the lane is untrusted. dontAsk / default will stall or deny writes.
Working directory
--cwd <PATH>sets the workspace. Missing path → EXIT 1 (Failed to set working directory … os error 2).- Session files are grouped by URL-encoded cwd under
~/.grok/sessions/. Example:D:\dev\omp-agent-skill\work\grok-lane→~\.grok\sessions\D%3A%5Cdev%5Comp-agent-skill%5Cwork%5Cgrok-lane\<session-id>\. - Project-root discovery walks up from cwd to
.git. Nested--cwdinside a huge repo loads that repo's AGENTS.md/skills. --worktreeexists for the TUI; help text: headless (-p) does not create a worktree from this flag.
Always pass --cwd <LANE_DIR> and launch from that directory so session grouping and tool paths match.
Stdin
Headless does not splice piped stdin into the prompt. Empirically: piping SECRET_STDIN_PAYLOAD_XYZ into grok -p "… Else reply STDIN_EMPTY" printed STDIN_EMPTY.
Pass prompt text via -p, --prompt-file, --prompt-json, or shell substitution (-p "$(cat file)"). Unlike omp, </dev/null is not required to prevent a stdin block on -p. (A positional PROMPT still opens the TUI and will block.)
Output, completion, polling
--output-format (default plain): plain | json | streaming-json | streaming-messages-json.
json (best for accounting) — one object after the turn, stdout silent until then:
{
"text": "PONG",
"stopReason": "end_turn",
"sessionId": "01a039de-10ac-7b12-a151-8df7538091ca",
"requestId": "…",
"usage": {
"input_tokens": 17703,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
"output_tokens": 32,
"reasoning_tokens": 26,
"total_tokens": 17735
},
"num_turns": 1,
"total_cost_usd": 0.00605166,
"modelUsage": { "grok-4.6-build": { "inputTokens": 17703, "outputTokens": 32, "modelCalls": 1, "costUSD": 0.00605166 } }
}
On failure grok still often emits JSON (or {"type":"error","message":"…"}) and a stderr Error: … line.
streaming-json — NDJSON while the turn runs (text, thought, tool_call, tool_call_update, usage, end, error). Last line is end or error. Prefer this when the orchestrator must see liveness in the log file; json does not grow until exit.
streaming-messages-json — Anthropic Messages stream-json shape. --include-partial-messages only affects this format.
Completion signal: run the whole line as a background task and poll for EXIT in the log (the echo "EXIT $?" sentinel). Also parse stopReason / "type":"end" if using JSON formats. Never a bare long sleep.
No wall-clock cap. --max-time is not a flag (parser: unexpected argument; tip: --max-turns). --max-turns is a turn cap, not a timer. Config inference_idle_timeout_secs / session inferenceIdleTimeoutConfiguredSecs (3600 on this install) is per-inference idle, not a run deadline. The orchestrator must impose an external timeout (kill the process, then salvage).
Useful extras (all in grok --help)
| Flag | Role |
|---|---|
-m, --model <ID> |
Model for this run (grok models on this install: grok-4.6 default, grok-4.5) |
--reasoning-effort / --effort |
Runtime error for ultra listed: xhigh, high, medium, low |
--rules <TEXT> |
Extra rules appended to the system prompt |
--system-prompt-override |
Replace the agent system prompt (compat --system-prompt) |
--verbatim |
Send the prompt exactly as given |
--no-subagents / --no-plan |
Disable those features |
--disable-web-search |
Drop web search and web fetch |
--agent <NAME> |
Agent name or definition file |
--debug / --debug-file <FILE> |
Debug logging (see §4) |
Auth for CI: XAI_API_KEY, or grok login --device-auth (alias --device-code). Cached ~/.grok/auth.json is used if already logged in.
2. Configuration
Home is ~/.grok, not ~/.codex. Override with GROK_HOME. There is no Codex-style -c key=value override (-c is --continue). There is no --oss flag.
User config: ~/.grok/config.toml. Project overlay (MCP / plugins / permission only): <repo>/.grok/config.toml. Inspect what a directory will load: grok inspect --json.
Precedence (from install docs): CLI flags > env vars > requirements/MDM > GROK_CONFIG / GROK_CONFIG_PATH overlay > ~/.grok/config.toml > managed config > defaults.
Model selection
# ~/.grok/config.toml
[models]
default = "grok-4.6"
default_reasoning_effort = "high"
grok models # catalog + default
grok -p "…" -m grok-4.5 # per-run
Agent "profiles" are agent definitions, not Codex config profiles:
--agent <NAME_OR_PATH>/--agent-profile <PATH>(agent subcommand)[agent] name = "…"in config, orGROK_AGENT
Launcher overlay without editing config.toml (1.0.5): GROK_CONFIG='{"models":{"default_reasoning_effort":"high"}}' or GROK_CONFIG_PATH (JSON/TOML). Allowlisted soft settings only.
Local / BYOK providers
No --oss. Point a [model.<name>] at any OpenAI-compatible /v1 (Ollama, LM Studio, vLLM, llama.cpp, llama-swap, …):
# Ollama — example from the install README
[model.ollama-codellama]
model = "codellama"
base_url = "http://localhost:11434/v1"
name = "CodeLlama (Ollama)"
# Any other local OpenAI-compatible server (LM Studio, vLLM, llama.cpp, …)
[model.local-llama]
model = "llama-3.1-70b"
base_url = "http://localhost:8080/v1"
name = "Local Llama"
api_backend = "chat_completions" # default; also "responses" | "messages"
# env_key = "LOCAL_API_KEY" # optional; string or array
# context_window = 128000
Then grok -p "…" -m ollama-codellama. api_key > env_key > session token > XAI_API_KEY. Never print or commit keys.
Fleet-wide custom catalog: GROK_MODELS_BASE_URL / [endpoints] models_base_url.
3. Attachments
No --image, --attach, --file, or --add-dir flags (all rejected as unexpected arguments). TUI @path fuzzy-attach is interactive-only.
Headless ways to hand the builder files:
- Put files in
--cwdand name the paths in the prompt — the agentread_files them. This is the reliable pattern for text/code/reference dumps. --prompt-file PATH— the prompt text is the file contents (not a binary attachment). Missing file → EXIT 1.--prompt-json— ACP content blocks. Parser-accepted variants (errors quoted from this binary):
type |
Required fields | Notes |
|---|---|---|
text |
text |
|
image |
data (base64), mimeType |
Missing data / mimeType rejected. Bad bytes: model reports unsupported format. |
audio |
data, mimeType |
|
resource_link |
uri, name |
File reference; model may then read_file. |
resource |
resource object |
Accepted shape: {"uri","text","mimeType"} (embedded text). Empty {} rejected. |
Array form: [{"type":"text","text":"…"},{"type":"image","data":"<b64>","mimeType":"image/png"}].
Object form: {"type":"acp","content":[…]} (bare object needs type + content; empty array → content blocks array is empty).
PowerShell mangles JSON quotes to native executables; pass --prompt-json via Python/cmd or write JSON to a file and construct the argv without the shell eating ".
--prompt-file / --prompt-json are headless triggers (no -p needed). Combine with --always-approve --max-turns --output-format.
4. Sessions & accounting
On-disk layout
~/.grok/sessions/<url-encoded-cwd>/<session-id>/ (observed on this install):
| File | What it is |
|---|---|
summary.json |
id, cwd, timestamps, last_active_at, model, title |
updates.jsonl |
ACP session stream (authoritative transcript) |
events.jsonl |
Turn/MCP/phase events (turn_started, turn_ended, …) |
chat_history.jsonl |
Raw messages sent to the model |
signals.json |
Session counters (contextTokensUsed, toolCallCount, turnCount, toolsUsed) |
terminal/*.log |
Captured shell-tool output |
--debug-file FILE |
TRACE/DEBUG log at that path (empirically ~85KB for a 1-turn ping) |
--debug (no path) wrote ~/.grok/debug/<session-id>.txt on this install. Also ~/.grok/logs/unified.jsonl and ~/.grok/logs/mcp/. GROK_LOG_FILE / RUST_LOG from install docs.
grok sessions list -n 20 lists cwd sessions (id, created, updated, summary). grok export <SESSION_ID> dumps Markdown.
Each grok -p creates a new session by default. Capture sessionId from JSON.
Liveness (polling orchestrator)
json stdout is silent until exit — do not treat a quiet log as death.
- Process still running (the pid you spawned).
~/.grok/active_sessions.jsonentries look like{session_id, pid, cwd, opened_at}— can retain a dead pid; confirm the pid is alive.- Session files growing:
events.jsonl/updates.jsonlmtime,summary.jsonlast_active_at.events.jsonlphase_changed(waiting_for_model,streaming_reasoning,streaming_text) is a live heartbeat.turn_endedmeans that turn finished inside the process. - Prefer
--output-format streaming-jsonso the log itself grows (tool_call/text/end).
Stale session files + dead pid + no EXIT in the orchestrator log = silent death. Salvage the tree; resume if needed.
Token usage per run
Best source: the headless json object (or streaming-json end / per-response usage lines). Fields observed: input_tokens (uncached), cache_read_input_tokens, cache_creation_input_tokens, output_tokens, reasoning_tokens, total_tokens, num_turns, modelUsage, total_cost_usd (omitted when the server does not stamp cost).
signals.json is session-level context-window usage (contextTokensUsed / contextWindowTokens), not a clean per-run bill.
events.jsonl did not carry a token-usage event on the ping run (turn_ended only).
Sum usage from each -p JSON result. Treat as approximate if usage_is_incomplete is true (documented for subagent drain).
5. Failure modes & exit codes
Determined on this binary (plus SIGINT/SIGTERM codes from the install headless doc, not re-signaled here):
| Code | When (observed or documented) |
|---|---|
0 |
Turn completed (stopReason: "end_turn") |
1 |
Runtime/app error: max turns, missing session, bad --prompt-json, missing --cwd/--prompt-file, unknown --reasoning-effort, --fork-session without resume, --session-id not a UUID |
2 |
Clap parse error (invalid --output-format, --permission-mode, --max-turns 0, unknown flags like --oss) |
130 / 143 |
SIGINT / SIGTERM (install docs; session saved through last completed tool; files not rolled back) |
--max-turns hit: stderr Error: max turns reached, JSON still written, stopReason: "cancelled", num_turns equals the cap, EXIT 1. Work already done on disk stays.
Resume / fork / continue
| Flag | Semantics (this version) |
|---|---|
-c, --continue |
Most recent session for this cwd. None → EXIT 1 No session found for current directory. |
-r, --resume [<ID_OR_TITLE>] |
Resume by UUID (always ID-shaped) or cwd title. Missing UUID tried remote then 404 → EXIT 1. |
-s, --session-id <UUID> |
New session UUID only. Not a UUID → EXIT 1. Does not resume (old upsert is gone). With -r/-c only valid together with --fork-session. |
--fork-session |
With -r/-c, new session id (optionally named by -s). Without -r/-c → EXIT 1 --fork-session requires --resume or --continue. |
--restore-code |
Resume restores the original repo snapshot; remote resumes need --worktree. Without it, resume is conversation-only. |
Salvage a dead/capped run:
# Inspect what landed, then continue the same conversation
grok -p "Repo state is <files+APIs>. Do not rewrite. Remaining scope: <list>" \
--resume <sessionId> --output-format json --always-approve --max-turns 40
# Or fork so the original transcript stays intact
grok -p "…" --resume <sessionId> --fork-session --output-format json --always-approve --max-turns 40
A fresh -p without -r/-c is a new context. Repo + NOTES.md (or equivalent) is the only memory unless you resume.
Verified gotcha (grok 1.0.5): files created by built-in skills like
/imagineare saved under the SESSION directory —~/.grok/sessions/<url-encoded-cwd>/<session-id>/images/…— not the working directory, even with--cwdset. An orchestrator must fish outputs from the newest session dir.