Claude Code Statusline Script Errors, Blank, or Hangs

Your Claude Code statusLine shows an error, stays blank, or stalls the prompt. Fix the JSON-on-stdin contract, missing jq, exit codes, slow git calls, and workspace trust.

You wired a custom statusLine to show git branch, model name, and context usage in your Claude Code prompt. The first run looked fine. Then it broke: the bar goes blank, shows literal text, or the prompt visibly stalls before each turn. As of June 2026, the single most common cause is that the script never receives what it expects, because Claude Code does not pass data through environment variables like $CLAUDE_MODEL. It pipes a JSON object to your script on stdin, and your script must read stdin and parse it (almost always with jq). If jq is missing, or your script reads env vars that do not exist, the bar silently goes blank with no error.

Fastest fix: confirm jq is installed (which jq), then feed your script the real input shape and see what it prints:

echo '{"model":{"display_name":"Opus 4.7"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":25},"session_id":"test"}' | ~/.claude/statusline.sh

If that prints nothing, errors, or hangs, the bug is in your script, not in Claude Code. The rest of this guide diagnoses each case.

How the statusline actually works

This is the part most broken scripts get wrong. Claude Code runs your command and pipes JSON session data to it on stdin. Your script reads that JSON, extracts the fields it wants, and prints a line to stdout. Whatever lands on stdout becomes the bar.

A minimal correct script (from the official docs):

#!/bin/bash
input=$(cat)                                            # read JSON from stdin
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
echo "[$MODEL] ${DIR##*/} | ${PCT}% context"

The settings block must set type to "command":

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 0
  }
}

Key field names you will use (full schema in the official statusline docs):

FieldWhat it is
.model.display_nameHuman-readable model name, e.g. Opus 4.7
.model.idModel identifier, e.g. claude-opus-4-7
.workspace.current_dirCurrent working directory (.cwd is the same value)
.workspace.project_dirDirectory where Claude Code was launched
.context_window.used_percentagePre-computed context-used percentage (may be null early)
.cost.total_cost_usdEstimated session cost in USD
.session_idStable per-session ID, ideal for cache filenames
.versionClaude Code version

When it runs: after each new assistant message, after /compact finishes, on permission-mode change, and on vim-mode toggle. Updates are debounced at 300ms. There is no separate “soft timeout” that kills slow scripts after 1-2 seconds. Instead, if a new update fires while your script is still running, the in-flight run is cancelled. A consistently slow script therefore produces a stale or blank bar rather than an error.

Common causes

Ordered by how often they actually bite, as of June 2026.

1. jq (or bc) is not installed

The /statusline generator and almost every example script shell out to jq. If jq is missing, the script fails on its first command and the bar goes blank with no warning. This is the most common silent failure on Windows (Git Bash) and minimal Linux/Docker images, where jq and bc are not installed by default.

How to spot it: which jq returns nothing, or running the script by hand prints jq: command not found on stderr.

2. Script reads env vars instead of stdin

Older or copy-pasted scripts reference $CLAUDE_MODEL, $CLAUDE_SESSION_ID, etc. Those are not provided. The data only arrives on stdin as JSON. A script that never runs input=$(cat) (or equivalent) gets empty values and prints a blank or unknown bar.

How to spot it: the script never reads stdin; the bar shows your fallback string (unknown, empty) regardless of model or directory.

3. Script exits non-zero or prints nothing

A non-zero exit or empty stdout blanks the bar. Classic trigger: set -e plus git rev-parse --abbrev-ref HEAD in a non-git directory, which dies with exit 128.

How to spot it: echo $? after running the script by hand is non-zero, or claude --debug logs a non-zero statusline exit and stderr.

4. Script too slow (stalls / gets cancelled)

A curl to a remote API, a git fetch, or a recursive find/git status in a large repo takes long enough that the next 300ms update cancels the in-flight run. The bar shows stale content or never updates.

How to spot it: time ./statusline.sh (with mock input piped in) takes more than ~300ms; you feel the prompt lag before turns.

5. Fields are null before the first API response

.context_window.used_percentage, .context_window.remaining_percentage, and .context_window.current_usage can be null until the first API call lands (and again right after /compact). A script that does math on null errors or prints --.

How to spot it: the bar shows --, null, or empty numbers right after launch, then fixes itself after your first message.

6. Workspace trust not accepted

Because statusLine runs a shell command, it is gated behind the same workspace-trust acceptance as hooks. If trust was not accepted for the current directory, the command does not run.

How to spot it: instead of your output you see the notice statusline skipped · restart to fix.

7. disableAllHooks is set, or the file/permissions are wrong

disableAllHooks: true in settings also disables the statusline. Separately, statusLine.command may point to a missing file, or the file may lack the execute bit. On Windows with Git Bash, backslashes in the command path get eaten as escapes before the script runs.

How to spot it: ls -l <path> shows a missing file or no +x; claude --debug shows a no-such-file error; or the path uses backslashes on Windows.

8. Escape sequences leak as literal text

Raw ANSI color codes work in some terminals and appear as literal \033[31m (or \e]8;;) in others. Embedded newlines split the bar across rows.

How to spot it: the bar shows literal escape codes, or breaks where it should be one line.

Which bucket are you in?

SymptomMost likely causeGo to
Bar is blank, which jq failsjq/bc missingStep 1
Bar shows unknown/empty regardless of contextScript reads env vars, not stdinStep 2
Bar shows -- or null only at launch, then recoversNull fields pre-first-responseStep 3
statusline skipped · restart to fix noticeWorkspace trust not acceptedStep 4
Prompt lags before each turnSlow git/network callStep 5
Literal \033[ or split linesEscape-sequence handlingStep 6
Bar never appears at allMissing file, no +x, or disableAllHooksStep 7

Before you start

  • Note whether the bar is blank, shows error/literal text, or has wrong content.
  • Note whether it is constant or intermittent (intermittent often means a network call inside the script).
  • Find the script path from ~/.claude/settings.json (or project .claude/settings.json) under statusLine.command.
  • Confirm dependencies: which jq (and which bc if your script uses it).

Step-by-step fix

Always test by piping mock JSON in. If it does not work standalone, it will not work in the prompt.

Step 1: Install dependencies, then run with mock input

which jq || echo "jq is MISSING"

echo '{"model":{"display_name":"Opus 4.7"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":25},"cost":{"total_cost_usd":0.12},"session_id":"test"}' \
  | ~/.claude/statusline.sh 1>/tmp/sl.out 2>/tmp/sl.err
echo "exit=$?"
echo "--- stdout ---"; cat /tmp/sl.out
echo "--- stderr ---"; cat /tmp/sl.err

If jq is missing, install it (brew install jq on macOS, apt-get install jq on Debian/Ubuntu, winget install jqlang.jq or choco install jq on Windows). Three checks on the output: exit code 0, non-empty stdout, silent stderr. If any fails, fix the script before touching Claude Code.

Step 2: Read stdin as JSON, not env vars

Make sure the very first thing the script does is consume stdin, and that every field comes from jq:

#!/usr/bin/env bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name // "unknown"')
dir=$(echo "$input" | jq -r '.workspace.current_dir // "?"')
branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")
printf "%s | %s | %s" "$model" "${dir##*/}" "$branch"
exit 0

Notes: the // "unknown" jq fallbacks keep the bar populated even when a field is absent. The explicit exit 0 is defensive against the last command failing. Pass -C "$dir" to git so it works regardless of where Claude spawned the shell.

Step 3: Handle null fields gracefully

Use // 0 (numbers) or // empty (optional fields) so pre-first-response nulls never break math:

pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
five_h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')

rate_limits is only present for Claude.ai Pro/Max sessions after the first API response, so // empty is the correct guard there.

Step 4: Accept workspace trust

If you see statusline skipped · restart to fix, the directory was never trusted. Restart Claude Code in that directory and accept the trust prompt. Also confirm disableAllHooks is not true in your settings, because it disables the statusline along with hooks.

Step 5: Keep it fast — cap blocking calls and cache

Wrap anything that might block, and never call a network API synchronously:

branch=$(timeout 0.3s git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?")

For slow git or remote data, cache to a temp file keyed by .session_id (stable per session, unlike $$):

SESSION_ID=$(echo "$input" | jq -r '.session_id')
CACHE="/tmp/statusline-git-cache-$SESSION_ID"
if [ ! -f "$CACHE" ] || [ -n "$(find "$CACHE" -mmin +0.1 2>/dev/null)" ]; then
  git -C "$dir" branch --show-current 2>/dev/null > "$CACHE.tmp" && mv "$CACHE.tmp" "$CACHE"
fi
branch=$(cat "$CACHE" 2>/dev/null || echo "?")

Because updates are debounced at 300ms and a new trigger cancels an in-flight run, anything slower than a couple hundred milliseconds will leave you with a stale bar.

Step 6: Fix escape-sequence output

If you do not need color, strip control characters so nothing leaks:

printf "%s | %s" "$model" "$branch" | tr -d '\n\r\t'

If you do want color or clickable OSC 8 links, prefer printf '%b' over echo -e for reliable escape interpretation across shells, and confirm your terminal supports it (Terminal.app, for example, does not render OSC 8 links). For multi-line bars, each separate echo/print becomes its own row by design.

Step 7: Fix permissions, shebang, and path

chmod +x ~/.claude/statusline.sh
head -1 ~/.claude/statusline.sh    # expect #!/usr/bin/env bash or #!/bin/bash

Test with the interpreter explicitly; if this works but invoking the script directly fails, the shebang is wrong:

echo '{"model":{"display_name":"Opus 4.7"}}' | /usr/bin/env bash ~/.claude/statusline.sh

On Windows, write the command path with forward slashes (C:/Users/you/.claude/statusline.sh), because Git Bash consumes backslashes as escapes and the command silently fails.

How to confirm it’s fixed

  • Restart Claude Code; the bar should populate within a few hundred milliseconds and update after your first message.
  • cd into a directory that is not a git repo and confirm the bar still renders (your no-git fallback path works).
  • Disconnect from the network and confirm the bar still renders (cache / no-sync path works).
  • Pipe mock input from several different cwds; each run should exit 0, print under ~200 characters, and finish in well under 300ms.

Long-term prevention

  • Always start the script with input=$(cat) and pull every value from that JSON via jq; never depend on environment variables.
  • Keep the script short (under ~50 lines). Anything bigger belongs in a separate tool that writes a cache file.
  • Never call network or git fetch synchronously; refresh in the background and read a cache keyed by .session_id.
  • End with exit 0 defensively, and give every jq lookup a // fallback.
  • Test from /tmp (no git, no node_modules) with mock JSON — it must not blow up there.
  • Version-control the script in your dotfiles and treat changes like any production script. A CI smoke test that pipes mock JSON, asserts exit 0, output under 200 chars, and runtime under 300ms catches regressions early.

Common pitfalls

  • Reading $CLAUDE_MODEL / $CLAUDE_SESSION_ID instead of parsing stdin JSON — those env vars do not exist.
  • Forgetting "type": "command" in the statusLine block; without it the entry is ignored.
  • Running git status (recursive, slow) when git rev-parse or git branch --show-current (one cheap call) would do.
  • Doing arithmetic on a null context field before the first API response — guard with // 0.
  • Hardcoding paths with ~ inside the command string on Windows, or using backslashes that Git Bash eats.
  • Letting one slow third-party command (docker ps, kubectl) gate the whole bar — cache it or drop it.
  • If env injection itself is the problem, see Claude Code settings.json not loading for related config-load issues.

FAQ

Q: My script works when I run it manually but the bar is blank in Claude. Why?

You almost certainly ran it interactively without piping JSON in, so it hung on cat waiting for stdin and you never noticed. Claude provides the data only on stdin. Test the real path: echo '{"model":{"display_name":"Opus 4.7"}}' | ~/.claude/statusline.sh. Also confirm jq is on the PATH Claude sees, and that you read stdin rather than env vars.

Q: The bar shows -- or null right after launch.

Several context_window fields are null before the first API response (and again right after /compact). Add jq fallbacks like .context_window.used_percentage // 0. The values populate once your first message returns.

Q: I see statusline skipped · restart to fix. What does that mean?

The directory has not been granted workspace trust, and statusLine runs a shell command, so it is gated behind that trust the same way hooks are. Restart Claude Code in the directory and accept the trust prompt. Also check that disableAllHooks is not set to true.

Q: Is there a max output length?

There is no hard cap, but the bar shares its row with system notifications (MCP errors, auto-updates, the context-low warning), so on narrow terminals long output gets truncated. Keep it tight — roughly under 200 characters.

Q: Can the statusline make tool calls or read agent state?

No. It is a plain shell command that runs out-of-band and gets no access to the agent loop. For dynamic data, run a background process that writes a cache file and have the statusline read the file — see Claude Code tool execution hangs for related blocking-call patterns.

Q: How do I have Claude Code build or remove the script for me?

Run /statusline with a natural-language description (for example, /statusline show model name and context percentage with a progress bar) and it writes the script plus the settings entry. To remove it, run /statusline delete or delete the statusLine key from settings.json.

Tags: #Claude Code #statusline #Troubleshooting #configuration #scripting