Claude Code statusline

The Claude Code statusline is a customizable bar at the bottom of the terminal that runs a shell script after each assistant message. Claude Code sends JSON session data to stdin, and whatever the script prints becomes the status bar content -- ANSI colors included.

My setup displays the current model, a context window progress bar, and rate limit information when available.

Configuration

The statusline is enabled in ~/.claude/settings.json:

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

The script at ~/.claude/statusline-command.sh:

#!/usr/bin/env bash
input=$(cat)

CYN='\033[1;36m' GRN='\033[0;32m' YLW='\033[0;33m' MAG='\033[0;35m' RST='\033[0m'

eval "$(echo "$input" | jq -r '
  @sh "model=\(.model.display_name // "Claude")",
  @sh "used=\(.context_window.used_percentage // "")",
  @sh "rl_pct=\(.rate_limits.five_hour.used_percentage // .rate_limits.seven_day.used_percentage // "")",
  @sh "rl_reset=\(.rate_limits.five_hour.resets_at // .rate_limits.seven_day.resets_at // "")",
  @sh "rl_label=\(if .rate_limits.five_hour.used_percentage then "5h" elif .rate_limits.seven_day.used_percentage then "7d" else "" end)"
')"

printf "%b%b" "$CYN" "$model"

if [ -n "$used" ]; then
  used_int=$(printf '%.0f' "$used")
  filled=$((used_int / 5))
  bar=$(printf "%${filled}s" | tr ' ' '#')$(printf "%$((20 - filled))s" | tr ' ' '-')
  printf "%b  📊 %b[%b%s%b]%b %s%%" "$RST" "$RST" "$GRN" "$bar" "$RST" "$YLW" "$used_int"
fi

if [ -n "$rl_pct" ]; then
  rl_int=$(printf '%.0f' "$rl_pct")
  reset_time=""
  [ -n "$rl_reset" ] && reset_time=$(date -d "@${rl_reset}" +%H:%M 2>/dev/null || date -r "${rl_reset}" +%H:%M)
  printf "  %b⏱️%s:%s%%%b" "$MAG" "$rl_label" "$rl_int" "$RST"
  [ -n "$reset_time" ] && printf "  %s" "$reset_time"
fi

printf "%b" "$RST"

What it shows

The script parses the JSON that Claude Code pipes in and displays up to three pieces of information on a single line:

  1. Model name in bold cyan (e.g. "Opus" or "Sonnet")

  2. Context window usage as a 20-character progress bar (# for filled, - for empty) with a percentage

  3. Rate limit usage for the 5-hour or 7-day window, including the reset time -- this only appears when the data is available (Claude Pro/Max subscriptions)

Before the first API response, only the model name is shown because context and rate limit fields are still null.

statusline

How it works

Claude Code runs the configured command after each assistant message and on permission mode changes. The full session state is sent as JSON to stdin -- the documentation lists all available fields. The script uses jq to extract what it needs and printf with ANSI escape codes to produce colored output.

One thing to note: the // empty fallback in jq is important. Fields like used_percentage and the rate limit data are null before the first API call, so without the fallback the script would print "null" in the status bar.

The /statusline slash command can generate a script from a natural language description, but I used Claude Code itself to iterate on the script until I was happy with the output.

Claude Code tmux window names

When running multiple Claude Code sessions in tmux, every window ends up named "claude" -- making it impossible to tell them apart.

before

Using Claude Code's hooks, the tmux window can be renamed automatically based on the working directory.

Using Hooks

A SessionStart hook runs once when a session begins or is resumed:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ -n \"$TMUX\" ] && tmux rename-window \"cc:$(basename \"$PWD\")\""
          }
        ]
      }
    ]
  }
}

This renames the tmux window to cc:<project-dir>, e.g. cc:madflex.de. The [ -n "$TMUX" ] guard makes sure it does nothing when running outside tmux.

Since tmux rename-window disables automatic renaming for that window, the name would stick after Claude exits. A SessionEnd hook restores it:

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ -n \"$TMUX\" ] && tmux set-option -w automatic-rename on"
          }
        ]
      }
    ]
  }
}

Add both hooks to ~/.claude/settings.json and use /hooks in a session to verify they are loaded.

Alternative: zsh preexec

The hooks approach is Claude Code-specific. A zsh preexec / precmd pair works for any long-running command -- claude, uv run, docker compose, etc:

# auto-rename tmux windows for long-running commands
preexec() {
  if [[ -n "$TMUX" ]]; then
    case "$1" in
      *claude*)          tmux rename-window "cc:$(basename "$PWD")" ;;
      *uv\ run\ *)       tmux rename-window "uv:$(basename "${${1##*uv run }%% *}")" ;;
      *docker\ compose*) tmux rename-window "dc:${1##* }" ;;
    esac
  fi
}
precmd() {
  [[ -n "$TMUX" ]] && tmux set-option -w automatic-rename on
}

preexec runs before each command, precmd runs before each new prompt -- so the window name resets automatically when the command finishes. The * prefix in each pattern handles commands with leading environment variables like FOO=bar docker compose up.

For Claude Code, the project directory is the most useful identifier since sessions don't have a single command. For uv run, the script name is extracted as the first argument after uv run. For docker compose, ${1##* } grabs the last word of the command -- typically the service name, e.g. docker compose up container-name becomes dc:container-name. This won't be perfect if flags come after the service name, but it works well enough in practice.

The case statement is easy to extend with more patterns.

After the change:

after

I removed the hooks from the ~/.claude/settings.json and chose to use only the zsh solution. But without investigating Claude Code's hooks I would not have found out about them!

Claude Code desktop notifications

When using Claude Code in a terminal, it's easy to switch to another workspace and forget that Claude is waiting for input. Using Claude Code's hooks and notify-send, any freedesktop-compatible notification daemon (dunst, mako, swaync, fnott, ...) can notify you.

The Notification hook supports matchers for different event types. Using two Notification hooks with separate matchers covers two cases:

  • idle_prompt — Claude finished and is waiting at the main prompt (fires roughly after 60 seconds of inactivity)

  • permission_prompt — Claude needs permission to run a tool (fires immediately, which may be too frequent if you're already at the terminal)

Add them to ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send -u normal -a 'Claude Code' 'Claude Code idle' \"Waiting in $(basename \"$PWD\")\""
          }
        ]
      },
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send -u normal -a 'Claude Code' 'Claude needs permission' \"Approve action in $(basename \"$PWD\")\""
          }
        ]
      }
    ]
  }
}

Both use "normal" priority, so that the notification vanishes after a little while without the need for me to click anything.

Restart Claude Code sessions after changing the hook config. Use /hooks in a session to verify the hooks are loaded.

Example for idle message:

idle

There are more hooks, but I decided to start with only Notification.

Limitations

This does not notify when Claude asks an interactive question mid-response (AskUserQuestion). There is no hook for this yet — see this feature request.