2 2
3# Status Line3# Status Line
4 4
5The status line is an optional row at the bottom of Grok Build, above the shortcuts bar. It shows live session values, such as the model, context window usage, and cost, or the output of a script you provide. Grok sends your script the current session data as JSON, so a few lines of shell are enough to build a display of your own.5The status line is an optional row that shows live session values. It can show the model, context-window usage, cost, or the output of a script you provide.
6 6
7Use a status line to watch values that change as you work:7In the full-screen view the row sits above the shortcuts bar. In minimal mode it sits under the prompt info row. The row is hidden on the welcome screen and while a fullscreen subagent view is open. The status line is off by default.
8 8
9* How full the context window is, and how close the session is to compacting9## Configure the status line
10* The branch and repository each session is working in
11* The session's running cost
12* A value from outside the session, such as a CI run, refreshed on a timer
13 10
14The status line is off by default. The sections below describe [each mode](#set-up-the-status-line), [how Grok runs your script](#how-the-status-line-works), [every field it sends](#available-data), and [a few scripts to start from](#examples).11Add a `[ui.status_line]` section to `~/.grok/config.toml`. Restart Grok.
15 12
16## Set up the status line13The `type` key selects the mode. `builtin` shows values Grok renders. `command` runs your script. `disabled` shows nothing.
17 14
18Add a `[ui.status_line]` section to `~/.grok/config.toml` and restart Grok. The `type` key selects the mode: `builtin` shows values Grok renders itself, `command` runs your script, and `disabled` shows nothing.15Grok reads this section only from your own configuration, or from configuration your administrator manages. A cloned repository cannot set a status line.
19 16
20Because a `command` status line runs a program, Grok reads this section only from your own configuration or from configuration your administrator manages. A cloned repository cannot set one.17### Built-in items
21 18
22### Show built-in items19Items appear in the order you list them. Long values shorten with an ellipsis.
23
24The `builtin` type fills the row with named items. Items appear in the order you list them, and long values shorten to an ellipsis.
25 20
26```toml customLanguage="toml"21```toml customLanguage="toml"
27[ui.status_line]22[ui.status_line]
35| --- | --- |30| --- | --- |
36| `cwd` | The name of the current directory. |31| `cwd` | The name of the current directory. |
37| `model` | The model's display name. |32| `model` | The model's display name. |
38| `context` | Context window usage, as a percentage. The value turns amber at the auto-compaction threshold, or at 80 percent when the agent does not report one. |33| `context` | Context-window usage, as a percentage. The value turns amber at the auto-compaction threshold, or at 80 percent when the agent reports none. |
39| `cost` | The session cost. Hidden below $0.005. |34| `cost` | Cost for this Grok process. Hidden below $0.005. A resumed session counts from the resume. |
40| `turn-timer` | The elapsed time of the current turn. Appears after one second. |35| `turn-timer` | Elapsed time of the current turn, after one second. |
41| `session-name` | The session name, if set. |36| `session-name` | The session name, if set. |
42 37
43### Run your own script38### A command script
39
40Set `type` to `command`. Point `command` at a script path or an inline shell command. A leading `~/` expands to your home directory.
44 41
45Set `type` to `command` and point `command` at a script path or an inline shell command. Pipes work as written, and a leading `~/` expands to your home directory. These recipes are POSIX shell, tested on macOS and Linux; on Windows a `command` status line is untested, and the fallback that runs a `.sh` file with no valid `#!` line is not available there.42These recipes are POSIX shell. They are tested on macOS and Linux. A `command` status line is untested on Windows.
46 43
471. Save a script, for example `~/.grok/statusline.sh`, that reads JSON from standard input and prints a line. This example uses [`jq`](https://jqlang.org/) to extract two fields:441. Save `~/.grok/statusline.sh`. The script reads JSON from standard input and prints a line. This example uses [`jq`](https://jqlang.org/):
48 45
49 ```bash customLanguage="bash"46 ```bash customLanguage="bash"
50 #!/bin/sh47 #!/bin/sh
54 printf '%s │ %s%% ctx\n' "$model" "$ctx"51 printf '%s │ %s%% ctx\n' "$model" "$ctx"
55 ```52 ```
56 53
572. Make it executable with `chmod +x ~/.grok/statusline.sh`.542. Run `chmod +x ~/.grok/statusline.sh`. A file without the execute bit shows `[status line: could not start the script: …]`.
58 55
593. Set it as the status line command:563. Set the command:
60 57
61 ```toml customLanguage="toml"58 ```toml customLanguage="toml"
62 [ui.status_line]59 [ui.status_line]
64 command = "~/.grok/statusline.sh"61 command = "~/.grok/statusline.sh"
65 ```62 ```
66 63
674. Restart Grok. The row appears once the session view is active.644. Restart Grok. The row appears once a session is active.
68
69### Turn it off
70
71Set `type` to `disabled` to show nothing. `off`, `none`, and `hidden` are accepted spellings of `disabled`. Removing the `[ui.status_line]` section has the same effect, because the status line is off by default.
72
73### Options
74
75| Key | Type | Default | Description |
76| --- | --- | --- | --- |
77| `type` | string | `disabled` | `builtin`, `command`, or `disabled`. |
78| `items` | array | `["cwd", "model", "context"]` | Built-in segments, in order. |
79| `command` | string | none | The script or shell command for `type = "command"`. |
80| `padding` | integer | `0` | Horizontal spacing, in characters per side, up to 16. |
81| `refresh_interval` | integer | unset | Seconds between timed re-runs of a `command` script, from 1 to 86,400. When unset, the script runs only on session changes. Ignored under `builtin` and `disabled`, where it schedules nothing and is surfaced only by `grok inspect`. See [Refresh on a timer](#refresh-on-a-timer). |
82
83## How the status line works
84
85Grok runs your command and writes the session data to the command's standard input, as one JSON object followed by a newline. What the command writes to standard output becomes the row. Each run is a fresh process, so edits to your script apply on the next update.
86
87**When it updates.** The script runs when session state changes and continuously while a turn runs, not on a timer. State changes include:
88
89* The session starting, or a client attaching to it
90* A turn ending
91* A model or reasoning effort switch
92* A commit or a branch switch
93* A compaction
94
95Updates are spaced at least 300 milliseconds apart, so a busy turn cannot run your script constantly. A change that must appear immediately, such as a window resize, waits about 100 milliseconds. A run that is already going is never cancelled. The next change waits for it to finish. An idle session does not run your script again, so a clock in the output will not tick on its own unless you set [`refresh_interval`](#refresh-on-a-timer).
96
97**What your script can output.**
98
99* Up to five lines, each cut at 1024 characters. Escape sequences count toward the limit. On a short terminal, extra lines drop from the bottom.
100* ANSI colors are supported. Every other escape sequence, such as cursor movement, is removed.
101* OSC 8 hyperlinks are supported for `http`, `https`, and `mailto` targets. Other targets render as plain text.
102* Output past 64 KiB is truncated and the script is stopped.
103* A script that succeeds and prints nothing removes the row for that update rather than falling back to the built-in items.
104 65
105**How big the row is.** The `COLUMNS` and `LINES` environment variables describe the row your output fills, not the terminal window. Padding is already deducted. `LINES` reads `1` until you print more, and never exceeds five.66The script receives one JSON object on standard input. Common fields are `model.display_name`, `context_window.used_percentage`, `workspace.branch`, and `cwd`. Grok omits a field it cannot determine. Guard missing keys (`// "?"` in jq).
106 67
107**The environment your script runs in.** Scripts run in the session's working directory. When that is unavailable, Grok uses the repository root, then its own directory. Each run has a 10-second limit, and a script that exceeds it shows `[status line: timed out]`. No shell startup files run: Grok clears `BASH_ENV` and `ENV`, though a shell may still read its own environment file, such as zsh with `~/.zshenv`, so keep expensive setup out of those. `GIT_OPTIONAL_LOCKS=0` is set so read-only `git` commands skip taking the optional index lock and do not collide with the agent's own git operations. When a run ends, for any reason, Grok terminates every process the script started, including processes left in the background.68An idle session does not re-run the script. Set `refresh_interval` (seconds) on a `command` row to also run it on a timer. The key does nothing under `builtin` or `disabled`. `grok inspect` still reports it there.
108 69
109## Refresh on a timer70Test the script before you configure it:
110
111Set `refresh_interval` on a `command` status line to also re-run the script on a fixed schedule. This lets a value from outside the session, such as a CI result, reach the row while the session is idle.
112
113```toml customLanguage="toml"
114[ui.status_line]
115type = "command"
116command = "~/.grok/statusline.sh"
117refresh_interval = 300 # seconds
118```
119
120* The `trigger` field in the JSON says why the script ran: `"refresh_interval"` for a timed run, `"state"` otherwise. A script that calls a network service should fetch on timed runs and read a cached copy on state runs. A busy turn re-runs the script continuously; those are state runs, except that when a timed fire comes due mid-turn, the run that carries it is stamped `"refresh_interval"`.
121* A timed run receives the session data from the most recent state change, so values such as cost and context usage are as of that change. Only what your script fetches itself is current.
122* When a timed run fails or times out, the row keeps its last output rather than showing an error, so an unreliable service does not disturb it. The exception is a script that has not answered yet, as in a fresh session or right after switching agents, where the first failure shows at once because there is nothing to keep. After three consecutive failures, the error shows regardless. A run triggered by session state reports its failure immediately.
123* Timed runs that come due while the row is hidden, or while an earlier run is still going, combine into a single run. Grok never runs the script several times in a row to catch up.
124
125## Available data
126
127Grok writes one JSON object to your script's standard input. The table below lists every field it can contain; nothing else is sent.
128
129| Field | Description |
130| --- | --- |
131| `cwd`, `session_id` | The working directory and the unique session identifier. |
132| `session_name` | The session's tab name, if set. |
133| `prompt_id` | The UUID of the prompt being processed. Present only during a turn. |
134| `transcript_path` | The path to the session's `updates.jsonl`. |
135| `model.id`, `model.display_name` | The model identifier and display name. |
136| `workspace.current_dir` | The current directory. |
137| `workspace.repo_root` | The repository root. Absent outside a repository. |
138| `workspace.branch` | The checked-out branch. Absent on a detached HEAD. |
139| `workspace.git_worktree` | The worktree name, inside a linked worktree. |
140| `workspace.repo.{host,owner,name}` | The repository identity, parsed from the `origin` remote. |
141| `version`, `schema_version` | The Grok release, and the revision of this JSON shape. Test `schema_version` with `>=`. |
142| `cost.total_cost_usd` | The session cost, counted from when this Grok process opened or resumed the session, so a resumed session counts from the resume. Absent until the session incurs a cost, so treat absence as unknown rather than zero. |
143| `cost.total_duration_ms`, `.total_api_duration_ms` | Milliseconds since this Grok process opened or resumed the session, and milliseconds spent waiting on the API. |
144| `context_window.context_window_size` | The maximum context size, in tokens. |
145| `context_window.context_tokens` | The tokens the conversation currently occupies, counting input only. The value can fall after a compaction. |
146| `context_window.used_percentage`, `.remaining_percentage` | Current context window usage, as whole numbers from 0 to 100. |
147| `context_window.session_input_tokens`, `.session_output_tokens` | Token totals across the session so far, counted from when this process opened or resumed it. The values only grow. |
148| `context_window.session_usage.{input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens}` | The session totals broken out. The input parts sum to `session_input_tokens`. |
149| `context_window.auto_compact_threshold_percent` | The usage percentage where the session auto-compacts. Absent when the agent does not report one. |
150| `effort.level` | The reasoning effort, when the model supports it. |
151| `turn.started_at_ms` | The Unix time, in milliseconds, when the current turn began. Absent between turns. |
152| `worktree.{name,path,branch,main_worktree_root}` | The active worktree, inside a linked worktree. `main_worktree_root` is the repository it branched from. |
153| `trigger` | Why this run happened: `refresh_interval` for a timed run, `state` otherwise. |
154
155The JSON with every field present looks like this:
156
157```json customLanguage="json"
158{
159 "schema_version": 1,
160 "cwd": "/home/user/project",
161 "session_id": "019fa651-6d59-7c83-a4f3-5a391e6901a1",
162 "session_name": "add status line",
163 "prompt_id": "97135ed2-71a5-4581-b959-3341bbd03e5f",
164 "transcript_path": "/home/user/sessions/019fa651/updates.jsonl",
165 "model": { "id": "grok-4.5", "display_name": "Grok 4.5" },
166 "workspace": {
167 "current_dir": "/home/user/project",
168 "repo_root": "/home/user/project",
169 "branch": "main",
170 "git_worktree": "feature-x",
171 "repo": { "host": "github.com", "owner": "owner", "name": "repo" }
172 },
173 "version": "0.2.112",
174 "cost": {
175 "total_cost_usd": 0.0123,
176 "total_duration_ms": 45000,
177 "total_api_duration_ms": 2300
178 },
179 "context_window": {
180 "context_window_size": 500000,
181 "context_tokens": 40000,
182 "session_input_tokens": 52000,
183 "session_output_tokens": 9500,
184 "session_usage": {
185 "input_tokens": 10000,
186 "output_tokens": 9500,
187 "cache_creation_input_tokens": 2000,
188 "cache_read_input_tokens": 40000
189 },
190 "used_percentage": 8,
191 "remaining_percentage": 92,
192 "auto_compact_threshold_percent": 80
193 },
194 "effort": { "level": "high" },
195 "turn": { "started_at_ms": 1730000000000 },
196 "worktree": {
197 "name": "feature-x",
198 "path": "/home/user/wt/feature-x",
199 "branch": "feature-x",
200 "main_worktree_root": "/home/user/project"
201 },
202 "trigger": "refresh_interval"
203}
204```
205
206Grok omits a field it cannot determine rather than sending a placeholder value, so an absent field is never a zero. Handle absent fields in your script: `jq -r` prints the literal text `null` for a missing key, so write `// 0` or `// "?"` in jq, and use `?.` in JavaScript.
207
208## Examples
209
210### The current branch
211
212The simplest status line is an inline command. Scripts run in the session's working directory, so this shows the checked-out branch:
213
214```toml customLanguage="toml"
215[ui.status_line]
216type = "command"
217command = "git branch --show-current"
218```
219
220### A session summary
221
222The session data does not carry a count of changed files, so this script reads one from `git` and combines it with fields from the JSON:
223
224```bash customLanguage="bash"
225#!/bin/sh
226payload=$(cat)
227dir=$(printf '%s' "$payload" | jq -r '.workspace.current_dir | split("/") | last')
228model=$(printf '%s' "$payload" | jq -r '.model.display_name // "?"')
229ctx=$(printf '%s' "$payload" | jq -r '.context_window.used_percentage // 0')
230branch=$(printf '%s' "$payload" | jq -r '.workspace.branch // "no branch"')
231changed=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
232printf '%s │ %s │ %s%% ctx │ \033[32m%s\033[0m ±%s\n' "$dir" "$model" "$ctx" "$branch" "$changed"
233```
234
235### An external status on a timer
236
237With `refresh_interval` set, this script fetches the latest CI result on timed runs and reads a cached copy on state runs, so a busy turn does not become a stream of network requests:
238 71
239```bash customLanguage="bash"72```bash customLanguage="bash"
240#!/bin/sh73~/.grok/statusline.sh <<'JSON'
241payload=$(cat)74{"workspace": {"current_dir": "/tmp/demo", "branch": "main"}, "model": {"display_name": "Grok 4.5"}}
242trigger=$(printf '%s' "$payload" | jq -r '.trigger // "state"')75JSON
243session=$(printf '%s' "$payload" | jq -r '.session_id')
244cache="/tmp/statusline-ci-$session"
245
246if [ "$trigger" = "refresh_interval" ] || [ ! -f "$cache" ]; then
247 # Only overwrite the cache when the fetch succeeds with output, so a
248 # failed or timed-out run keeps the last result instead of blanking it.
249 if result=$(gh run list --limit 1 --json status,conclusion \
250 --jq '.[0].conclusion // .[0].status' 2>/dev/null) && [ -n "$result" ]; then
251 printf '%s' "$result" > "$cache"
252 fi
253fi
254[ -f "$cache" ] && printf 'CI: %s\n' "$(cat "$cache")"
255```76```
256 77
257## Tips78### Disable the status line
258
259* Run your script by hand before configuring it, with sample JSON on standard input:
260 79
261 ```bash customLanguage="bash"80Set `type` to `disabled`. `off`, `none`, and `hidden` mean the same. Removing the `[ui.status_line]` section also disables the row.
262 ./statusline.sh <<'JSON'
263 {"workspace": {"current_dir": "/tmp/demo", "branch": "main"}, "model": {"display_name": "Grok 4.5"}}
264 JSON
265 ```
266
267* A script that runs `git` on every update can lag in a large repository. Write the result to a file under `/tmp` named after `session_id`, and reuse it until it ages out. The identifier does not change during a session, and no two sessions share one, so concurrent sessions keep separate caches.
268
269* Prefer `printf` over `echo -e`. Shells disagree about how `echo` treats escape sequences.
270
271* Keep lines short. The row does not wrap, and a line longer than the row is cut.
272 81
273## Troubleshooting82## Troubleshooting
274 83
275**Nothing shows.** Grok reads `[ui.status_line]` at startup, so restart it after editing `config.toml`. Check that `type` is not `disabled`, that a script is executable, and that it writes to standard output. The row renders once a session is active, in both the full-screen interface and minimal mode. It does not appear on the welcome screen or while a subagent view is open full screen.84Grok reads `[ui.status_line]` at startup. Restart Grok after you edit `config.toml`.
276
277**A message beginning `[ui.status_line]` fills the row.** Grok could not use the section as written. The message names the key it could not read, or what the chosen mode still needs. `grok inspect` lists the same problems. Fix the named key, or set `type = "disabled"` to remove the row and the message.
278 85
279**A script error shows.** Anything your script prints is displayed even when it exits with a nonzero status. A script that prints nothing and fails shows `[status line: exit N]` until the next successful run. A script Grok cannot start, such as a file without the execute bit, shows `[status line: could not start the script: …]`, and one the system kills shows `[status line: killed by signal]`. Standard error is never displayed. Run Grok with `--debug` to read it.86`grok inspect` lists problems in the section. A row that begins with `[ui.status_line]` names the key Grok could not read.
280 87
281**A setting in a repository has no effect.** Only your own `~/.grok/config.toml`, or configuration your administrator manages, can set a status line. A repository's local configuration cannot, because a `command` status line names a program your machine would run.88A script that prints nothing and fails shows `[status line: exit N]`. A timeout shows `[status line: timed out]`. A kill shows `[status line: killed by signal]`. A spawn failure, including a missing execute bit, shows `[status line: could not start the script: …]`. Standard error is never shown. Run Grok with `--debug` to read it.