Statusline in AI CLI Tools: Model, Limit, and Context in One Line
Documented statusline payloads for Claude Code and agy, Codex's built-in line, jq examples that do not treat missing fields as zero, and the quota/context/cost distinction.
Statusline in AI CLI Tools
I've made a habit of keeping a single line at the bottom of terminal-based AI tools: which model I'm on, how much of the usage window is left, how full the context is. Claude Code and agy hand JSON to a script; Codex's built-in field list is a different interface. The name "Grok CLI" also doesn't denote a single product and payload contract.
Why a Statusline?
In long sessions two things are easy to miss: how much of the limit is left, and the context filling up. If you keep both fixed in the terminal subtitle, you take the "should we stop now, or open a new session" decision away before the model even starts answering.
There's a third benefit too: when the model changes (whether it dropped from Opus to Sonnet, whether the effort level stayed at "high") you see it instantly in the line. Otherwise you'd have to guess from the answer's style.
Requirements
The one hard requirement for every script is jq. If it's missing:
# macOS
brew install jq
# Debian/Ubuntu
sudo apt-get install -y jq
# Fedora
sudo dnf install -y jq
# Arch
sudo pacman -S jq
# Verify
jq --version # record the installed version; a specific 1.7.x output is not requiredYou also need gawk or the system awk, plus date; both ship by default on macOS and Linux. The date on Linux wants different flags to produce a date from an epoch, which I solved with a shim in each script below.
I'd also recommend shellcheck, for scanning the scripts after you change them:
brew install shellcheck
shellcheck ~/.claude/statusline-command.shPayload Structure (General)
With Claude Code and agy the command reads JSON from stdin and prints the line to stdout. With Codex, don't assume the same JSON script contract. The Claude Code statusline reference, the agy payload guide, and the Codex configuration reference are separate contracts:
| Tool | Model | Effort | Context | Rate limit |
|---|---|---|---|---|
| Claude Code | .model.display_name | .effort.level (optional) | .context_window.used_percentage | .rate_limits.five_hour.*, .rate_limits.seven_day.*; optional .rate_limits.spend_limit.* behind a Claude apps gateway |
| agy (Antigravity) | .model.display_name | Don't assume a separate effort field | .context_window.used_percentage | model/bucket keys inside .quota |
| Codex | Built-in TUI field selection | Selectable alongside the model field | Built-in indicator | Built-in indicator if account/provider data exists |
| Grok CLI | Project- and version-dependent | Project- and version-dependent | No common schema | No common schema |
Treating a missing field as 0 shows the absence of data as "no usage at all." A numeric field should only enter the math if it really is a number; otherwise ?, or skipping the field, is more correct. Context fullness, account quota, and estimated dollar cost are different denominators; one is not derived from another.
rate_limits.spend_limit is a gateway spend cap, not a session-cost estimate. The object appears after the first API response; its percentage/reset fields require Claude Code v2.1.251 or later. The optional used_usd, limit_usd, and period fields require v2.1.284 or later on both the CLI and gateway. The script below prints only the percentage, which can exceed 100; .cost.total_cost_usd remains a separate estimated session cost.
Capture the payload briefly and with limited permission. The line below is temporarily added after input=$(cat):
(umask 077; printf '%s\n' "$input" > "$HOME/.claude/statusline-payload.json")The file may contain an email, session id, transcript path, and project directory. Before sharing, select only the fields you need; when done, remove the capture line and the file. Printing the whole JSON to stdout with tee ... | jq . also wrecks the statusline display.
Claude Code
~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-command.sh"
}
}~/.claude/statusline-command.sh - the full script that reads the payload from stdin and parses it with jq:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
model_name=$(echo "$input" | jq -r '.model.display_name // "?"')
effort_level=$(echo "$input" | jq -r '.effort.level // empty')
used_pct=$(printf '%s\n' "$input" | jq -r '.context_window.used_percentage | if type == "number" then floor else "?" end')
five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_resets=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
weekly_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
spend_pct=$(printf '%s\n' "$input" | jq -r '.rate_limits.spend_limit.used_percentage | if type == "number" then floor else empty end')
now_str=""
if [[ -n "$five_resets" && "$five_resets" != "null" ]]; then
if date -r "$five_resets" "+%H:%M" >/dev/null 2>&1; then
now_str=$(date -r "$five_resets" "+%H:%M")
else
now_str=$(date -d "@$five_resets" "+%H:%M")
fi
fi
effort_s=""
if [[ -n "$effort_level" && "$effort_level" != "null" ]]; then
effort_s=" ($effort_level)"
fi
parts="${model_name}${effort_s}"
if [[ -n "$five_pct" && "$five_pct" != "null" ]]; then
parts+=" 5h:${five_pct}%"
fi
if [[ -n "$now_str" ]]; then
parts+="(~${now_str})"
fi
if [[ -n "$weekly_pct" && "$weekly_pct" != "null" ]]; then
parts+=" w:${weekly_pct}%"
fi
if [[ -n "$spend_pct" ]]; then
parts+=" spend:${spend_pct}%"
fi
parts+=" ctx:${used_pct}%"
printf '%s\n' "$parts"Make it executable:
chmod +x ~/.claude/statusline-command.shExample format: Opus (high) 5h:34%(~14:20) w:40% spend:72% ctx:61%. This is a fixture display; the reset clock depends on the real epoch and the local time zone. The optional spend segment is a cap-usage percentage, not session cost. The documented five_hour/seven_day fields may also be absent depending on account type and version; don't assume .rate_limits.daily as a daily limit.
If you want to show the remaining percentage instead of the used percentage:
remaining=$(awk -v u="$five_pct" 'BEGIN{printf "%.0f", 100 - u}')
echo "5h remaining: ${remaining}%"Antigravity CLI (agy)
~/.gemini/antigravity-cli/settings.json:
{
"statusLine": {
"type": "command",
"command": "bash ~/.gemini/antigravity-cli/statusline-command.sh"
}
}In the agy guide, quota is an optional map from bucket/model identity to state. The example doc has gemini-weekly; it is not documented that .quota.provider or gemini-5h is always present. Write the bucket name you see together with the percentage; don't mislabel the weekly allowance as a 5-hour one.
~/.gemini/antigravity-cli/statusline-command.sh:
#!/usr/bin/env bash
set -euo pipefail
input=$(cat)
model_name=$(printf '%s\n' "$input" | jq -r '.model.display_name // "?"')
used_pct=$(printf '%s\n' "$input" | jq -r '
.context_window.used_percentage |
if type == "number" then floor else "?" end')
quota_text=$(printf '%s\n' "$input" | jq -r '
(.quota // {}) | to_entries |
map(select(.value.remaining_fraction | type == "number") |
"\(.key):\(((1 - .value.remaining_fraction) * 100) | floor)% used") |
join(" ")')
printf '%s ctx:%s%%%s\n' "$model_name" "$used_pct" "${quota_text:+ $quota_text}"remaining_fraction is a fraction: 0.66 means 66 percent remains; the used ratio is (1 - 0.66) * 100. reset_in_seconds is a duration delta, not an absolute epoch; if it's to be shown, it's added to the payload capture time. The reset_time in the payload is a separate date representation. Local time, the provider refresh time, and the age of cached quota are not the same thing.
Codex
Codex does this not with a script but with a config key: ~/.codex/config.toml
[tui]
status_line = ["model-with-reasoning", "five-hour-limit", "weekly-limit", "context-used"]Pick the supported fields from the statusline picker in the TUI; the generic codex --help doesn't have to list every field id. tui.status_line is an ordered array of field ids, not a script path. A setting that doesn't work in a version isn't fixed by a ~/.codex/statusline.sh imitating Claude Code JSON; a contract where Codex calls that script is required.
Grok
There are multiple community clients under this name. For example superagent-ai/grok-cli describes itself as a project not affiliated with xAI and documents JSON settings files. No universal ~/.grok/config.toml, [statusline], or Claude-like .cost.total_cost_usd schema follows from that.
Don't use a previous TOML/script example as an install recipe without the real renderer contract of the project you're using. If a cost field is offered, record the currency, its scope (single session/account), and whether it's an estimate or a bill. A 5-hour quota percentage cannot be calculated from an estimated dollar value. Claude Code also provides a cost field, so the claim that "only Grok shows cost" is not correct.
Missing Data, Zero Usage, and Broken JSON
Test the script detached from the CLI with three inputs. The following are synthetic examples, not a real account payload:
printf '%s\n' '{"model":{"display_name":"fixture"},"context_window":{"used_percentage":0}}' \
| bash ~/.claude/statusline-command.sh
printf '%s\n' '{"model":{"display_name":"fixture"}}' \
| bash ~/.claude/statusline-command.sh
printf '%s\n' 'not-json' \
| bash ~/.claude/statusline-command.shIn the first look for the ctx:0% vs ctx:?% distinction in the second. The third input is a parse error; because of set -euo pipefail the command may fail and the statusline may stay blank. That is a data/renderer problem, not quota running out. In diagnosis, examine stderr and the exit code separately; don't use a fallback that turns every error into 0%.
Also test bucket changes for agy:
printf '%s\n' '{"model":{"display_name":"fixture"},"quota":{"gemini-weekly":{"remaining_fraction":0.66}}}' \
| bash ~/.gemini/antigravity-cli/statusline-command.shIn this example the goal is to preserve the gemini-weekly label; don't guess the time window from the code. Then remove quota and check that the line doesn't invent an unused limit percentage.
Keeping the Renderer Cheap and Side-Effect-Free
The statusline command runs often and can be cancelled and restarted. Querying quota over the network, appending to a cost file on every render, or making changes inside the repo can count the same data twice. Format the snapshot the provider sends; if data collection is needed, it should be a separate, controlled process.
date -r <epoch> is for BSD/macOS, date -d "@<epoch>" for GNU date. Don't put the reset value into shell arithmetic before verifying it's a number/epoch. LC_NUMERIC=C prevents locale differences in printf/awk math that expects a decimal point. Bluntly cutting text with cut can split UTF-8 and ANSI escape sequences; reduce fields first, then use the renderer's terminal-width knowledge.
Keep the CLI version and the inspected schema link next to the fixtures. After an update, drawing the line isn't enough: check that 0, null, a missing field, and a different bucket name aren't confused with each other.
Related Research & Internal References
- AI Security - treating a statusline as a data-exposure surface (payload capture, session ids).
- Security Logging Design - parsing structured tool output without collapsing missing values into zero.
What do you think?
React to show your appreciation