Building a Live Terminal Statusline for Claude Code: Tokens, Limits, and Responsive Wrapping

How to design a resilient Bash statusline command for Claude Code that displays workspace context, token consumption, rolling rate limits, and ANSI color formatting without terminal overflow.

A status bar drawn as a glass panel with segments for folder, git branch, token count, and session and weekly usage meters, with a narrower copy wrapped onto two rows
Editorial Note: Public developer tooling guide; uses standard Linux terminal APIs and public Claude Code configuration parameters.

When running long agentic sessions with command-line tools like Claude Code, visibility into session telemetry is crucial. Developers frequently need answers to four questions at a glance:

  1. Where is the agent executing? (Current working directory)
  2. What branch is currently active? (Git status without locking repository indexes)
  3. How many tokens have been consumed? (Context window budget)
  4. Where do my rate limits stand? (Session 5-hour and weekly 7-day quota utilization)

Claude Code exposes an extensible configuration hook called statusLine.command. By pointing this setting to an executable shell script, Claude Code pipes a rich JSON telemetry payload into your command’s stdin on every refresh cycle.

However, implementing a production-grade statusline in Bash surfaces subtle systems-level challenges: ANSI escape sequence decoding pitfalls, subshell latency overhead, git index lock contention, and terminal character counting errors during line wrapping.

In this guide, we walk through the architecture of a custom statusline, analyze common failure modes, and build a modular Bash implementation.


The Protocol Contract: Claude Code Stdin Payload

When configured with "type": "command", Claude Code invokes your statusline script and pipes a serialized JSON object into standard input:

{
  "workspace": {
    "current_dir": "/home/engineer/projects/distributed-service"
  },
  "context_window": {
    "total_input_tokens": 128450,
    "context_window_size": 200000
  },
  "rate_limits": {
    "five_hour": {
      "used_percentage": 42.5
    },
    "seven_day": {
      "used_percentage": 18.2
    }
  }
}

Your script’s responsibility is simple: parse this input, compute any additional metadata (such as git branches), format the line with terminal colors, and emit the final string to stdout.

Setting Up the Configuration Hook

To activate the hook, register it in your Claude Code user configuration file at ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline-command.sh",
    "timeout": 10
  }
}

[!TIP] Always enforce a strict timeout parameter (such as 10 seconds). If a subshell freezes—for example, during network-mounted filesystem traversal or slow git status evaluation—Claude Code gracefully aborts the execution without stalling your interactive prompt.


Architectural Pitfalls in Terminal Scripting

Before writing the script, several subtle Bash and terminal rendering behaviors must be accounted for.

1. The ANSI Escape Concatenation Trap

A frequent mistake in Bash statuslines is concatenating raw ANSI color sequences using standard double-quoted strings:

# BROKEN: Prints literal characters "\033[2;33mSession:\033[0m"
var="${var} \033[2;33mSession:\033[0m"

In POSIX shell and Bash, plain double quotes ("...") and single quotes ('...') treat \033 as a literal backslash followed by three numbers. The escape byte (0x1B or ESC) is never emitted to the terminal emulator.

There are only two reliable ways to output colors in Bash:

  1. Through printf’s format-string parameter:
    printf '\033[2;33m%s\033[0m' "Session:"
  2. Via ANSI-C Quoting ($'...') stored in a reusable variable:
    esc=$'\033'
    reset="${esc}[0m"
    yellow="${esc}[2;33m"
    cyan="${esc}[2;36m"
    
    # Safe concatenation
    var="${var} ${yellow}Session:${reset}"

2. Git Lock Contention via --no-optional-locks

The incoming JSON payload supplies the current working directory, but it deliberately omits git information to avoid blocking on slow repositories.

When querying the current branch from a background statusline hook, never run a bare git branch or git status. If another build tool, editor daemon, or Claude Code command is simultaneously interacting with git, you risk colliding with .git/index.lock.

Always query the branch using:

git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null

The --no-optional-locks flag instructs git to avoid creating background locks or refreshing file status caches, keeping the query fast and non-disruptive.

3. Terminal Character Counting & Invisible ANSI Bytes

A responsive statusline should adapt gracefully:

  • Wide viewports (> 100 columns): Display everything on a single horizontal line.
  • Narrow viewports (split panes or mobile terminals): Wrap metrics onto a clean second line instead of allowing the terminal emulator to truncate or mangle the text mid-word.

Calculating whether text exceeds the terminal width (tput cols or $COLUMNS) requires knowing the visible display length of the line.

Running wc -m directly on a string containing ANSI escape codes will overcount substantially. For example:

  • Visible text: "Session: 42%" (12 characters)
  • Raw string with ANSI escapes: "\033[2;33mSession:\033[0m 42%" (23 characters)

If you calculate length without stripping the escape bytes, your script will trigger multi-line wrapping prematurely. The solution is to strip ANSI codes with a regular expression before measuring length:

plain_length() {
  echo -n "$1" | sed -E 's/\x1b\[[0-9;]*m//g' | wc -m
}

Visualizing Quota: Unicode Block Progress Bars

Text percentages (42%) require mental effort to parse. A mini horizontal bar gauge ([████░░░░░░]) provides instantaneous spatial feedback.

Using standard UTF-8 block elements (█ for filled segments, ░ for unfilled segments), we can compute a scaled bar with pure integer arithmetic:

draw_progress_bar() {
  local percentage="${1%.*}" # Strip decimals
  local total_width="$2"     # Desired character width (e.g. 10)
  
  [ -z "$percentage" ] && percentage=0
  (( percentage > 100 )) && percentage=100

  local filled=$(( percentage * total_width / 100 ))
  local empty=$(( total_width - filled ))

  printf '%0.s█' $(seq 1 $filled 2>/dev/null)
  printf '%0.s░' $(seq 1 $empty 2>/dev/null)
}

The Complete Implementation

Here is the complete production script. Save this file to ~/.claude/statusline-command.sh and make it executable with chmod +x ~/.claude/statusline-command.sh.

#!/usr/bin/env bash
set -eo pipefail

# 1. Capture JSON payload from Claude Code via standard input
raw_input=$(cat)

if [ -z "$raw_input" ]; then
  exit 0
fi

# 2. Extract telemetry fields using jq
cwd=$(echo "$raw_input" | jq -r '.workspace.current_dir // .cwd // empty')
total_tokens=$(echo "$raw_input" | jq -r '.context_window.total_input_tokens // 0')
session_pct=$(echo "$raw_input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
weekly_pct=$(echo "$raw_input" | jq -r '.rate_limits.seven_day.used_percentage // empty')

# 3. ANSI Escape definitions via ANSI-C quoting
esc=$'\033'
c_reset="${esc}[0m"
c_dim="${esc}[2m"
c_cyan="${esc}[2;36m"
c_yellow="${esc}[2;33m"
c_magenta="${esc}[2;35m"
c_green="${esc}[2;32m"

# 4. Progress bar generator
render_bar() {
  local pct="${1%.*}"
  local width="${2:-8}"
  
  [ -z "$pct" ] && pct=0
  (( pct > 100 )) && pct=100
  
  local filled=$(( pct * width / 100 ))
  local empty=$(( width - filled ))
  
  local bar=""
  if [ "$filled" -gt 0 ]; then
    bar+=$(printf '%0.s█' $(seq 1 "$filled"))
  fi
  if [ "$empty" -gt 0 ]; then
    bar+=$(printf '%0.s░' $(seq 1 "$empty"))
  fi
  printf '%s' "$bar"
}

# 5. Format the workspace & context segment (Line 1)
path_display=""
if [ -n "$cwd" ]; then
  # Compact home directory to ~
  path_display="${cwd/#$HOME/\~}"
fi

branch_display=""
if [ -n "$cwd" ] && [ -d "$cwd/.git" ]; then
  branch=$(git -C "$cwd" --no-optional-locks branch --show-current 2>/dev/null || true)
  if [ -n "$branch" ]; then
    branch_display="${c_cyan}(${branch})${c_reset}"
  fi
fi

# Format token count (e.g., 128450 -> 128k)
if (( total_tokens >= 1000 )); then
  token_str="$(( total_tokens / 1000 ))k tok"
else
  token_str="${total_tokens} tok"
fi

line_context="${c_dim}${path_display}${c_reset}"
[ -n "$branch_display" ] && line_context="${line_context} ${branch_display}"
line_context="${line_context} ${c_magenta}${token_str}${c_reset}"

# 6. Format rate limits segment
limits_parts=()

if [ -n "$session_pct" ]; then
  int_session="${session_pct%.*}"
  bar_session=$(render_bar "$int_session" 8)
  limits_parts+=("${c_yellow}5h:${c_reset} [${bar_session}] ${int_session}%")
fi

if [ -n "$weekly_pct" ]; then
  int_weekly="${weekly_pct%.*}"
  bar_weekly=$(render_bar "$int_weekly" 8)
  limits_parts+=("${c_green}7d:${c_reset} [${bar_weekly}] ${int_weekly}%")
fi

limits_str=""
if [ ${#limits_parts[@]} -gt 0 ]; then
  limits_str=$(IFS=" "; echo "${limits_parts[*]}")
fi

# 7. Adaptive Viewport Wrapping
# Determine terminal columns available
cols=$(tput cols 2>/dev/null || echo "${COLUMNS:-80}")

# Helper: compute visible character length by stripping ANSI escape sequences
calc_visible_len() {
  echo -n "$1" | sed -E 's/\x1b\[[0-9;]*m//g' | wc -m
}

if [ -z "$limits_str" ]; then
  # If no rate limits were reported, emit single context line
  printf '%s\n' "$line_context"
else
  len_context=$(calc_visible_len "$line_context")
  len_limits=$(calc_visible_len "$limits_str")
  total_projected_len=$(( len_context + 2 + len_limits ))

  # If both segments exceed available columns, wrap cleanly onto two lines
  if [ "$total_projected_len" -ge "$cols" ]; then
    printf '%s\n%s\n' "$line_context" "$limits_str"
  else
    printf '%s  %s\n' "$line_context" "$limits_str"
  fi
fi

Verifying in Isolation

You can test the script immediately without restarting Claude Code by piping mock JSON into it:

echo '{
  "workspace": { "current_dir": "'"$PWD"'" },
  "context_window": { "total_input_tokens": 142000 },
  "rate_limits": {
    "five_hour": { "used_percentage": 68.4 },
    "seven_day": { "used_percentage": 24.1 }
  }
}' | bash ~/.claude/statusline-command.sh

Result:

~/projects/service (main) 142k tok  5h: [█████░░░] 68% 7d: [██░░░░░░] 24%

When resized into a narrow terminal split (< 60 columns), the script automatically shifts the quotas onto line two:

~/projects/service (main) 142k tok
5h: [█████░░░] 68% 7d: [██░░░░░░] 24%

Key Engineering Takeaways

  1. String concatenation does not interpret \033: Always use ANSI-C quoting (esc=$'\033') or pass formatting strings directly to printf.
  2. Never lock repositories from telemetry hooks: Background status scripts must always invoke git with --no-optional-locks to avoid race conditions with developer tooling.
  3. Strip ANSI before measuring geometry: Display metrics depend on printable character count, not byte count. Failure to strip escape codes leads to broken responsive wrapping.
  4. Resilient failure boundaries: Defaulting all JSON extracts through jq // empty guarantees the script never crashes when Claude Code adds or renames telemetry keys.