2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.3> Use this file to discover all available pages before exploring further.
4 4
5# Status line configuration5# Customize your status line
6 6
7> Create a custom status line for Claude Code to display contextual information7> Configure a custom status bar to monitor context window usage, costs, and git status in Claude Code
8 8
9Make Claude Code your own with a custom status line that displays at the bottom of the Claude Code interface, similar to how terminal prompts (PS1) work in shells like Oh-my-zsh.9The 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, giving you a persistent, at-a-glance view of context usage, costs, git status, or anything else you want to track.
10 10
11## Create a custom status line11Status lines are useful when you:
12 12
13You can either:13* Want to monitor context window usage as you work
14* Need to track session costs
15* Work across multiple sessions and need to distinguish them
16* Want git branch and status always visible
14 17
15* Run `/statusline` to ask Claude Code to help you set up a custom status line. By default, it will try to reproduce your terminal's prompt, but you can provide additional instructions about the behavior you want to Claude Code, such as `/statusline show the model name in orange`18Here's an example of a [multi-line status line](#display-multiple-lines) that displays git info on the first line and a color-coded context bar on the second.
16 19
17* Directly add a `statusLine` command to your `.claude/settings.json`:20<Frame>
21 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=60f11387658acc9ff75158ae85f2ac87" alt="A multi-line status line showing model name, directory, git branch on the first line, and a context usage progress bar with cost and duration on the second line" data-og-width="776" width="776" data-og-height="212" height="212" data-path="images/statusline-multiline.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=2e448b44c332620e6c9c2be4ded992e5 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=f796af2db9c68ab2ddbc5136840b9551 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d29c13d6164773198a0b2c47b31f6c09 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d7720e5f51310185c0c02152f6c10d8b 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=b4e008cde27990a8d5783e41e5b93246 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=40ab24813303dc2e4c09f2675f3faf6e 2500w" />
22</Frame>
23
24This page walks through [setting up a basic status line](#set-up-a-status-line), explains [how the data flows](#how-status-lines-work) from Claude Code to your script, lists [all the fields you can display](#available-data), and provides [ready-to-use examples](#examples) for common patterns like git status, cost tracking, and progress bars.
25
26## Set up a status line
27
28Use the [`/statusline` command](#use-the-statusline-command) to have Claude Code generate a script for you, or [manually create a script](#manually-configure-a-status-line) and add it to your settings.
29
30### Use the /statusline command
31
32The `/statusline` command accepts natural language instructions describing what you want displayed. Claude Code generates a script file in `~/.claude/` and updates your settings automatically:
33
34```
35/statusline show model name and context percentage with a progress bar
36```
37
38### Manually configure a status line
39
40Add a `statusLine` field to your user settings (`~/.claude/settings.json`, where `~` is your home directory) or [project settings](/en/settings#settings-files). Set `type` to `"command"` and point `command` to a script path or an inline shell command. For a full walkthrough of creating a script, see [Build a status line step by step](#build-a-status-line-step-by-step).
18 41
19```json theme={null}42```json theme={null}
20{43{
21 "statusLine": {44 "statusLine": {
22 "type": "command",45 "type": "command",
23 "command": "~/.claude/statusline.sh",46 "command": "~/.claude/statusline.sh",
24 "padding": 0 // Optional: set to 0 to let status line go to edge47 "padding": 2
48 }
49}
50```
51
52The `command` field runs in a shell, so you can also use inline commands instead of a script file. This example uses `jq` to parse the JSON input and display the model name and context percentage:
53
54```json theme={null}
55{
56 "statusLine": {
57 "type": "command",
58 "command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
25 }59 }
26}60}
27```61```
28 62
29## How it Works63The optional `padding` field adds extra horizontal spacing (in characters) to the status line content. Defaults to `0`. This padding is in addition to the interface's built-in spacing, so it controls relative indentation rather than absolute distance from the terminal edge.
30 64
31* The status line is updated when the conversation messages update65### Disable the status line
32* Updates run at most every 300 ms
33* The first line of stdout from your command becomes the status line text
34* ANSI color codes are supported for styling your status line
35* Claude Code passes contextual information about the current session (model, directories, etc.) as JSON to your script via stdin
36 66
37## JSON Input Structure67Run `/statusline` and ask it to remove or clear your status line (e.g., `/statusline delete`, `/statusline clear`, `/statusline remove it`). You can also manually delete the `statusLine` field from your settings.json.
38 68
39Your status line command receives structured data via stdin in JSON format:69## Build a status line step by step
40 70
41```json theme={null}71This walkthrough shows what's happening under the hood by manually creating a status line that displays the current model, working directory, and context window usage percentage.
42{72
43 "hook_event_name": "Status",73<Note>Running [`/statusline`](#use-the-statusline-command) with a description of what you want configures all of this for you automatically.</Note>
44 "session_id": "abc123...",74
45 "transcript_path": "/path/to/transcript.json",75These examples use Bash scripts, which work on macOS and Linux. On Windows, you can run Bash scripts through [WSL (Windows Subsystem for Linux)](https://learn.microsoft.com/en-us/windows/wsl/install) or rewrite them in PowerShell.
76
77<Frame>
78 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=696445e59ca0059213250651ad23db6b" alt="A status line showing model name, directory, and context percentage" data-og-width="726" width="726" data-og-height="164" height="164" data-path="images/statusline-quickstart.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=728c4bd06c8559cb46ddffffad983373 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=f9d28e0f8f48f695167dd1d632a6cf4f 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=57a2803a18cafe8cf1aa05619444f20c 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=52cdd52865842f0cda24489dd5310d3b 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=f8876ea1f72bf40bd0aeec483ee20164 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-quickstart.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=6b1524305c7c71122cde65d0c3822374 2500w" />
79</Frame>
80
81<Steps>
82 <Step title="Create a script that reads JSON and prints output">
83 Claude Code sends JSON data to your script via stdin. This script uses [`jq`](https://jqlang.github.io/jq/), a command-line JSON parser you may need to install, to extract the model name, directory, and context percentage, then prints a formatted line.
84
85 Save this to `~/.claude/statusline.sh` (where `~` is your home directory, such as `/Users/username` on macOS or `/home/username` on Linux):
86
87 ```bash theme={null}
88 #!/bin/bash
89 # Read JSON data that Claude Code sends to stdin
90 input=$(cat)
91
92 # Extract fields using jq
93 MODEL=$(echo "$input" | jq -r '.model.display_name')
94 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
95 # The "// 0" provides a fallback if the field is null
96 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
97
98 # Output the status line - ${DIR##*/} extracts just the folder name
99 echo "[$MODEL] 📁 ${DIR##*/} | ${PCT}% context"
100 ```
101 </Step>
102
103 <Step title="Make it executable">
104 Mark the script as executable so your shell can run it:
105
106 ```bash theme={null}
107 chmod +x ~/.claude/statusline.sh
108 ```
109 </Step>
110
111 <Step title="Add to settings">
112 Tell Claude Code to run your script as the status line. Add this configuration to `~/.claude/settings.json`, which sets `type` to `"command"` (meaning "run this shell command") and points `command` to your script:
113
114 ```json theme={null}
115 {
116 "statusLine": {
117 "type": "command",
118 "command": "~/.claude/statusline.sh"
119 }
120 }
121 ```
122
123 Your status line appears at the bottom of the interface. Settings reload automatically, but changes won't appear until your next interaction with Claude Code.
124 </Step>
125</Steps>
126
127## How status lines work
128
129Claude Code runs your script and pipes [JSON session data](#available-data) to it via stdin. Your script reads the JSON, extracts what it needs, and prints text to stdout. Claude Code displays whatever your script prints.
130
131**When it updates**
132
133Your script runs after each new assistant message, when the permission mode changes, or when vim mode toggles. Updates are debounced at 300ms, meaning rapid changes batch together and your script runs once things settle. If a new update triggers while your script is still running, the in-flight execution is cancelled. If you edit your script, the changes won't appear until your next interaction with Claude Code triggers an update.
134
135**What your script can output**
136
137* **Multiple lines**: each `echo` or `print` statement displays as a separate row. See the [multi-line example](#display-multiple-lines).
138* **Colors**: use [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) like `\033[32m` for green (terminal must support them). See the [git status example](#git-status-with-colors).
139* **Links**: use [OSC 8 escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#OSC) to make text clickable (Cmd+click on macOS, Ctrl+click on Windows/Linux). Requires a terminal that supports hyperlinks like iTerm2, Kitty, or WezTerm. See the [clickable links example](#clickable-links).
140
141<Note>The status line runs locally and does not consume API tokens. It temporarily hides during certain UI interactions, including autocomplete suggestions, the help menu, and permission prompts.</Note>
142
143## Available data
144
145Claude Code sends the following JSON fields to your script via stdin:
146
147| Field | Description |
148| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
149| `model.id`, `model.display_name` | Current model identifier and display name |
150| `cwd`, `workspace.current_dir` | Current working directory. Both fields contain the same value; `workspace.current_dir` is preferred for consistency with `workspace.project_dir`. |
151| `workspace.project_dir` | Directory where Claude Code was launched, which may differ from `cwd` if the working directory changes during a session |
152| `cost.total_cost_usd` | Total session cost in USD |
153| `cost.total_duration_ms` | Total wall-clock time since the session started, in milliseconds |
154| `cost.total_api_duration_ms` | Total time spent waiting for API responses in milliseconds |
155| `cost.total_lines_added`, `cost.total_lines_removed` | Lines of code changed |
156| `context_window.total_input_tokens`, `context_window.total_output_tokens` | Cumulative token counts across the session |
157| `context_window.context_window_size` | Maximum context window size in tokens. 200000 by default, or 1000000 for models with extended context. |
158| `context_window.used_percentage` | Pre-calculated percentage of context window used |
159| `context_window.remaining_percentage` | Pre-calculated percentage of context window remaining |
160| `context_window.current_usage` | Token counts from the last API call, described in [context window fields](#context-window-fields) |
161| `exceeds_200k_tokens` | Whether the total token count (input, cache, and output tokens combined) from the most recent API response exceeds 200k. This is a fixed threshold regardless of actual context window size. |
162| `session_id` | Unique session identifier |
163| `transcript_path` | Path to conversation transcript file |
164| `version` | Claude Code version |
165| `output_style.name` | Name of the current output style |
166| `vim.mode` | Current vim mode (`NORMAL` or `INSERT`) when [vim mode](/en/interactive-mode#vim-editor-mode) is enabled |
167| `agent.name` | Agent name when running with the `--agent` flag or agent settings configured |
168
169<Accordion title="Full JSON schema">
170 Your status line command receives this JSON structure via stdin:
171
172 ```json theme={null}
173 {
46 "cwd": "/current/working/directory",174 "cwd": "/current/working/directory",
175 "session_id": "abc123...",
176 "transcript_path": "/path/to/transcript.jsonl",
47 "model": {177 "model": {
48 "id": "claude-opus-4-1",178 "id": "claude-opus-4-6",
49 "display_name": "Opus"179 "display_name": "Opus"
50 },180 },
51 "workspace": {181 "workspace": {
67 "total_input_tokens": 15234,197 "total_input_tokens": 15234,
68 "total_output_tokens": 4521,198 "total_output_tokens": 4521,
69 "context_window_size": 200000,199 "context_window_size": 200000,
70 "used_percentage": 42.5,200 "used_percentage": 8,
71 "remaining_percentage": 57.5,201 "remaining_percentage": 92,
72 "current_usage": {202 "current_usage": {
73 "input_tokens": 8500,203 "input_tokens": 8500,
74 "output_tokens": 1200,204 "output_tokens": 1200,
75 "cache_creation_input_tokens": 5000,205 "cache_creation_input_tokens": 5000,
76 "cache_read_input_tokens": 2000206 "cache_read_input_tokens": 2000
77 }207 }
208 },
209 "exceeds_200k_tokens": false,
210 "vim": {
211 "mode": "NORMAL"
212 },
213 "agent": {
214 "name": "security-reviewer"
78 }215 }
79}216 }
80```217 ```
81 218
82## Example Scripts219 **Fields that may be absent** (not present in JSON):
83 220
84### Simple Status Line221 * `vim`: appears only when vim mode is enabled
222 * `agent`: appears only when running with the `--agent` flag or agent settings configured
85 223
86```bash theme={null}224 **Fields that may be `null`**:
87#!/bin/bash
88# Read JSON input from stdin
89input=$(cat)
90 225
91# Extract values using jq226 * `context_window.current_usage`: `null` before the first API call in a session
92MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name')227 * `context_window.used_percentage`, `context_window.remaining_percentage`: may be `null` early in the session
93CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir')
94 228
95echo "[$MODEL_DISPLAY] 📁 ${CURRENT_DIR##*/}"229 Handle missing fields with conditional access and null values with fallback defaults in your scripts.
96```230</Accordion>
231
232### Context window fields
233
234The `context_window` object provides two ways to track context usage:
235
236* **Cumulative totals** (`total_input_tokens`, `total_output_tokens`): sum of all tokens across the entire session, useful for tracking total consumption
237* **Current usage** (`current_usage`): token counts from the most recent API call, use this for accurate context percentage since it reflects the actual context state
238
239The `current_usage` object contains:
240
241* `input_tokens`: input tokens in current context
242* `output_tokens`: output tokens generated
243* `cache_creation_input_tokens`: tokens written to cache
244* `cache_read_input_tokens`: tokens read from cache
245
246The `used_percentage` field is calculated from input tokens only: `input_tokens + cache_creation_input_tokens + cache_read_input_tokens`. It does not include `output_tokens`.
247
248If you calculate context percentage manually from `current_usage`, use the same input-only formula to match `used_percentage`.
249
250The `current_usage` object is `null` before the first API call in a session.
251
252## Examples
253
254These examples show common status line patterns. To use any example:
255
2561. Save the script to a file like `~/.claude/statusline.sh` (or `.py`/`.js`)
2572. Make it executable: `chmod +x ~/.claude/statusline.sh`
2583. Add the path to your [settings](#manually-configure-a-status-line)
259
260The Bash examples use [`jq`](https://jqlang.github.io/jq/) to parse JSON. Python and Node.js have built-in JSON parsing.
261
262### Context window usage
263
264Display the current model and context window usage with a visual progress bar. Each script reads JSON from stdin, extracts the `used_percentage` field, and builds a 10-character bar where filled blocks (▓) represent usage:
265
266<Frame>
267 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=15b58ab3602f036939145dde3165c6f7" alt="A status line showing model name and a progress bar with percentage" data-og-width="448" width="448" data-og-height="152" height="152" data-path="images/statusline-context-window-usage.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=a18fecd31f06b16e984b1ab3310acbc0 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=2f4b3caff156efede2ded995dbaf167f 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=8f6b8c7e7d3a999c570e96ad2ea13d5a 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d9334e6a08e6f11a253733c8592774a9 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=e79490da8f62952e4d92837c408e63dc 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=6f7c9ef8e629a794969c54b24163f92d 2500w" />
268</Frame>
269
270<CodeGroup>
271 ```bash Bash theme={null}
272 #!/bin/bash
273 # Read all of stdin into a variable
274 input=$(cat)
275
276 # Extract fields with jq, "// 0" provides fallback for null
277 MODEL=$(echo "$input" | jq -r '.model.display_name')
278 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
279
280 # Build progress bar: printf creates spaces, tr replaces with blocks
281 BAR_WIDTH=10
282 FILLED=$((PCT * BAR_WIDTH / 100))
283 EMPTY=$((BAR_WIDTH - FILLED))
284 BAR=""
285 [ "$FILLED" -gt 0 ] && BAR=$(printf "%${FILLED}s" | tr ' ' '▓')
286 [ "$EMPTY" -gt 0 ] && BAR="${BAR}$(printf "%${EMPTY}s" | tr ' ' '░')"
287
288 echo "[$MODEL] $BAR $PCT%"
289 ```
290
291 ```python Python theme={null}
292 #!/usr/bin/env python3
293 import json, sys
97 294
98### Git-Aware Status Line295 # json.load reads and parses stdin in one step
296 data = json.load(sys.stdin)
297 model = data['model']['display_name']
298 # "or 0" handles null values
299 pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
99 300
100```bash theme={null}301 # String multiplication builds the bar
101#!/bin/bash302 filled = pct * 10 // 100
102# Read JSON input from stdin303 bar = '▓' * filled + '░' * (10 - filled)
103input=$(cat)
104 304
105# Extract values using jq305 print(f"[{model}] {bar} {pct}%")
106MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name')306 ```
107CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir')
108 307
109# Show git branch if in a git repo308 ```javascript Node.js theme={null}
110GIT_BRANCH=""309 #!/usr/bin/env node
111if git rev-parse --git-dir > /dev/null 2>&1; then310 // Node.js reads stdin asynchronously with events
311 let input = '';
312 process.stdin.on('data', chunk => input += chunk);
313 process.stdin.on('end', () => {
314 const data = JSON.parse(input);
315 const model = data.model.display_name;
316 // Optional chaining (?.) safely handles null fields
317 const pct = Math.floor(data.context_window?.used_percentage || 0);
318
319 // String.repeat() builds the bar
320 const filled = Math.floor(pct * 10 / 100);
321 const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled);
322
323 console.log(`[${model}] ${bar} ${pct}%`);
324 });
325 ```
326</CodeGroup>
327
328### Git status with colors
329
330Show git branch with color-coded indicators for staged and modified files. This script uses [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) for terminal colors: `\033[32m` is green, `\033[33m` is yellow, and `\033[0m` resets to default.
331
332<Frame>
333 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=e656f34f90d1d9a1d0e220988914345f" alt="A status line showing model, directory, git branch, and colored indicators for staged and modified files" data-og-width="742" width="742" data-og-height="178" height="178" data-path="images/statusline-git-context.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=c1bced5f46afdc9aae549702591f8457 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=debe46a7a888234ec692751243bba492 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=3a069d5c8b0395908e42f0e295fd4854 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=26aff0978865756d5ea299a22e5e9afd 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d5ac1d59881e6f2032af053557dc4590 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-git-context.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=46febbf34b0ee646502d095433132709 2500w" />
334</Frame>
335
336Each script checks if the current directory is a git repository, counts staged and modified files, and displays color-coded indicators:
337
338<CodeGroup>
339 ```bash Bash theme={null}
340 #!/bin/bash
341 input=$(cat)
342
343 MODEL=$(echo "$input" | jq -r '.model.display_name')
344 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
345
346 GREEN='\033[32m'
347 YELLOW='\033[33m'
348 RESET='\033[0m'
349
350 if git rev-parse --git-dir > /dev/null 2>&1; then
112 BRANCH=$(git branch --show-current 2>/dev/null)351 BRANCH=$(git branch --show-current 2>/dev/null)
113 if [ -n "$BRANCH" ]; then352 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
114 GIT_BRANCH=" | 🌿 $BRANCH"353 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
115 fi
116fi
117 354
118echo "[$MODEL_DISPLAY] 📁 ${CURRENT_DIR##*/}$GIT_BRANCH"355 GIT_STATUS=""
119```356 [ "$STAGED" -gt 0 ] && GIT_STATUS="${GREEN}+${STAGED}${RESET}"
357 [ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${YELLOW}~${MODIFIED}${RESET}"
120 358
121### Python Example359 echo -e "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH $GIT_STATUS"
360 else
361 echo "[$MODEL] 📁 ${DIR##*/}"
362 fi
363 ```
122 364
123```python theme={null}365 ```python Python theme={null}
124#!/usr/bin/env python3366 #!/usr/bin/env python3
125import json367 import json, sys, subprocess, os
126import sys
127import os
128 368
129# Read JSON from stdin369 data = json.load(sys.stdin)
130data = json.load(sys.stdin)370 model = data['model']['display_name']
371 directory = os.path.basename(data['workspace']['current_dir'])
131 372
132# Extract values373 GREEN, YELLOW, RESET = '\033[32m', '\033[33m', '\033[0m'
133model = data['model']['display_name']
134current_dir = os.path.basename(data['workspace']['current_dir'])
135 374
136# Check for git branch
137git_branch = ""
138if os.path.exists('.git'):
139 try:375 try:
140 with open('.git/HEAD', 'r') as f:376 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
141 ref = f.read().strip()377 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
142 if ref.startswith('ref: refs/heads/'):378 staged_output = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
143 git_branch = f" | 🌿 {ref.replace('ref: refs/heads/', '')}"379 modified_output = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
380 staged = len(staged_output.split('\n')) if staged_output else 0
381 modified = len(modified_output.split('\n')) if modified_output else 0
382
383 git_status = f"{GREEN}+{staged}{RESET}" if staged else ""
384 git_status += f"{YELLOW}~{modified}{RESET}" if modified else ""
385
386 print(f"[{model}] 📁 {directory} | 🌿 {branch} {git_status}")
144 except:387 except:
145 pass388 print(f"[{model}] 📁 {directory}")
389 ```
146 390
147print(f"[{model}] 📁 {current_dir}{git_branch}")391 ```javascript Node.js theme={null}
148```392 #!/usr/bin/env node
393 const { execSync } = require('child_process');
394 const path = require('path');
149 395
150### Node.js Example396 let input = '';
397 process.stdin.on('data', chunk => input += chunk);
398 process.stdin.on('end', () => {
399 const data = JSON.parse(input);
400 const model = data.model.display_name;
401 const dir = path.basename(data.workspace.current_dir);
402
403 const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RESET = '\x1b[0m';
151 404
152```javascript theme={null}405 try {
153#!/usr/bin/env node406 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
407 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
408 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
409 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
410
411 let gitStatus = staged ? `${GREEN}+${staged}${RESET}` : '';
412 gitStatus += modified ? `${YELLOW}~${modified}${RESET}` : '';
154 413
155const fs = require('fs');414 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} ${gitStatus}`);
156const path = require('path');415 } catch {
416 console.log(`[${model}] 📁 ${dir}`);
417 }
418 });
419 ```
420</CodeGroup>
157 421
158// Read JSON from stdin422### Cost and duration tracking
159let input = '';423
160process.stdin.on('data', chunk => input += chunk);424Track your session's API costs and elapsed time. The `cost.total_cost_usd` field accumulates the cost of all API calls in the current session. The `cost.total_duration_ms` field measures total elapsed time since the session started, while `cost.total_api_duration_ms` tracks only the time spent waiting for API responses.
161process.stdin.on('end', () => {425
426Each script formats cost as currency and converts milliseconds to minutes and seconds:
427
428<Frame>
429 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=e3444a51fe6f3440c134bd5f1f08ad29" alt="A status line showing model name, session cost, and duration" data-og-width="588" width="588" data-og-height="180" height="180" data-path="images/statusline-cost-tracking.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=b1d35fa8acd792f559b6b1662ed10204 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=a3ed4330c3645fc28b87a6cab55be0b7 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=386ee2ed68a7d520eba20eac54f7fe52 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=479c2515e53f46d5d1da3b87a6dd993a 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=1340c7589a4cb89ec071234aba3571d1 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-cost-tracking.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=69056cf4fe3271770cac4dc1704bcd0a 2500w" />
430</Frame>
431
432<CodeGroup>
433 ```bash Bash theme={null}
434 #!/bin/bash
435 input=$(cat)
436
437 MODEL=$(echo "$input" | jq -r '.model.display_name')
438 COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
439 DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
440
441 COST_FMT=$(printf '$%.2f' "$COST")
442 DURATION_SEC=$((DURATION_MS / 1000))
443 MINS=$((DURATION_SEC / 60))
444 SECS=$((DURATION_SEC % 60))
445
446 echo "[$MODEL] 💰 $COST_FMT | ⏱️ ${MINS}m ${SECS}s"
447 ```
448
449 ```python Python theme={null}
450 #!/usr/bin/env python3
451 import json, sys
452
453 data = json.load(sys.stdin)
454 model = data['model']['display_name']
455 cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
456 duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
457
458 duration_sec = duration_ms // 1000
459 mins, secs = duration_sec // 60, duration_sec % 60
460
461 print(f"[{model}] 💰 ${cost:.2f} | ⏱️ {mins}m {secs}s")
462 ```
463
464 ```javascript Node.js theme={null}
465 #!/usr/bin/env node
466 let input = '';
467 process.stdin.on('data', chunk => input += chunk);
468 process.stdin.on('end', () => {
162 const data = JSON.parse(input);469 const data = JSON.parse(input);
470 const model = data.model.display_name;
471 const cost = data.cost?.total_cost_usd || 0;
472 const durationMs = data.cost?.total_duration_ms || 0;
473
474 const durationSec = Math.floor(durationMs / 1000);
475 const mins = Math.floor(durationSec / 60);
476 const secs = durationSec % 60;
477
478 console.log(`[${model}] 💰 $${cost.toFixed(2)} | ⏱️ ${mins}m ${secs}s`);
479 });
480 ```
481</CodeGroup>
482
483### Display multiple lines
484
485Your script can output multiple lines to create a richer display. Each `echo` statement produces a separate row in the status area.
486
487<Frame>
488 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=60f11387658acc9ff75158ae85f2ac87" alt="A multi-line status line showing model name, directory, git branch on the first line, and a context usage progress bar with cost and duration on the second line" data-og-width="776" width="776" data-og-height="212" height="212" data-path="images/statusline-multiline.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=2e448b44c332620e6c9c2be4ded992e5 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=f796af2db9c68ab2ddbc5136840b9551 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d29c13d6164773198a0b2c47b31f6c09 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d7720e5f51310185c0c02152f6c10d8b 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=b4e008cde27990a8d5783e41e5b93246 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-multiline.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=40ab24813303dc2e4c09f2675f3faf6e 2500w" />
489</Frame>
490
491This example combines several techniques: threshold-based colors (green under 70%, yellow 70-89%, red 90%+), a progress bar, and git branch info. Each `print` or `echo` statement creates a separate row:
492
493<CodeGroup>
494 ```bash Bash theme={null}
495 #!/bin/bash
496 input=$(cat)
497
498 MODEL=$(echo "$input" | jq -r '.model.display_name')
499 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
500 COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
501 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
502 DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
503
504 CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'
505
506 # Pick bar color based on context usage
507 if [ "$PCT" -ge 90 ]; then BAR_COLOR="$RED"
508 elif [ "$PCT" -ge 70 ]; then BAR_COLOR="$YELLOW"
509 else BAR_COLOR="$GREEN"; fi
510
511 FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
512 BAR=$(printf "%${FILLED}s" | tr ' ' '█')$(printf "%${EMPTY}s" | tr ' ' '░')
513
514 MINS=$((DURATION_MS / 60000)); SECS=$(((DURATION_MS % 60000) / 1000))
515
516 BRANCH=""
517 git rev-parse --git-dir > /dev/null 2>&1 && BRANCH=" | 🌿 $(git branch --show-current 2>/dev/null)"
163 518
164 // Extract values519 echo -e "${CYAN}[$MODEL]${RESET} 📁 ${DIR##*/}$BRANCH"
520 COST_FMT=$(printf '$%.2f' "$COST")
521 echo -e "${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${YELLOW}${COST_FMT}${RESET} | ⏱️ ${MINS}m ${SECS}s"
522 ```
523
524 ```python Python theme={null}
525 #!/usr/bin/env python3
526 import json, sys, subprocess, os
527
528 data = json.load(sys.stdin)
529 model = data['model']['display_name']
530 directory = os.path.basename(data['workspace']['current_dir'])
531 cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
532 pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
533 duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
534
535 CYAN, GREEN, YELLOW, RED, RESET = '\033[36m', '\033[32m', '\033[33m', '\033[31m', '\033[0m'
536
537 bar_color = RED if pct >= 90 else YELLOW if pct >= 70 else GREEN
538 filled = pct // 10
539 bar = '█' * filled + '░' * (10 - filled)
540
541 mins, secs = duration_ms // 60000, (duration_ms % 60000) // 1000
542
543 try:
544 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True, stderr=subprocess.DEVNULL).strip()
545 branch = f" | 🌿 {branch}" if branch else ""
546 except:
547 branch = ""
548
549 print(f"{CYAN}[{model}]{RESET} 📁 {directory}{branch}")
550 print(f"{bar_color}{bar}{RESET} {pct}% | {YELLOW}${cost:.2f}{RESET} | ⏱️ {mins}m {secs}s")
551 ```
552
553 ```javascript Node.js theme={null}
554 #!/usr/bin/env node
555 const { execSync } = require('child_process');
556 const path = require('path');
557
558 let input = '';
559 process.stdin.on('data', chunk => input += chunk);
560 process.stdin.on('end', () => {
561 const data = JSON.parse(input);
165 const model = data.model.display_name;562 const model = data.model.display_name;
166 const currentDir = path.basename(data.workspace.current_dir);563 const dir = path.basename(data.workspace.current_dir);
564 const cost = data.cost?.total_cost_usd || 0;
565 const pct = Math.floor(data.context_window?.used_percentage || 0);
566 const durationMs = data.cost?.total_duration_ms || 0;
567
568 const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';
167 569
168 // Check for git branch570 const barColor = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
169 let gitBranch = '';571 const filled = Math.floor(pct / 10);
572 const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
573
574 const mins = Math.floor(durationMs / 60000);
575 const secs = Math.floor((durationMs % 60000) / 1000);
576
577 let branch = '';
170 try {578 try {
171 const headContent = fs.readFileSync('.git/HEAD', 'utf8').trim();579 branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
172 if (headContent.startsWith('ref: refs/heads/')) {580 branch = branch ? ` | 🌿 ${branch}` : '';
173 gitBranch = ` | 🌿 ${headContent.replace('ref: refs/heads/', '')}`;581 } catch {}
174 }582
175 } catch (e) {583 console.log(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}`);
176 // Not a git repo or can't read HEAD584 console.log(`${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s`);
585 });
586 ```
587</CodeGroup>
588
589### Clickable links
590
591This example creates a clickable link to your GitHub repository. It reads the git remote URL, converts SSH format to HTTPS with `sed`, and wraps the repo name in OSC 8 escape codes. Hold Cmd (macOS) or Ctrl (Windows/Linux) and click to open the link in your browser.
592
593<Frame>
594 <img src="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=4bcc6e7deb7cf52f41ab85a219b52661" alt="A status line showing a clickable link to a GitHub repository" data-og-width="726" width="726" data-og-height="198" height="198" data-path="images/statusline-links.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=280&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=9386f78056f7be99599bcefe9e838180 280w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=560&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=d748012a0866c37dddc6babd4b7a88c4 560w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=840&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=bade8fbfcde957c1033c376c58b89131 840w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=1100&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=9f7e0c729ea093c3b39682619fd3f201 1100w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=1650&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=ccec17e90a89d82381888a4a9a8fa40e 1650w, https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-links.png?w=2500&fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=4d2e34a4d2f24e174cae1256c84f9a52 2500w" />
595</Frame>
596
597Each script gets the git remote URL, converts SSH format to HTTPS, and wraps the repo name in OSC 8 escape codes. The Bash version uses `printf '%b'` which interprets backslash escapes more reliably than `echo -e` across different shells:
598
599<CodeGroup>
600 ```bash Bash theme={null}
601 #!/bin/bash
602 input=$(cat)
603
604 MODEL=$(echo "$input" | jq -r '.model.display_name')
605
606 # Convert git SSH URL to HTTPS
607 REMOTE=$(git remote get-url origin 2>/dev/null | sed 's/git@github.com:/https:\/\/github.com\//' | sed 's/\.git$//')
608
609 if [ -n "$REMOTE" ]; then
610 REPO_NAME=$(basename "$REMOTE")
611 # OSC 8 format: \e]8;;URL\a then TEXT then \e]8;;\a
612 # printf %b interprets escape sequences reliably across shells
613 printf '%b' "[$MODEL] 🔗 \e]8;;${REMOTE}\a${REPO_NAME}\e]8;;\a\n"
614 else
615 echo "[$MODEL]"
616 fi
617 ```
618
619 ```python Python theme={null}
620 #!/usr/bin/env python3
621 import json, sys, subprocess, re, os
622
623 data = json.load(sys.stdin)
624 model = data['model']['display_name']
625
626 # Get git remote URL
627 try:
628 remote = subprocess.check_output(
629 ['git', 'remote', 'get-url', 'origin'],
630 stderr=subprocess.DEVNULL, text=True
631 ).strip()
632 # Convert SSH to HTTPS format
633 remote = re.sub(r'^git@github\.com:', 'https://github.com/', remote)
634 remote = re.sub(r'\.git$', '', remote)
635 repo_name = os.path.basename(remote)
636 # OSC 8 escape sequences
637 link = f"\033]8;;{remote}\a{repo_name}\033]8;;\a"
638 print(f"[{model}] 🔗 {link}")
639 except:
640 print(f"[{model}]")
641 ```
642
643 ```javascript Node.js theme={null}
644 #!/usr/bin/env node
645 const { execSync } = require('child_process');
646 const path = require('path');
647
648 let input = '';
649 process.stdin.on('data', chunk => input += chunk);
650 process.stdin.on('end', () => {
651 const data = JSON.parse(input);
652 const model = data.model.display_name;
653
654 try {
655 let remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
656 // Convert SSH to HTTPS format
657 remote = remote.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
658 const repoName = path.basename(remote);
659 // OSC 8 escape sequences
660 const link = `\x1b]8;;${remote}\x07${repoName}\x1b]8;;\x07`;
661 console.log(`[${model}] 🔗 ${link}`);
662 } catch {
663 console.log(`[${model}]`);
177 }664 }
665 });
666 ```
667</CodeGroup>
178 668
179 console.log(`[${model}] 📁 ${currentDir}${gitBranch}`);669### Cache expensive operations
180});
181```
182 670
183### Helper Function Approach671Your status line script runs frequently during active sessions. Commands like `git status` or `git diff` can be slow, especially in large repositories. This example caches git information to a temp file and only refreshes it every 5 seconds.
184
185For more complex bash scripts, you can create helper functions:
186
187```bash theme={null}
188#!/bin/bash
189# Read JSON input once
190input=$(cat)
191
192# Helper functions for common extractions
193get_model_name() { echo "$input" | jq -r '.model.display_name'; }
194get_current_dir() { echo "$input" | jq -r '.workspace.current_dir'; }
195get_project_dir() { echo "$input" | jq -r '.workspace.project_dir'; }
196get_version() { echo "$input" | jq -r '.version'; }
197get_cost() { echo "$input" | jq -r '.cost.total_cost_usd'; }
198get_duration() { echo "$input" | jq -r '.cost.total_duration_ms'; }
199get_lines_added() { echo "$input" | jq -r '.cost.total_lines_added'; }
200get_lines_removed() { echo "$input" | jq -r '.cost.total_lines_removed'; }
201get_input_tokens() { echo "$input" | jq -r '.context_window.total_input_tokens'; }
202get_output_tokens() { echo "$input" | jq -r '.context_window.total_output_tokens'; }
203get_context_window_size() { echo "$input" | jq -r '.context_window.context_window_size'; }
204
205# Use the helpers
206MODEL=$(get_model_name)
207DIR=$(get_current_dir)
208echo "[$MODEL] 📁 ${DIR##*/}"
209```
210 672
211### Context Window Usage673Use a stable, fixed filename for the cache file like `/tmp/statusline-git-cache`. Each status line invocation runs as a new process, so process-based identifiers like `$$`, `os.getpid()`, or `process.pid` produce a different value every time and the cache is never reused.
212 674
213Display the percentage of context window consumed. The `context_window` object contains:675Each script checks if the cache file is missing or older than 5 seconds before running git commands:
214 676
215* `total_input_tokens` / `total_output_tokens`: Cumulative totals across the entire session677<CodeGroup>
216* `used_percentage`: Pre-calculated percentage of context window used (0-100)678 ```bash Bash theme={null}
217* `remaining_percentage`: Pre-calculated percentage of context window remaining (0-100)679 #!/bin/bash
218* `current_usage`: Current context window usage from the last API call (may be `null` if no messages yet)680 input=$(cat)
219 * `input_tokens`: Input tokens in current context
220 * `output_tokens`: Output tokens generated
221 * `cache_creation_input_tokens`: Tokens written to cache
222 * `cache_read_input_tokens`: Tokens read from cache
223 681
224You can use the pre-calculated `used_percentage` and `remaining_percentage` fields directly, or calculate from `current_usage` for more control.682 MODEL=$(echo "$input" | jq -r '.model.display_name')
683 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
225 684
226**Simple approach using pre-calculated percentages:**685 CACHE_FILE="/tmp/statusline-git-cache"
686 CACHE_MAX_AGE=5 # seconds
227 687
228```bash theme={null}688 cache_is_stale() {
229#!/bin/bash689 [ ! -f "$CACHE_FILE" ] || \
230input=$(cat)690 # stat -f %m is macOS, stat -c %Y is Linux
691 [ $(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
692 }
231 693
232MODEL=$(echo "$input" | jq -r '.model.display_name')694 if cache_is_stale; then
233PERCENT_USED=$(echo "$input" | jq -r '.context_window.used_percentage // 0')695 if git rev-parse --git-dir > /dev/null 2>&1; then
696 BRANCH=$(git branch --show-current 2>/dev/null)
697 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
698 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
699 echo "$BRANCH|$STAGED|$MODIFIED" > "$CACHE_FILE"
700 else
701 echo "||" > "$CACHE_FILE"
702 fi
703 fi
234 704
235echo "[$MODEL] Context: ${PERCENT_USED}%"705 IFS='|' read -r BRANCH STAGED MODIFIED < "$CACHE_FILE"
236```
237 706
238**Advanced approach with manual calculation:**707 if [ -n "$BRANCH" ]; then
708 echo "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH +$STAGED ~$MODIFIED"
709 else
710 echo "[$MODEL] 📁 ${DIR##*/}"
711 fi
712 ```
239 713
240```bash theme={null}714 ```python Python theme={null}
241#!/bin/bash715 #!/usr/bin/env python3
242input=$(cat)716 import json, sys, subprocess, os, time
243 717
244MODEL=$(echo "$input" | jq -r '.model.display_name')718 data = json.load(sys.stdin)
245CONTEXT_SIZE=$(echo "$input" | jq -r '.context_window.context_window_size')719 model = data['model']['display_name']
246USAGE=$(echo "$input" | jq '.context_window.current_usage')720 directory = os.path.basename(data['workspace']['current_dir'])
247 721
248if [ "$USAGE" != "null" ]; then722 CACHE_FILE = "/tmp/statusline-git-cache"
249 # Calculate current context from current_usage fields723 CACHE_MAX_AGE = 5 # seconds
250 CURRENT_TOKENS=$(echo "$USAGE" | jq '.input_tokens + .cache_creation_input_tokens + .cache_read_input_tokens')724
251 PERCENT_USED=$((CURRENT_TOKENS * 100 / CONTEXT_SIZE))725 def cache_is_stale():
252 echo "[$MODEL] Context: ${PERCENT_USED}%"726 if not os.path.exists(CACHE_FILE):
253else727 return True
254 echo "[$MODEL] Context: 0%"728 return time.time() - os.path.getmtime(CACHE_FILE) > CACHE_MAX_AGE
255fi729
256```730 if cache_is_stale():
731 try:
732 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
733 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
734 staged = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
735 modified = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
736 staged_count = len(staged.split('\n')) if staged else 0
737 modified_count = len(modified.split('\n')) if modified else 0
738 with open(CACHE_FILE, 'w') as f:
739 f.write(f"{branch}|{staged_count}|{modified_count}")
740 except:
741 with open(CACHE_FILE, 'w') as f:
742 f.write("||")
743
744 with open(CACHE_FILE) as f:
745 branch, staged, modified = f.read().strip().split('|')
746
747 if branch:
748 print(f"[{model}] 📁 {directory} | 🌿 {branch} +{staged} ~{modified}")
749 else:
750 print(f"[{model}] 📁 {directory}")
751 ```
752
753 ```javascript Node.js theme={null}
754 #!/usr/bin/env node
755 const { execSync } = require('child_process');
756 const fs = require('fs');
757 const path = require('path');
758
759 let input = '';
760 process.stdin.on('data', chunk => input += chunk);
761 process.stdin.on('end', () => {
762 const data = JSON.parse(input);
763 const model = data.model.display_name;
764 const dir = path.basename(data.workspace.current_dir);
765
766 const CACHE_FILE = '/tmp/statusline-git-cache';
767 const CACHE_MAX_AGE = 5; // seconds
768
769 const cacheIsStale = () => {
770 if (!fs.existsSync(CACHE_FILE)) return true;
771 return (Date.now() / 1000) - fs.statSync(CACHE_FILE).mtimeMs / 1000 > CACHE_MAX_AGE;
772 };
773
774 if (cacheIsStale()) {
775 try {
776 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
777 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
778 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
779 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
780 fs.writeFileSync(CACHE_FILE, `${branch}|${staged}|${modified}`);
781 } catch {
782 fs.writeFileSync(CACHE_FILE, '||');
783 }
784 }
785
786 const [branch, staged, modified] = fs.readFileSync(CACHE_FILE, 'utf8').trim().split('|');
787
788 if (branch) {
789 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} +${staged} ~${modified}`);
790 } else {
791 console.log(`[${model}] 📁 ${dir}`);
792 }
793 });
794 ```
795</CodeGroup>
257 796
258## Tips797## Tips
259 798
260* Keep your status line concise - it should fit on one line799* **Test with mock input**: `echo '{"model":{"display_name":"Opus"},"context_window":{"used_percentage":25}}' | ./statusline.sh`
261* Use emojis (if your terminal supports them) and colors to make information scannable800* **Keep output short**: the status bar has limited width, so long output may get truncated or wrap awkwardly
262* Use `jq` for JSON parsing in Bash (see examples above)801* **Cache slow operations**: your script runs frequently during active sessions, so commands like `git status` can cause lag. See the [caching example](#cache-expensive-operations) for how to handle this.
263* Test your script by running it manually with mock JSON input: `echo '{"model":{"display_name":"Test"},"workspace":{"current_dir":"/test"}}' | ./statusline.sh`802
264* Consider caching expensive operations (like git status) if needed803Community projects like [ccstatusline](https://github.com/sirmalloc/ccstatusline) and [starship-claude](https://github.com/martinemde/starship-claude) provide pre-built configurations with themes and additional features.
265 804
266## Troubleshooting805## Troubleshooting
267 806
268* If your status line doesn't appear, check that your script is executable (`chmod +x`)807**Status line not appearing**
269* Ensure your script outputs to stdout (not stderr)808
809* Verify your script is executable: `chmod +x ~/.claude/statusline.sh`
810* Check that your script outputs to stdout, not stderr
811* Run your script manually to verify it produces output
812* If `disableAllHooks` is set to `true` in your settings, the status line is also disabled. Remove this setting or set it to `false` to re-enable.
813
814**Status line shows `--` or empty values**
815
816* Fields may be `null` before the first API response completes
817* Handle null values in your script with fallbacks such as `// 0` in jq
818* Restart Claude Code if values remain empty after multiple messages
819
820**Context percentage shows unexpected values**
821
822* Use `used_percentage` for accurate context state rather than cumulative totals
823* The `total_input_tokens` and `total_output_tokens` are cumulative across the session and may exceed the context window size
824* Context percentage may differ from `/context` output due to when each is calculated
825
826**OSC 8 links not clickable**
827
828* Verify your terminal supports OSC 8 hyperlinks (iTerm2, Kitty, WezTerm)
829* Terminal.app does not support clickable links
830* SSH and tmux sessions may strip OSC sequences depending on configuration
831* If escape sequences appear as literal text like `\e]8;;`, use `printf '%b'` instead of `echo -e` for more reliable escape handling
832
833**Display glitches with escape sequences**
834
835* Complex escape sequences (ANSI colors, OSC 8 links) can occasionally cause garbled output if they overlap with other UI updates
836* If you see corrupted text, try simplifying your script to plain text output
837* Multi-line status lines with escape codes are more prone to rendering issues than single-line plain text
838
839**Script errors or hangs**
840
841* Scripts that exit with non-zero codes or produce no output cause the status line to go blank
842* Slow scripts block the status line from updating until they complete. Keep scripts fast to avoid stale output.
843* If a new update triggers while a slow script is running, the in-flight script is cancelled
844* Test your script independently with mock input before configuring it
845
846**Notifications share the status line row**
847
848* System notifications like MCP server errors, auto-updates, and token warnings display on the right side of the same row as your status line
849* Enabling verbose mode adds a token counter to this area
850* On narrow terminals, these notifications may truncate your status line output