Claude Code can show a line of your own at the bottom of the screen: which model is answering, which folder and git branch the session is in, what the session has cost so far, and how full the context window is. You set it up with one statusLine block in settings.json and one script. This page gives you both, in bash with no jq, plus a Python twin, and a table of every field Claude Code hands the script.

Everything below about Claude Code’s behaviour is taken from the official status line documentation, fetched on 2026-09-29 (the old docs.claude.com address redirects there). The Claude Code installed on the machine where these scripts were tested reports 2.1.284 (Claude Code). The docs describe the feature this way: “The status line is a customizable bar at the bottom of Claude Code that runs any shell script you configure. It receives JSON session data on stdin and displays whatever your script prints”. Where this page says what Claude Code does, it links to the part of the docs that says so.

The end result, run against the example payload printed in the docs, inside a git repository on a branch called feature/statusline:

Opus | directory | feature/statusline | $0.01 | ctx 8%

That is model | folder | branch | session cost | context used. The folder reads directory because the docs’ example payload uses the placeholder path /current/working/directory.

The shortest working setup

Add a statusLine block with type command to ~/.claude/settings.json, point command at an executable script, and have the script print one line built from the JSON it reads on stdin. The docs’ manual setup section says to set type to "command" and point command “to a script path or an inline shell command”. If your settings.json already has other keys, add statusLine beside them rather than replacing the file:

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

The user file, ~/.claude/settings.json, applies to every project. The docs also allow the project settings file, .claude/settings.json, if you want a status line for one repository only. Save the script from the next section as ~/.claude/statusline.sh and make it executable:

# save the script below as ~/.claude/statusline.sh, then:
chmod +x ~/.claude/statusline.sh

You don’t need to restart anything. The docs’ step-by-step walkthrough says: “Claude Code reloads settings automatically and runs your script as soon as you save the file.” If you would rather have Claude write a script for you, the docs describe a /statusline command that takes a plain-English description and edits your settings itself. This page is for when you want to know exactly what the script does.

The bash script: no jq, five fields

The status line JSON has few enough fields that grep and sed can pull out the five this script needs, as long as used_percentage is read from inside context_window. The docs’ own examples use jq, which the docs call “a command-line JSON parser you may need to install”. This script needs only bash, grep, sed and git, which a Mac or a Linux box with git already has.

It reads five fields from the JSON on stdin:

  • model.display_name: the model’s display name, such as Opus.
  • workspace.current_dir, falling back to cwd. The Available data table says of the two: “Both fields contain the same value; workspace.current_dir is preferred for consistency with workspace.project_dir.”
  • cost.total_cost_usd: the docs call it “Estimated session cost in USD, computed client-side at list price” and warn it “May differ from your actual bill.”
  • context_window.used_percentage: “Pre-calculated percentage of context window used”.
#!/usr/bin/env bash
# statusline.sh: model | folder | git branch | session cost | context used,
# for Claude Code's status line. Reads the status line JSON on stdin.
# Needs only bash, grep, sed and git: no jq.

IFS= read -r -d '' json
json=${json//$'\n'/ }

# str KEY TEXT / num KEY TEXT: the first string / number value of "KEY" in TEXT
str() { printf '%s' "$2" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | sed -n '1s/.*"\([^"]*\)"$/\1/p'; }
num() { printf '%s' "$2" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*[-0-9.eE+]*" | sed -n '1s/.*:[[:space:]]*//p'; }

model=$(str display_name "$json")
dir=$(str current_dir "$json")
[ -n "$dir" ] || dir=$(str cwd "$json")
cost=$(num total_cost_usd "$json")

# used_percentage also appears under rate_limits, so read it from inside
# context_window only: drop the nested current_usage object, stop at the next }
pct=
case $json in
  *'"context_window"'*)
    cw=$(printf '%s' "${json#*\"context_window\"}" |
      sed 's/"current_usage"[[:space:]]*:[[:space:]]*{[^}]*}//; s/}.*//')
    pct=$(num used_percentage "$cw") ;;
esac

# the branch: two cheap reads of HEAD, never git status; empty outside a repo
[ -n "$dir" ] && cd "$dir" 2>/dev/null
branch=$(git symbolic-ref --short -q HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)

if [ -n "$pct" ]; then ctx="ctx ${pct%%.*}%"; else ctx="ctx --"; fi

line="${model:-?} | ${dir##*/}"
[ -n "$branch" ] && line="$line | $branch"
LC_ALL=C printf '%s | $%.2f | %s\n' "$line" "${cost:-0}" "$ctx"

Four details are there on purpose:

  • Why used_percentage gets special treatment. In the documented payload the key used_percentage appears four times: once under context_window and once under each of the three rate_limits windows. A plain “first match” would read the wrong one whenever rate_limits happens to come first. So the script cuts the text down to the context_window object before looking. current_usage is the only object nested inside it, so that is dropped first and the cut stops at the next closing brace.
  • Null and missing values. The docs’ schema notes say used_percentage and remaining_percentage “may be null early in the session”. A null gives an empty value here, and the script prints ctx -- rather than a fake 0%. A missing cost prints as zero.
  • Which folder git looks at. The docs don’t say which directory the status line command starts in. Their own git examples call git without changing directory. This script changes into workspace.current_dir first when that folder exists, and otherwise stays where it started. Outside a git repository the branch segment is left out. On a detached HEAD it shows the short commit hash instead of a branch name.
  • Decimals. LC_ALL=C keeps printf using a decimal point in locales that write a comma. The percentage is cut at the decimal point, the same way the docs’ examples do it with cut -d. -f1.

It is a line-oriented reader, not a JSON parser, and that has limits. A folder or model name containing a double quote is cut short at the quote. It relies on the payload’s shape as documented. If a later release nests another object inside context_window, the context segment may fall back to ctx --, and the Python version below is the safer choice.

The same status line in Python

If python3 is on the machine, this version reads the JSON with a real parser and prints the same line. Save it as ~/.claude/statusline.py and run chmod +x ~/.claude/statusline.py:

#!/usr/bin/env python3
# statusline.py: the same line as statusline.sh, in Python 3 (standard library only).
import json
import os
import subprocess
import sys

data = json.load(sys.stdin)


def get(path):
    """The value at a dotted path such as "model.display_name", or None."""
    node = data
    for key in path.split("."):
        if not isinstance(node, dict):
            return None
        node = node.get(key)
    return node


model = get("model.display_name") or "?"
directory = get("workspace.current_dir") or get("cwd") or ""
cost = get("cost.total_cost_usd") or 0
pct = get("context_window.used_percentage")


def git(*args):
    """stdout of a git command run in the session's folder, or "" on any failure."""
    try:
        r = subprocess.run(["git", *args], capture_output=True, text=True,
                           cwd=directory if os.path.isdir(directory) else None)
    except OSError:
        return ""
    return r.stdout.strip() if r.returncode == 0 else ""


branch = git("symbolic-ref", "--short", "-q", "HEAD") or git("rev-parse", "--short", "HEAD")

parts = [model, os.path.basename(directory)]
if branch:
    parts.append(branch)
parts.append(f"${cost:.2f}")
parts.append(f"ctx {int(pct)}%" if pct is not None else "ctx --")
print(" | ".join(parts))

Then point settings.json at it instead:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.py"
  }
}

The two versions were run against the same payloads and print byte-identical lines. On a Mac, python3 may not be there until Apple’s Command Line Tools are installed, which is the main reason the bash version comes first.

Every field in the status line JSON

Read context_window.used_percentage from the status line JSON. It is the documented, pre-calculated share of the context window in use, and it can be null before the first API response. That field, and every other one below, comes from the docs’ Available data table, in the docs’ order. The descriptions are shortened; the docs have the full wording and the version notes.

FieldWhat it holds
model.id, model.display_nameThe current model’s identifier and its display name
cwd, workspace.current_dirThe current working directory; both hold the same value
workspace.project_dirThe directory Claude Code was launched in, which can differ from cwd later in a session
workspace.added_dirsDirectories added with /add-dir or --add-dir; an empty array if none
workspace.git_worktreeThe git worktree name when the directory is inside a linked worktree; absent in the main working tree
workspace.repo.host, workspace.repo.owner, workspace.repo.nameRepository identity parsed from the origin remote; absent outside a repo or without an origin
cost.total_cost_usdEstimated session cost in USD, computed on your machine at list price
cost.total_duration_msWall-clock time the session has been running, in milliseconds
cost.total_api_duration_msTime spent waiting for API responses, in milliseconds
cost.total_lines_added, cost.total_lines_removedLines of code changed
context_window.total_input_tokens, context_window.total_output_tokensToken counts in the context window from the most recent API response; input includes cache reads and writes
context_window.context_window_sizeThe maximum context window in tokens: 200000 by default, or 1000000 for models with extended context
context_window.used_percentagePre-calculated percentage of the context window used
context_window.remaining_percentagePre-calculated percentage of the context window remaining
context_window.current_usageToken counts from the last API call, split into input, output, cache-write and cache-read
exceeds_200k_tokensWhether the most recent response’s total tokens exceed a fixed 200k threshold, whatever the window size
fast_modeWhether fast mode is on for the session
effort.levelThe current reasoning effort; absent when the model does not support the effort parameter
thinking.enabledWhether extended thinking is on for the session
rate_limits.five_hour.used_percentage, rate_limits.seven_day.used_percentageShare of the 5-hour or 7-day rate limit used, from 0 to 100
rate_limits.five_hour.resets_at, rate_limits.seven_day.resets_atWhen those windows reset, in Unix epoch seconds
rate_limits.spend_limit.used_percentage, rate_limits.spend_limit.resets_atBehind a Claude apps gateway, the share of your spend limit used and when its period resets
prompt_cachePrompt cache statistics for the main conversation: hit ratio, misses, and whether the cache is warm
session_idThe unique session identifier
session_nameThe custom name from --name or /rename, or the AI-generated session title
prompt_idA UUID for the user prompt being processed; absent until the first input
transcript_pathPath to the conversation transcript file
versionThe Claude Code version
output_style.nameThe name of the current output style
vim.modeThe current vim mode, when vim mode is on
agent.nameThe agent name when running with --agent or agent settings
pr.number, pr.urlThe open pull request (or GitLab merge request) for the current branch
pr.review_stateThe open PR’s review status: approved, pending, changes requested or draft
pr.kindmr when pr describes a GitLab merge request; absent for GitHub
worktree.nameThe active worktree’s name, only during a worktree session
worktree.pathAbsolute path to the worktree directory
worktree.branchThe worktree’s git branch name
worktree.original_cwdThe directory Claude was in before entering the worktree
worktree.original_branchThe branch checked out before entering the worktree

Two notes from the docs are worth knowing before you build on the context fields. First, “The used_percentage field is calculated from input tokens only”: output tokens are not counted. If you compute a percentage yourself from current_usage, the docs say to use the same input-only sum. Second, the docs’ troubleshooting list warns that “Context percentage may differ from /context output due to when each is calculated”. Several fields are simply absent in some sessions: rate_limits “appears only for claude.ai Pro and Max subscribers” or behind a gateway spend limit, and effort, vim, agent, pr and worktree each appear only when their feature is in use. A script should treat any of them as optional.

Colours and more than one line

The docs’ list of what a script can output covers colour: “use ANSI escape codes like \033[32m for green (terminal must support them)”. Their multi-line example colours context use “green under 70%, yellow 70-89%, red 90%+”, with RED='\033[31m' for the red. To do the same to the ctx segment, replace the line starting if [ -n "$pct" ] in statusline.sh with:

p=${pct%%.*}
if [ -z "$p" ]; then ctx="ctx --"
elif [ "$p" -ge 90 ]; then ctx=$'\033[31m'"ctx $p%"$'\033[0m'
elif [ "$p" -ge 70 ]; then ctx=$'\033[33m'"ctx $p%"$'\033[0m'
else ctx=$'\033[32m'"ctx $p%"$'\033[0m'
fi

\033[0m resets the colour so the rest of the line stays plain.

As for lines: the docs say “each echo or print statement displays as a separate row”, so a second printf gives you a second row. The docs give no maximum number of rows. They do say “the status bar has limited width, so long output may get truncated or wrap awkwardly”, and that “Multi-line status lines with escape codes are more prone to rendering issues than single-line plain text”. One short line is the sturdy choice.

If you want to fit the line to the window, don’t use tput cols. The docs explain: “Claude Code captures your script’s output instead of connecting it directly to the terminal, so tput cols and language-level width detection cannot read the terminal size from inside the script.” Read the COLUMNS and LINES environment variables instead, which Claude Code sets before each run.

How often it refreshes

It runs when a session starts or resumes, then again on events such as a new assistant message, with a 300ms debounce. The optional refreshInterval setting adds a timer on top. The docs’ When it updates section says “Your script runs once when a session starts, including when you resume one.” After that it runs again when:

  • “A new assistant message arrives”
  • “/compact finishes”
  • “The permission mode changes”
  • “Vim mode toggles”
  • “You change the command in your statusLine settings”
  • a refreshInterval timer elapses, if you set one
  • a rate-limit window or a warm prompt cache in the data your script last received reaches its reset or expiry time

Two rules shape how fast the script needs to be. “Claude Code debounces updates at 300ms, so rapid changes batch together and your script runs once after the changes stop.” And “If a new update triggers while your script is still running, Claude Code cancels the in-flight script.”

Because it is event-driven, a clock or a branch that changes while the session sits idle can go stale. The optional refreshInterval field “re-runs your command every N seconds in addition to the event-driven updates”, and “The minimum is 1.” Leave it out for this script: the model, cost and context only change when a message arrives, and that already triggers a run. The docs also note that “The status line runs locally and does not consume API tokens.”

Troubleshooting

Three causes from the docs’ troubleshooting list cover most cases: a script without the execute bit, a script that exits non-zero or prints nothing, and a folder whose workspace trust dialog has not been accepted.

Test the script without Claude Code. Pipe a sample payload in by hand from inside any git repository. This one is the mock input from the docs’ Tips section:

echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/user/project"},"context_window":{"used_percentage":25},"session_id":"test-session-abc"}' | ~/.claude/statusline.sh

On a branch called feature/statusline it prints:

Opus | project | feature/statusline | $0.00 | ctx 25%

The mock payload has no cost field, so the cost shows as zero. If that works, the script is fine and any remaining problem is in the settings or in Claude Code.

Nothing shows at all. Work through the docs’ Troubleshooting list:

  • “Verify your script is executable: chmod +x ~/.claude/statusline.sh”. Do it again after copying the file from another machine.
  • “Scripts that exit with non-zero codes or produce no output cause the status line to go blank”. The script above always ends with a printf, so it always prints and exits 0.
  • Output has to go to stdout. The docs list “Check that your script outputs to stdout, not stderr”.
  • Workspace trust: “Because statusLine executes a shell command, Claude Code runs it under the same workspace trust rule as hooks in settings files.” Until you accept the trust dialog for the folder, the status line stays blank.
  • “Run claude --debug to log your script’s stderr on every status line invocation, and its exit code on the first invocation in a session”.

It shows ctx -- for a while. That is expected at the start. The docs say “Fields may be null before the first API response completes”. It fills in after Claude’s first reply.

It is slow in a big repository. The docs warn that “Commands like git status or git diff can be slow, especially in large repositories”, and that “Slow scripts block the status line from updating until they complete.” git status has to compare the working tree with the index, which is the slow part. Reading the branch name only has to read HEAD. These three print the same branch name, and none of them looks at your files:

git symbolic-ref --short -q HEAD            # what the script uses
git rev-parse --abbrev-ref HEAD             # same answer; prints HEAD when detached
sed -n 's|^ref: refs/heads/||p' .git/HEAD   # no git process at all

The last one only works from the top of the repository, and not in a linked worktree or submodule, where .git is a file rather than a folder. If you do want staged and modified counts, the docs’ caching example caches git output for a few seconds, keyed on session_id.

The status line tells you what a session is doing while you watch it. For when you are not watching, Claude Code Notification When Done uses two hooks and one bash script to send a desktop notification and a sound when Claude finishes or needs your permission. If you run several sessions at once, Make Your Terminal Glow When Claude Code Needs You makes the right terminal window light up.

What was tested, and what was not

The scripts on this page were run against the documented example payload and synthetic variants on Linux, not inside a live Claude Code session and not on a Mac. The test pulls every bash and Python block out of the built page and runs it in a scratch git repository on a branch called feature/statusline. It checks that both scripts print exactly the line recorded for each payload. The variants cover null context fields, a missing cost, rate_limits placed before context_window, a folder outside any repository, a detached HEAD and JSON on a single line. It also runs each command in the settings blocks through sh -c, and the colour replacement above. Every field the scripts read is checked against the field list recorded from the docs. What that cannot prove is how a given terminal draws the result, or what a future Claude Code release puts in the payload. If yours looks wrong, the pipe test in Troubleshooting shows whether the script or the setup is at fault.

← Back to all posts

Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.

Back to the top