Claude Code Statusline Custom Script Errors or Hangs

Your Claude Code statusline shows an error, stays blank, or freezes the prompt — the custom script is failing silently. Diagnose exit codes, output format, and timeout limits.

You wired a custom statusline script to show git branch, model name, and token budget in your Claude Code prompt. The first run looked fine. Then it broke: the bar shows literal text like statusline: error or stays empty entirely, or worse, the whole prompt hangs for a few seconds before each turn while the script blocks. The Claude Code statusline runs your script on every refresh — typically once per assistant turn — and any non-zero exit, slow command, or malformed stdout will degrade the experience. Most fixes boil down to making the script fast, deterministic, and silent on stderr.

Common causes

Ordered by frequency.

1. Script exits non-zero

The statusline runner expects exit code 0. If your script set -e then runs git rev-parse --abbrev-ref HEAD in a non-git directory, it dies with exit 128, and the bar shows an error.

How to spot it: echo $? after manually running the script returns non-zero. Or claude --debug log shows statusline exited 128.

2. Script too slow (blocks the prompt)

Statusline scripts have a soft timeout (often 1-2 seconds). A curl to a remote API, a git fetch, or a recursive find will routinely exceed that. The script either gets killed or the prompt visibly stalls.

How to spot it: time ./statusline.sh takes more than 500ms. Or you feel the prompt pause before each turn.

3. Output contains ANSI / control chars that get mangled

Outputting raw ANSI escape sequences (color codes) works in some renderers and breaks in others. Embedded newlines split into two lines. Tabs become \t literal.

How to spot it: Statusline shows literal \033[31m or has line breaks where it should be one line.

4. Path or interpreter mismatch

#!/usr/bin/env bash is portable; #!/bin/bash may not exist on Alpine. #!/usr/bin/python may resolve to Python 2 on macOS. Wrong shebang means the script never runs.

How to spot it: head -1 statusline.sh shows a shebang that does not match which bash / which python3 on your system.

5. Script writes to stderr and noise leaks into the bar

Some renderers concatenate stderr with stdout. A noisy git status that prints warnings on stderr leaks into the visible statusline.

How to spot it: Strange text in the bar like warning: ... or progress messages.

6. Settings.json points to a missing or non-executable file

statusLine.command references a path that does not exist, or the file exists but lacks +x. The script never runs.

How to spot it: ls -l <path> shows missing file or no execute bit. claude --debug shows statusline: no such file.

7. JSON-mode statusline returns invalid JSON

If your statusline is configured in JSON mode (returning {"text": "...", "color": "..."}), a trailing comma or wrong key breaks parsing and falls back to empty.

How to spot it: Bar is empty even though stdout from the script looks fine. claude --debug shows statusline: json parse error.

Before you start

  • Note whether the bar is empty, shows error text, or has wrong content.
  • Capture whether the issue is constant or intermittent (intermittent often = network call in the script).
  • Find the statusline script path from ~/.claude/settings.jsonstatusLine.command.
  • Have a “before” screenshot or copy of expected output for comparison.

Information to collect

  • Contents of the statusline script.
  • The statusLine block in ~/.claude/settings.json (or project settings).
  • time ./statusline.sh runtime.
  • ./statusline.sh 1>/tmp/sl.out 2>/tmp/sl.err; echo exit=$? outputs.
  • Claude Code version.

Step-by-step fix

Start with manual invocation — if it does not work standalone, it definitely will not work in the prompt.

Step 1: Run the script directly and inspect

time ./statusline.sh
echo "---"
./statusline.sh 1>/tmp/sl.out 2>/tmp/sl.err
echo "exit=$?"
cat /tmp/sl.out
echo "---stderr---"
cat /tmp/sl.err

Three checks: does it exit 0, is stdout clean, is stderr silent? If any fails, the fix is in the script, not Claude.

Step 2: Make the script tolerant of edge cases

Wrap potentially-failing commands in error suppression:

#!/usr/bin/env bash
set +e
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")
model="${CLAUDE_MODEL:-unknown}"
printf "%s | %s" "$branch" "$model"
exit 0

Note the explicit exit 0 — defensive against the last command failing.

Step 3: Enforce a fast timeout in the script itself

For commands that might block:

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

Never call network APIs synchronously in a statusline. Cache them:

CACHE=/tmp/claude-statusline-cache
if [ ! -f "$CACHE" ] || [ "$(find "$CACHE" -mmin +5)" ]; then
  curl -s --max-time 1 https://api.example.com/quota > "$CACHE.tmp" && mv "$CACHE.tmp" "$CACHE"
fi
quota=$(cat "$CACHE" 2>/dev/null || echo "?")

Step 4: Strip ANSI / newlines from output

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

If you want color and your renderer supports it, confirm by testing one literal escape first. Otherwise omit color and let Claude Code style the bar itself.

Step 5: Silence stderr at the script boundary

In settings.json, wrap the command:

{
  "statusLine": {
    "command": "/bin/sh -c '/Users/me/.claude/statusline.sh 2>/dev/null'"
  }
}

This guarantees stderr never reaches the renderer.

Step 6: Fix permissions and shebang

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

Test with the absolute path to the interpreter:

/usr/bin/env bash ~/.claude/statusline.sh

If this works but invoking the script directly fails, the shebang is the issue.

Step 7: Validate JSON-mode output (if used)

./statusline.sh | jq .

Should parse without error. A correct JSON-mode response:

{"text": "main | opus-4.7 | 12k tokens", "color": "cyan"}

If your settings uses type: "json" (or equivalent), unparseable output gives an empty bar.

Verify

  • Restart Claude Code; bar should populate within 200ms of session start.
  • Open a new directory that is NOT a git repo and confirm the bar still works (fallback path).
  • Disconnect from network and confirm the bar still works (cache path).
  • Run time ./statusline.sh from several different cwds; all should be under 500ms.

Long-term prevention

  • Keep the statusline script under 50 lines. Anything bigger probably belongs in a separate tool.
  • Never call network or git-fetch synchronously; cache values from a background refresh.
  • Always end with exit 0 defensively.
  • Test the script from /tmp (no git, no node_modules) — it must not blow up there.
  • Version-control the script in dotfiles; treat changes the same as any production script.
  • Add a smoke test in CI that runs the script and asserts exit 0, output less than 200 chars, runtime under 500ms.
  • Keep the bar useful but minimal — model name, git branch, maybe one custom signal. Overloading it slows every turn.

Common pitfalls

  • Running git status (recursive, slow) when git rev-parse (one syscall) would do.
  • Adding token-counting that re-tokenizes the entire context on every refresh.
  • Hardcoding the script’s path with ~; Claude resolves ~/.claude/settings.json differently in different launch contexts.
  • Letting one slow third-party command (docker ps, kubectl) gate the entire bar.
  • Forgetting to handle the case where CLAUDE_MODEL env var is unset — see Claude Code settings.json not loading for related env-injection issues.
  • Embedding curly braces in the output string — they may be interpreted by some renderers; use [] or ().

FAQ

Q: The bar works when I run the script manually but is empty in Claude. Why?

Most often the environment is different. Claude spawns the script without your interactive shell’s PATH or env vars. Use absolute paths inside the script and reference Claude-provided env ($CLAUDE_MODEL, $CLAUDE_SESSION_ID) only.

Q: Is there a max output length?

Soft cap around 200 chars. Beyond that, renderers truncate or wrap. Keep it tight.

Q: Can the statusline make tool calls?

No. It is a plain shell command, runs out-of-band, gets no access to the agent loop. If you want dynamic data, write a background daemon that updates a cache file and have the statusline read the file.

Q: Why does the statusline run twice on session start?

Once at session init and once after the first turn renders. Make the script idempotent and cheap — see Claude Code tool execution hangs for related blocking-call issues.

Q: Can I disable the statusline entirely?

Remove the statusLine key from settings.json, or set statusLine.command to an empty string. Restart the session.

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