← All postsHow Do You Set Up a Status Line in Claude Code?

How Do You Set Up a Status Line in Claude Code?

Carlos GarciaCarlos GarciaPublished

How Do You Set Up a Status Line in Claude Code?

Claude Code sessions have a habit of running long, and the two things you most want to know while one is running are the two things the interface does not shout at you: how much of the context window you have burned, and which branch you are actually on.

The status line exists for exactly that. It is a configurable row at the bottom of the session that runs a shell script of your choosing, hands it a blob of JSON about the current session, and displays whatever the script prints. Context percentage, session cost, git branch, the model name, a coloured progress bar — all of it is just text your script decided to output.

The setup is genuinely small. The interesting part is the data you get to work with, and the handful of failure modes that leave the row stubbornly blank.

What Is the Short Answer?

Add a statusLine field to your settings file, set type to "command", and point command at a script. Claude Code runs that script, pipes session JSON to it on stdin, and prints its stdout at the bottom of the session.

The minimum viable configuration in ~/.claude/settings.json is three lines:

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

If you would rather not write the script yourself, run the /statusline command and describe what you want in plain language. It generates a script in ~/.claude/ and updates your settings for you.

Running an audit on your codebase but not your site? A free SEO audit from SEO Stuff shows you what is costing you traffic.

Free audit

Where do you stand in AI search?

A manual SEO + AI search audit of your site: how ChatGPT, Gemini, Perplexity and Claude read and cite it today, and what to fix first. Report within 48 hours, no credit card.

Get my free audit

Or have it all done: AI SEO services, $999 once

What the Status Line Actually Is

It is worth being precise about the mechanism, because it explains every quirk further down.

The status line is a shell command that Claude Code executes, not a template language or a set of built-in widgets. There is no list of placeholders to choose from. Claude Code starts your process, writes JSON to its stdin, waits for it to exit, and renders what came back on stdout.

That has three consequences that matter.

It Is Your Script, With Your Dependencies

Most published examples use jq to parse the incoming JSON, which is a separate command-line tool you may need to install. You can equally write the script in Python or Node, or anything else that reads stdin and prints a line. The command field runs in a shell, so a short inline command works too rather than a file:

{
  "statusLine": {
    "type": "command",
    "command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
  }
}

It Costs You Nothing in Tokens

The status line runs locally. It does not consume API tokens, and it is not something Claude sees or reasons about. It is a readout, not part of the conversation.

The row renders above the built-in footer badges rather than instead of them. With a custom status line configured, though, Claude Code stops showing most of the footer's keyboard hints, including esc to interrupt, the ? for shortcuts fallback and the voice dictation hint. If all you actually wanted was clickable link badges when an ID appears in the conversation, there is a separate footerLinksRegexes setting for that and you do not need a script at all.

How Do You Build One, Step by Step?

Here is the manual version of what /statusline does for you. The example shows the model, the current folder and context usage.

Step 1: write a script that reads JSON from stdin. Save this as ~/.claude/statusline.sh:

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

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 // 0 in the jq expressions is a fallback for null, which matters because several fields are null until the first API response lands. The ${DIR##*/} is shell parameter expansion that strips everything up to the last slash, leaving just the folder name.

Step 2: make it executable. This step gets skipped more often than any other:

chmod +x ~/.claude/statusline.sh

Step 3: point your settings at it. Add the statusLine block shown earlier to ~/.claude/settings.json, or to your project settings if you want it scoped to one repository. Claude Code reloads settings automatically and runs the script as soon as you save.

Two optional fields are worth knowing about from the start. padding adds horizontal spacing in characters, defaulting to 0, and is relative to the interface's built-in spacing rather than an absolute distance from the terminal edge. hideVimModeIndicator suppresses the built-in -- INSERT -- text, which you want set to true if your script renders vim.mode itself and you would otherwise see the mode twice.

To remove a status line later, run /statusline and ask it to clear or delete, or just delete the statusLine field from your settings by hand.

Your dashboards show context usage. What shows your search visibility? Get a free audit from SEO Stuff.

What Data Does Your Script Receive?

This is the part worth reading properly, because the field list is much richer than most status lines use.

Session and model. model.id and model.display_name give you the current model. session_id is a unique identifier, session_name carries a custom name set with --name or /rename where one exists, and version is the Claude Code version. transcript_path points at the conversation transcript file.

Directories and repository. cwd and workspace.current_dir hold the same value, with the latter preferred for consistency. workspace.project_dir is where Claude Code was launched, which can differ from cwd if the working directory changes mid-session. workspace.added_dirs lists directories added via /add-dir. workspace.repo.host, workspace.repo.owner and workspace.repo.name are parsed from the origin remote and are absent outside a git repository or when no origin is configured.

Context window. context_window.used_percentage and context_window.remaining_percentage are pre-calculated, which is why almost every example reaches for them rather than doing arithmetic. context_window.context_window_size is the maximum in tokens, 200000 by default or 1000000 for models with extended context. context_window.total_input_tokens and total_output_tokens give raw counts from the most recent API response, with input including cache reads and writes.

Cost and duration. cost.total_cost_usd is an estimated session cost computed client-side at list price, and the documentation is explicit that it may differ from your actual bill. cost.total_duration_ms is wall-clock time, accumulating across resumes. cost.total_lines_added and total_lines_removed track code churn.

Rate limits. rate_limits.five_hour.used_percentage and rate_limits.seven_day.used_percentage run from 0 to 100, with matching resets_at fields in Unix epoch seconds.

Session state. effort.level reflects the live reasoning effort and is absent on models that do not support the parameter. thinking.enabled, fast_mode and output_style.name cover the rest. vim.mode gives NORMAL, INSERT, VISUAL or VISUAL LINE when vim mode is on.

Pull requests and worktrees. pr.number and pr.url describe the open pull request for the current branch, with pr.review_state as approved, pending, changes_requested or draft. On a GitLab remote these are filled from the branch's open merge request instead, with pr.kind set to mr. The worktree.* fields are present only inside a worktree session, while workspace.git_worktree is populated for any git worktree.

Visibility is a metric too. Find out where you rank with a free audit from SEO Stuff.

When Does It Update?

The status line is event-driven rather than on a timer by default, and knowing the triggers saves you from writing a clock that never ticks.

Your script runs once when a session starts, including on resume. After that it re-runs when a new assistant message arrives, when /compact finishes, when the permission mode changes, when vim mode toggles, when you change the command in settings, when a rate-limit window in the data your script last received reaches its resets_at, and when a warm prompt cache reaches its expires_at.

Updates are debounced at 300ms, so rapid changes batch together and the script runs once after things settle. A change to the command itself skips the debounce and runs immediately. If a new trigger fires while your script is still running, the in-flight script is cancelled.

The gap in that list is idle time. If the main session is sitting quiet — waiting on background subagents, say — nothing triggers an update, and a status line showing a clock or an externally-sourced value goes stale. That is what the optional refreshInterval field is for: set it to re-run the command every N seconds in addition to the event triggers, with a minimum of 1. Leave it unset if your status line only shows session state.

What Can Your Script Output?

More than one line of plain text, though restraint pays off here.

Multiple lines work, with each echo or print rendering as a separate row. A common pattern puts git information on the first line and a context bar on the second.

ANSI colour codes work where the terminal supports them, so \033[32m for green and similar.

OSC 8 escape sequences make text clickable, which requires a terminal with hyperlink support such as iTerm2, Kitty or WezTerm. Terminal.app does not support them.

One non-obvious constraint: Claude Code captures your script's output rather than 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 running your script.

When Is a Status Line Worth It?

Not every session needs one, and there is a real cost to a cluttered bottom row.

It is clearly worth it if you run long sessions. Watching context usage climb is the single best argument for the feature. Knowing you are at 80 percent before you start a large refactor changes what you do next.

It is worth it if you work across several sessions. session_name or the directory name on screen removes the "which window is this" problem entirely.

It is worth it if you keep losing track of branch state. Git branch and dirty status always visible is a small thing that prevents a specific category of mistake.

It is probably not worth it for short, single-purpose sessions, and it is actively counterproductive if you pack in so many fields that the row truncates. Start with two or three values you will actually read.

What Goes Wrong?

Almost every status line problem presents the same way — a blank row — and there are more causes than you would guess.

The script is not executable. Run chmod +x on it. This is the most common cause.

The script writes to stderr instead of stdout. Only stdout is displayed.

The script exits with a non-zero code. Claude Code displays output only after the script exits with code 0. A non-zero exit or no output at all leaves the row blank.

The workspace is not trusted yet. Because statusLine executes a shell command, it runs under the same workspace trust rule as hooks in settings files. Until you accept the trust dialog for the folder, or for a parent whose trust extends to it, the status line stays blank. Running claude --debug logs Status line command skipped: workspace trust not accepted in that case.

A managed setting is overriding you. If disableAllHooks is true outside managed settings, Claude Code runs only a statusLine from managed settings — and with none configured there, the status line is disabled entirely. Separately, if your organisation sets allowManagedHooksOnly, your custom status line disappears without warning and you can only get one from managed settings. Neither failure announces itself, so if you are on a managed machine, ask your administrator before debugging your script further.

Windows paths with backslashes. With Git Bash installed, backslashes in the command path are consumed as escape characters before the script runs. Use forward slashes.

Values show as -- or empty. Fields can be null before the first API response completes, which is exactly what the // 0 fallbacks are for.

The script is slow. A slow script blocks the status line from updating until it finishes, leaving stale output on screen. Keep it fast, and test it independently with mock input before wiring it up.

The general-purpose diagnostic is claude --debug, which logs your script's stderr on every invocation and its exit code on the first invocation in a session. Failing that, asking Claude to read your settings file and run the statusLine command directly surfaces most errors in one step.

One more thing to expect rather than fix: outside fullscreen rendering, notifications share the status line row. System notifications such as MCP server errors and auto-updates appear on the right of it, and on a narrow terminal they can truncate your output.

Status Line vs the Alternatives

Versus /context. The two disagree, and it is not a bug. The status line reports counts from the last API response, while /context adds an estimate for messages since that response, so /context can read higher until the next response lands. For a simple, accurate at-a-glance state, used_percentage is the one to display.

Versus verbose mode. Enabling verbose mode adds a token counter to the notification area, showing context size as the last API response reported it. That is less flexible than a status line but requires no setup at all.

Versus hooks. The comparison people reach for, and the distinction is clean: a status line is a read-only display that runs on UI events, while a hook runs at defined points in the agent loop and can actually change what happens, including blocking a tool call. If you want to see something, write a status line. If you want to enforce something, you want a hook — our guide to Claude Code hooks covers the exit code contract and the events available.

One last thing worth checking. Get a free SEO audit from SEO Stuff and see what your site looks like from the outside.

Final Thoughts

The status line is a small feature that pays for itself the first time it stops you running out of context mid-task. Set it up with two fields, live with it for a week, and add a third only when you notice you are missing something.

The thing to internalise is that this is a script contract, not a configuration menu: stdin gets JSON, stdout gets rendered, exit code 0 or nothing displays. Every troubleshooting step above follows from that. When the row goes blank, work down the contract rather than rewriting the script — executable, stdout, exit code, trust dialog, managed settings — and the cause is usually the second or third thing you check.

And if your organisation manages your settings, check that first. A status line that works perfectly on your laptop and silently never appears on a work machine is a policy question, not a scripting one.

Want this done for you?

SEO Stuff gets businesses cited by ChatGPT, Gemini, Perplexity and Claude, and ranking in Google. Start with the free audit, a call, or the package.

See where you stand

A free, manual SEO + AI search audit of your site, with a report within 48 hours. No credit card, no sales call.

Get my free audit

Talk it through

A free 15-minute call with the founder about whether AI search is a meaningful opportunity for your business.

Free strategy call

Have it done

The Done-For-You Package: audit, 10 pages of content, 3 DR50+ placements, dashboard. $999 once, delivered in 21 business days.

See the package