Customize your status line changedstatusline
Nearest release: v2.1.304, published an hour after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 19 Oct 2026 18:23 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.
Upstream edited
Recorded here
Lines+794added
Lines−826removed
From line
15
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits33to this page, all time
#### Last miss cause
The whole hunk
from line 15, old and new numbered
/
The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 15
1515
1616Here'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.
1717
18<Frame>
19 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-multiline.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=a9d0a2fe8e446d80b1abc46da3f93270" 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" width="1224" height="262" data-path="images/statusline-multiline.png" />
20</Frame>
18
2119
2220This 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.
2321
from line 72
7472
7573This walkthrough shows what `/statusline` sets up for you by manually creating a status line that displays the current model, working directory, and context window usage percentage.
7674
77<Note>Running [`/statusline`](#use-the-%2Fstatusline-command) with a description of what you want configures all of this for you automatically.</Note>
75> [!NOTE]
76>
77> Running [`/statusline`](#use-the-%2Fstatusline-command) with a description of what you want configures all of this for you automatically.
7878
7979These examples use Bash scripts, which work on macOS and Linux. On Windows, see [Windows configuration](#windows-configuration) for PowerShell and Git Bash examples.
8080
81<Frame>
82 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-quickstart.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=88a7eab9c1038dd098ee8e284d96b7e6" alt="A status line showing model name, directory, and context percentage" width="1224" height="224" data-path="images/statusline-quickstart.png" />
83</Frame>
84
85<Steps>
86 <Step title="Create a script that reads JSON and prints output">
87 Claude Code sends JSON data to your script via stdin. This script uses [`jq`](https://jqlang.org/), a command-line JSON parser you may need to install, to extract the model name, directory, and context percentage, then prints a formatted line.
88
89 Save this to `~/.claude/statusline.sh` (where `~` is your home directory, such as `/Users/username` on macOS or `/home/username` on Linux):
90
91 ```bash theme={null}
92 #!/bin/bash
93 # Read JSON data that Claude Code sends to stdin
94 input=$(cat)
95
96 # Extract fields using jq
97 MODEL=$(echo "$input" | jq -r '.model.display_name')
98 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
99 # The "// 0" provides a fallback if the field is null
100 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
101
102 # Output the status line - ${DIR##*/} extracts just the folder name
103 echo "[$MODEL] 📁 ${DIR##*/} | ${PCT}% context"
104 ```
105 </Step>
106
107 <Step title="Make it executable">
108 Mark the script as executable so your shell can run it:
109
110 ```bash theme={null}
111 chmod +x ~/.claude/statusline.sh
112 ```
113 </Step>
114
115 <Step title="Add to settings">
116 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:
117
118 ```json theme={null}
119 {
120 "statusLine": {
121 "type": "command",
122 "command": "~/.claude/statusline.sh"
123 }
124 }
125 ```
126
127 Your status line appears at the bottom of the interface. Claude Code reloads settings automatically and runs your script as soon as you save the file.
128 </Step>
129</Steps>
81
82
83**1. Create a script that reads JSON and prints output**
84
85Claude Code sends JSON data to your script via stdin. This script uses [`jq`](https://jqlang.org/), a command-line JSON parser you may need to install, to extract the model name, directory, and context percentage, then prints a formatted line.
86
87Save this to `~/.claude/statusline.sh` (where `~` is your home directory, such as `/Users/username` on macOS or `/home/username` on Linux):
88
89```bash theme={null}
90#!/bin/bash
91# Read JSON data that Claude Code sends to stdin
92input=$(cat)
93
94# Extract fields using jq
95MODEL=$(echo "$input" | jq -r '.model.display_name')
96DIR=$(echo "$input" | jq -r '.workspace.current_dir')
97# The "// 0" provides a fallback if the field is null
98PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
99
100# Output the status line - ${DIR##*/} extracts just the folder name
101echo "[$MODEL] 📁 ${DIR##*/} | ${PCT}% context"
102```
103
104**2. Make it executable**
105
106Mark the script as executable so your shell can run it:
107
108```bash theme={null}
109chmod +x ~/.claude/statusline.sh
110```
111
112**3. Add to settings**
113
114Tell 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:
115
116```json theme={null}
117{
118 "statusLine": {
119 "type": "command",
120 "command": "~/.claude/statusline.sh"
121 }
122}
123```
124
125Your status line appears at the bottom of the interface. Claude Code reloads settings automatically and runs your script as soon as you save the file.
130126
131127## How status lines work
132128
133129Claude Code runs your script with [JSON session data](#available-data) on stdin and displays whatever the script prints to stdout.
134130
135<Note>The status line runs locally and does not consume API tokens. It temporarily hides during certain UI interactions, including the help menu and permission prompts.</Note>
131> [!NOTE]
132>
133> The status line runs locally and does not consume API tokens. It temporarily hides during certain UI interactions, including the help menu and permission prompts.
136134
137135### When the status line updates
138136
from line 208
210208| `worktree.original_cwd` | The directory Claude was in before entering the worktree |
211209| `worktree.original_branch` | Git branch checked out before entering the worktree. Absent for hook-based worktrees |
212210
213<Accordion title="Full JSON schema">
214 Your status line command receives this JSON structure via stdin:
215
216 ```json theme={null}
217 {
218 "cwd": "/current/working/directory",
219 "session_id": "abc123...",
220 "session_name": "my-session",
221 "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
222 "transcript_path": "/path/to/transcript.jsonl",
223 "model": {
224 "id": "claude-opus-5-5",
225 "display_name": "Opus"
226 },
227 "workspace": {
228 "current_dir": "/current/working/directory",
229 "project_dir": "/original/project/directory",
230 "added_dirs": [],
231 "git_worktree": "feature-xyz",
232 "repo": {
233 "host": "github.com",
234 "owner": "anthropics",
235 "name": "claude-code"
236 }
237 },
238 "version": "2.1.90",
239 "output_style": {
240 "name": "default"
241 },
242 "cost": {
243 "total_cost_usd": 0.01234,
244 "total_duration_ms": 45000,
245 "total_api_duration_ms": 2300,
246 "total_lines_added": 156,
247 "total_lines_removed": 23
248 },
249 "context_window": {
250 "total_input_tokens": 15500,
251 "total_output_tokens": 1200,
252 "context_window_size": 200000,
253 "used_percentage": 8,
254 "remaining_percentage": 92,
255 "current_usage": {
256 "input_tokens": 8500,
257 "output_tokens": 1200,
258 "cache_creation_input_tokens": 5000,
259 "cache_read_input_tokens": 2000
260 }
261 },
262 "exceeds_200k_tokens": false,
263 "prompt_cache": {
264 "warm": true,
265 "caching_observed": true,
266 "ttl": "1h",
267 "expires_at": 1738429200,
268 "requests": 14,
269 "misses": 2,
270 "expected_rebuilds": 1,
271 "hit_ratio": 0.91,
272 "cache_write_tokens": 352000,
273 "miss_recache_tokens": 310200,
274 "last_miss_at": 1738425230,
275 "last_miss_cause": {
276 "causes": ["tools_changed"],
277 "tools_added": 2,
278 "tools_removed": 0
279 },
280 "miss_causes": {
281 "tools_changed": 2
282 },
283 "recache_tokens_if_cold": 45000
284 },
285 "fast_mode": false,
286 "effort": {
287 "level": "high"
288 },
289 "thinking": {
290 "enabled": true
291 },
292 "rate_limits": {
293 "five_hour": {
294 "used_percentage": 23.5,
295 "resets_at": 1738425600
296 },
297 "seven_day": {
298 "used_percentage": 41.2,
299 "resets_at": 1738857600
300 },
301 "spend_limit": {
302 "used_percentage": 62.8,
303 "resets_at": 1740787200,
304 "used_usd": 314.12,
305 "limit_usd": 500,
306 "period": "monthly"
307 }
308 },
309 "vim": {
310 "mode": "NORMAL"
311 },
312 "agent": {
313 "name": "security-reviewer"
314 },
315 "pr": {
316 "number": 1234,
317 "url": "https://github.com/anthropics/claude-code/pull/1234",
318 "review_state": "pending"
319 },
320 "worktree": {
321 "name": "my-feature",
322 "path": "/path/to/.claude/worktrees/my-feature",
323 "branch": "worktree-my-feature",
324 "original_cwd": "/path/to/project",
325 "original_branch": "main"
326 }
327 }
328 ```
329
330 **Fields that may be absent** (not present in JSON):
331
332 * `session_name`: appears when a custom name has been set with `--name` or `/rename`, or once an AI-generated session title exists. The default display name, such as `my-app-3f`, doesn't populate it
333 * `prompt_id`: appears only after the first user input
334 * `workspace.git_worktree`: appears only when the current directory is inside a linked git worktree
335 * `workspace.repo`: appears only inside a git repository with an `origin` remote configured
336 * `effort`: appears only when the current model supports the reasoning effort parameter
337 * `vim`: appears only when vim mode is enabled
338 * `agent`: appears only when running with the `--agent` flag or agent settings configured
339 * `pr`: appears only while an open PR or GitLab merge request is found for the current branch, and is removed once it merges or closes. `pr.review_state` and `pr.kind` may be independently absent
340 * `worktree`: appears only while the session is in a [worktree session](/docs/en/worktrees). When present, `branch` and `original_branch` may also be absent for hook-based worktrees
341 * `rate_limits`: appears only for claude.ai Pro and Max subscribers, or behind a Claude apps gateway that sets a spend limit for you, and only after the first API response in the session. Each window (`five_hour`, `seven_day`, `spend_limit`) may be independently absent, and Claude Code drops a window once its `resets_at` time passes. Use `jq -r '.rate_limits.five_hour.used_percentage // empty'` to handle absence gracefully.
342 * `prompt_cache`: appears after the main conversation's first API response. See [prompt cache fields](#prompt-cache-fields)
343
344 **Fields that may be `null`**:
345
346 * `context_window.current_usage`: `null` before the first API call in a session, and again after `/compact` until the next API call repopulates it
347 * `context_window.used_percentage`, `context_window.remaining_percentage`: may be `null` early in the session
348
349 Handle missing fields with conditional access and null values with fallback defaults in your scripts.
350</Accordion>
211> **Full JSON schema**
212>
213> Your status line command receives this JSON structure via stdin:
214>
215> ```json theme={null}
216> {
217> "cwd": "/current/working/directory",
218> "session_id": "abc123...",
219> "session_name": "my-session",
220> "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
221> "transcript_path": "/path/to/transcript.jsonl",
222> "model": {
223> "id": "claude-opus-5-5",
224> "display_name": "Opus"
225> },
226> "workspace": {
227> "current_dir": "/current/working/directory",
228> "project_dir": "/original/project/directory",
229> "added_dirs": [],
230> "git_worktree": "feature-xyz",
231> "repo": {
232> "host": "github.com",
233> "owner": "anthropics",
234> "name": "claude-code"
235> }
236> },
237> "version": "2.1.90",
238> "output_style": {
239> "name": "default"
240> },
241> "cost": {
242> "total_cost_usd": 0.01234,
243> "total_duration_ms": 45000,
244> "total_api_duration_ms": 2300,
245> "total_lines_added": 156,
246> "total_lines_removed": 23
247> },
248> "context_window": {
249> "total_input_tokens": 15500,
250> "total_output_tokens": 1200,
251> "context_window_size": 200000,
252> "used_percentage": 8,
253> "remaining_percentage": 92,
254> "current_usage": {
255> "input_tokens": 8500,
256> "output_tokens": 1200,
257> "cache_creation_input_tokens": 5000,
258> "cache_read_input_tokens": 2000
259> }
260> },
261> "exceeds_200k_tokens": false,
262> "prompt_cache": {
263> "warm": true,
264> "caching_observed": true,
265> "ttl": "1h",
266> "expires_at": 1738429200,
267> "requests": 14,
268> "misses": 2,
269> "expected_rebuilds": 1,
270> "hit_ratio": 0.91,
271> "cache_write_tokens": 352000,
272> "miss_recache_tokens": 310200,
273> "last_miss_at": 1738425230,
274> "last_miss_cause": {
275> "causes": ["tools_changed"],
276> "tools_added": 2,
277> "tools_removed": 0
278> },
279> "miss_causes": {
280> "tools_changed": 2
281> },
282> "recache_tokens_if_cold": 45000
283> },
284> "fast_mode": false,
285> "effort": {
286> "level": "high"
287> },
288> "thinking": {
289> "enabled": true
290> },
291> "rate_limits": {
292> "five_hour": {
293> "used_percentage": 23.5,
294> "resets_at": 1738425600
295> },
296> "seven_day": {
297> "used_percentage": 41.2,
298> "resets_at": 1738857600
299> },
300> "spend_limit": {
301> "used_percentage": 62.8,
302> "resets_at": 1740787200,
303> "used_usd": 314.12,
304> "limit_usd": 500,
305> "period": "monthly"
306> }
307> },
308> "vim": {
309> "mode": "NORMAL"
310> },
311> "agent": {
312> "name": "security-reviewer"
313> },
314> "pr": {
315> "number": 1234,
316> "url": "https://github.com/anthropics/claude-code/pull/1234",
317> "review_state": "pending"
318> },
319> "worktree": {
320> "name": "my-feature",
321> "path": "/path/to/.claude/worktrees/my-feature",
322> "branch": "worktree-my-feature",
323> "original_cwd": "/path/to/project",
324> "original_branch": "main"
325> }
326> }
327> ```
328>
329> **Fields that may be absent** (not present in JSON):
330>
331> * `session_name`: appears when a custom name has been set with `--name` or `/rename`, or once an AI-generated session title exists. The default display name, such as `my-app-3f`, doesn't populate it
332> * `prompt_id`: appears only after the first user input
333> * `workspace.git_worktree`: appears only when the current directory is inside a linked git worktree
334> * `workspace.repo`: appears only inside a git repository with an `origin` remote configured
335> * `effort`: appears only when the current model supports the reasoning effort parameter
336> * `vim`: appears only when vim mode is enabled
337> * `agent`: appears only when running with the `--agent` flag or agent settings configured
338> * `pr`: appears only while an open PR or GitLab merge request is found for the current branch, and is removed once it merges or closes. `pr.review_state` and `pr.kind` may be independently absent
339> * `worktree`: appears only while the session is in a [worktree session](/docs/en/worktrees). When present, `branch` and `original_branch` may also be absent for hook-based worktrees
340> * `rate_limits`: appears only for claude.ai Pro and Max subscribers, or behind a Claude apps gateway that sets a spend limit for you, and only after the first API response in the session. Each window (`five_hour`, `seven_day`, `spend_limit`) may be independently absent, and Claude Code drops a window once its `resets_at` time passes. Use `jq -r '.rate_limits.five_hour.used_percentage // empty'` to handle absence gracefully.
341> * `prompt_cache`: appears after the main conversation's first API response. See [prompt cache fields](#prompt-cache-fields)
342>
343> **Fields that may be `null`**:
344>
345> * `context_window.current_usage`: `null` before the first API call in a session, and again after `/compact` until the next API call repopulates it
346> * `context_window.used_percentage`, `context_window.remaining_percentage`: may be `null` early in the session
347>
348> Handle missing fields with conditional access and null values with fallback defaults in your scripts.
351349
352350### Context window fields
353351
from line 405
407405
408406Claude Code shows the same statistics in the terminal, on the [`/usage` command's `Prompt cache (main)` line](/docs/en/costs#prompt-cache-statistics).
409407
410<h4 id="last-miss-cause">
411 Last miss cause
412</h4>
408#### Last miss cause
413409
414410The `last_miss_cause` object reports what Claude Code identified as the likely cause of the most recent miss. Its `causes` array holds one or more cause names, such as `tools_changed`, `system_prompt_changed`, `ttl_expired_5m`, or `likely_server_side`. The object is `null` until the session's first miss, and again whenever Claude Code couldn't identify a cause for the most recent miss. Requires Claude Code v2.1.260 or later.
415411
from line 428
432428
433429Display 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:
434430
435<Frame>
436 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-context-window-usage.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=f3918a549912dc47e90f2b69e68bc847" alt="A status line showing model name and a progress bar with percentage" width="1224" height="224" data-path="images/statusline-context-window-usage.png" />
437</Frame>
438
439<CodeGroup>
440 ```bash Bash theme={null}
441 #!/bin/bash
442 # Read all of stdin into a variable
443 input=$(cat)
444
445 # Extract fields with jq, "// 0" provides fallback for null
446 MODEL=$(echo "$input" | jq -r '.model.display_name')
447 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
448
449 # Build progress bar: printf -v creates a run of spaces, then
450 # ${var// /▓} replaces each space with a block character
451 BAR_WIDTH=10
452 FILLED=$((PCT * BAR_WIDTH / 100))
453 EMPTY=$((BAR_WIDTH - FILLED))
454 BAR=""
455 [ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /▓}"
456 [ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"
457
458 echo "[$MODEL] $BAR $PCT%"
459 ```
460
461 ```python Python theme={null}
462 #!/usr/bin/env python3
463 import json, sys
464
465 # json.load reads and parses stdin in one step
466 data = json.load(sys.stdin)
467 model = data['model']['display_name']
468 # "or 0" handles null values
469 pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
470
471 # String multiplication builds the bar
472 filled = pct * 10 // 100
473 bar = '▓' * filled + '░' * (10 - filled)
474
475 print(f"[{model}] {bar} {pct}%")
476 ```
477
478 ```javascript Node.js theme={null}
479 #!/usr/bin/env node
480 // Node.js reads stdin asynchronously with events
481 let input = '';
482 process.stdin.on('data', chunk => input += chunk);
483 process.stdin.on('end', () => {
484 const data = JSON.parse(input);
485 const model = data.model.display_name;
486 // Optional chaining (?.) safely handles null fields
487 const pct = Math.floor(data.context_window?.used_percentage || 0);
488
489 // String.repeat() builds the bar
490 const filled = Math.floor(pct * 10 / 100);
491 const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled);
492
493 console.log(`[${model}] ${bar} ${pct}%`);
494 });
495 ```
496</CodeGroup>
431
432
433```bash Bash theme={null}
434#!/bin/bash
435# Read all of stdin into a variable
436input=$(cat)
437
438# Extract fields with jq, "// 0" provides fallback for null
439MODEL=$(echo "$input" | jq -r '.model.display_name')
440PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
441
442# Build progress bar: printf -v creates a run of spaces, then
443# ${var// /▓} replaces each space with a block character
444BAR_WIDTH=10
445FILLED=$((PCT * BAR_WIDTH / 100))
446EMPTY=$((BAR_WIDTH - FILLED))
447BAR=""
448[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /▓}"
449[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"
450
451echo "[$MODEL] $BAR $PCT%"
452```
453
454```python Python theme={null}
455#!/usr/bin/env python3
456import json, sys
457
458# json.load reads and parses stdin in one step
459data = json.load(sys.stdin)
460model = data['model']['display_name']
461# "or 0" handles null values
462pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
463
464# String multiplication builds the bar
465filled = pct * 10 // 100
466bar = '▓' * filled + '░' * (10 - filled)
467
468print(f"[{model}] {bar} {pct}%")
469```
470
471```javascript Node.js theme={null}
472#!/usr/bin/env node
473// Node.js reads stdin asynchronously with events
474let input = '';
475process.stdin.on('data', chunk => input += chunk);
476process.stdin.on('end', () => {
477 const data = JSON.parse(input);
478 const model = data.model.display_name;
479 // Optional chaining (?.) safely handles null fields
480 const pct = Math.floor(data.context_window?.used_percentage || 0);
481
482 // String.repeat() builds the bar
483 const filled = Math.floor(pct * 10 / 100);
484 const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled);
485
486 console.log(`[${model}] ${bar} ${pct}%`);
487});
488```
497489
498490### Git status with colors
499491
500492Show 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.
501493
502<Frame>
503 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-git-context.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=f13c190724d9ec7188c17cd2f98b7bf4" alt="A status line showing model, directory, git branch, and colored indicators for staged and modified files" width="1224" height="224" data-path="images/statusline-git-context.png" />
504</Frame>
494
505495
506496Each script checks if the current directory is a git repository, counts staged and modified files, and displays color-coded indicators:
507497
508<CodeGroup>
509 ```bash Bash theme={null}
510 #!/bin/bash
511 input=$(cat)
512
513 MODEL=$(echo "$input" | jq -r '.model.display_name')
514 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
515
516 GREEN='\033[32m'
517 YELLOW='\033[33m'
518 RESET='\033[0m'
519
520 if git rev-parse --git-dir > /dev/null 2>&1; then
521 BRANCH=$(git branch --show-current 2>/dev/null)
522 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
523 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
524
525 GIT_STATUS=""
526 [ "$STAGED" -gt 0 ] && GIT_STATUS="${GREEN}+${STAGED}${RESET}"
527 [ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${YELLOW}~${MODIFIED}${RESET}"
528
529 echo -e "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH $GIT_STATUS"
530 else
531 echo "[$MODEL] 📁 ${DIR##*/}"
532 fi
533 ```
534
535 ```python Python theme={null}
536 #!/usr/bin/env python3
537 import json, sys, subprocess, os
538
539 data = json.load(sys.stdin)
540 model = data['model']['display_name']
541 directory = os.path.basename(data['workspace']['current_dir'])
542
543 GREEN, YELLOW, RESET = '\033[32m', '\033[33m', '\033[0m'
544
545 try:
546 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
547 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
548 staged_output = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
549 modified_output = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
550 staged = len(staged_output.split('\n')) if staged_output else 0
551 modified = len(modified_output.split('\n')) if modified_output else 0
552
553 git_status = f"{GREEN}+{staged}{RESET}" if staged else ""
554 git_status += f"{YELLOW}~{modified}{RESET}" if modified else ""
555
556 print(f"[{model}] 📁 {directory} | 🌿 {branch} {git_status}")
557 except:
558 print(f"[{model}] 📁 {directory}")
559 ```
560
561 ```javascript Node.js theme={null}
562 #!/usr/bin/env node
563 const { execSync } = require('child_process');
564 const path = require('path');
565
566 let input = '';
567 process.stdin.on('data', chunk => input += chunk);
568 process.stdin.on('end', () => {
569 const data = JSON.parse(input);
570 const model = data.model.display_name;
571 const dir = path.basename(data.workspace.current_dir);
572
573 const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RESET = '\x1b[0m';
574
575 try {
576 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
577 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
578 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
579 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
580
581 let gitStatus = staged ? `${GREEN}+${staged}${RESET}` : '';
582 gitStatus += modified ? `${YELLOW}~${modified}${RESET}` : '';
583
584 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} ${gitStatus}`);
585 } catch {
586 console.log(`[${model}] 📁 ${dir}`);
587 }
588 });
589 ```
590</CodeGroup>
498```bash Bash theme={null}
499#!/bin/bash
500input=$(cat)
501
502MODEL=$(echo "$input" | jq -r '.model.display_name')
503DIR=$(echo "$input" | jq -r '.workspace.current_dir')
504
505GREEN='\033[32m'
506YELLOW='\033[33m'
507RESET='\033[0m'
508
509if git rev-parse --git-dir > /dev/null 2>&1; then
510 BRANCH=$(git branch --show-current 2>/dev/null)
511 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
512 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
513
514 GIT_STATUS=""
515 [ "$STAGED" -gt 0 ] && GIT_STATUS="${GREEN}+${STAGED}${RESET}"
516 [ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${YELLOW}~${MODIFIED}${RESET}"
517
518 echo -e "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH $GIT_STATUS"
519else
520 echo "[$MODEL] 📁 ${DIR##*/}"
521fi
522```
523
524```python Python theme={null}
525#!/usr/bin/env python3
526import json, sys, subprocess, os
527
528data = json.load(sys.stdin)
529model = data['model']['display_name']
530directory = os.path.basename(data['workspace']['current_dir'])
531
532GREEN, YELLOW, RESET = '\033[32m', '\033[33m', '\033[0m'
533
534try:
535 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
536 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
537 staged_output = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
538 modified_output = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
539 staged = len(staged_output.split('\n')) if staged_output else 0
540 modified = len(modified_output.split('\n')) if modified_output else 0
541
542 git_status = f"{GREEN}+{staged}{RESET}" if staged else ""
543 git_status += f"{YELLOW}~{modified}{RESET}" if modified else ""
544
545 print(f"[{model}] 📁 {directory} | 🌿 {branch} {git_status}")
546except:
547 print(f"[{model}] 📁 {directory}")
548```
549
550```javascript Node.js theme={null}
551#!/usr/bin/env node
552const { execSync } = require('child_process');
553const path = require('path');
554
555let input = '';
556process.stdin.on('data', chunk => input += chunk);
557process.stdin.on('end', () => {
558 const data = JSON.parse(input);
559 const model = data.model.display_name;
560 const dir = path.basename(data.workspace.current_dir);
561
562 const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RESET = '\x1b[0m';
563
564 try {
565 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
566 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
567 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
568 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
569
570 let gitStatus = staged ? `${GREEN}+${staged}${RESET}` : '';
571 gitStatus += modified ? `${YELLOW}~${modified}${RESET}` : '';
572
573 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} ${gitStatus}`);
574 } catch {
575 console.log(`[${model}] 📁 ${dir}`);
576 }
577});
578```
591579
592580### Cost and duration tracking
593581
from line 583
595583
596584Each script formats cost as currency and converts milliseconds to minutes and seconds:
597585
598<Frame>
599 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-cost-tracking.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=925f7024c3b38be0f0eca63564bfb52f" alt="A status line showing model name, session cost, and duration" width="1224" height="224" data-path="images/statusline-cost-tracking.png" />
600</Frame>
601
602<CodeGroup>
603 ```bash Bash theme={null}
604 #!/bin/bash
605 input=$(cat)
606
607 MODEL=$(echo "$input" | jq -r '.model.display_name')
608 COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
609 DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
610
611 COST_FMT=$(printf '$%.2f' "$COST")
612 DURATION_SEC=$((DURATION_MS / 1000))
613 MINS=$((DURATION_SEC / 60))
614 SECS=$((DURATION_SEC % 60))
615
616 echo "[$MODEL] 💰 $COST_FMT | ⏱️ ${MINS}m ${SECS}s"
617 ```
618
619 ```python Python theme={null}
620 #!/usr/bin/env python3
621 import json, sys
622
623 data = json.load(sys.stdin)
624 model = data['model']['display_name']
625 cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
626 duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
627
628 duration_sec = duration_ms // 1000
629 mins, secs = duration_sec // 60, duration_sec % 60
630
631 print(f"[{model}] 💰 ${cost:.2f} | ⏱️ {mins}m {secs}s")
632 ```
633
634 ```javascript Node.js theme={null}
635 #!/usr/bin/env node
636 let input = '';
637 process.stdin.on('data', chunk => input += chunk);
638 process.stdin.on('end', () => {
639 const data = JSON.parse(input);
640 const model = data.model.display_name;
641 const cost = data.cost?.total_cost_usd || 0;
642 const durationMs = data.cost?.total_duration_ms || 0;
643
644 const durationSec = Math.floor(durationMs / 1000);
645 const mins = Math.floor(durationSec / 60);
646 const secs = durationSec % 60;
647
648 console.log(`[${model}] 💰 $${cost.toFixed(2)} | ⏱️ ${mins}m ${secs}s`);
649 });
650 ```
651</CodeGroup>
586
587
588```bash Bash theme={null}
589#!/bin/bash
590input=$(cat)
591
592MODEL=$(echo "$input" | jq -r '.model.display_name')
593COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
594DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
595
596COST_FMT=$(printf '$%.2f' "$COST")
597DURATION_SEC=$((DURATION_MS / 1000))
598MINS=$((DURATION_SEC / 60))
599SECS=$((DURATION_SEC % 60))
600
601echo "[$MODEL] 💰 $COST_FMT | ⏱️ ${MINS}m ${SECS}s"
602```
603
604```python Python theme={null}
605#!/usr/bin/env python3
606import json, sys
607
608data = json.load(sys.stdin)
609model = data['model']['display_name']
610cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
611duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
612
613duration_sec = duration_ms // 1000
614mins, secs = duration_sec // 60, duration_sec % 60
615
616print(f"[{model}] 💰 ${cost:.2f} | ⏱️ {mins}m {secs}s")
617```
618
619```javascript Node.js theme={null}
620#!/usr/bin/env node
621let input = '';
622process.stdin.on('data', chunk => input += chunk);
623process.stdin.on('end', () => {
624 const data = JSON.parse(input);
625 const model = data.model.display_name;
626 const cost = data.cost?.total_cost_usd || 0;
627 const durationMs = data.cost?.total_duration_ms || 0;
628
629 const durationSec = Math.floor(durationMs / 1000);
630 const mins = Math.floor(durationSec / 60);
631 const secs = durationSec % 60;
632
633 console.log(`[${model}] 💰 $${cost.toFixed(2)} | ⏱️ ${mins}m ${secs}s`);
634});
635```
652636
653637### Display multiple lines
654638
655639Your script can output multiple lines to create a richer display.
656640
657<Frame>
658 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-multiline.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=a9d0a2fe8e446d80b1abc46da3f93270" 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" width="1224" height="262" data-path="images/statusline-multiline.png" />
659</Frame>
641
660642
661643This 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:
662644
663<CodeGroup>
664 ```bash Bash theme={null}
665 #!/bin/bash
666 input=$(cat)
667
668 MODEL=$(echo "$input" | jq -r '.model.display_name')
669 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
670 COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
671 PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
672 DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
673
674 CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'
675
676 # Pick bar color based on context usage
677 if [ "$PCT" -ge 90 ]; then BAR_COLOR="$RED"
678 elif [ "$PCT" -ge 70 ]; then BAR_COLOR="$YELLOW"
679 else BAR_COLOR="$GREEN"; fi
680
681 FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
682 printf -v FILL "%${FILLED}s"; printf -v PAD "%${EMPTY}s"
683 BAR="${FILL// /█}${PAD// /░}"
684
685 MINS=$((DURATION_MS / 60000)); SECS=$(((DURATION_MS % 60000) / 1000))
686
687 BRANCH=""
688 git rev-parse --git-dir > /dev/null 2>&1 && BRANCH=" | 🌿 $(git branch --show-current 2>/dev/null)"
689
690 echo -e "${CYAN}[$MODEL]${RESET} 📁 ${DIR##*/}$BRANCH"
691 COST_FMT=$(printf '$%.2f' "$COST")
692 echo -e "${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${YELLOW}${COST_FMT}${RESET} | ⏱️ ${MINS}m ${SECS}s"
693 ```
694
695 ```python Python theme={null}
696 #!/usr/bin/env python3
697 import json, sys, subprocess, os
698
699 data = json.load(sys.stdin)
700 model = data['model']['display_name']
701 directory = os.path.basename(data['workspace']['current_dir'])
702 cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
703 pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
704 duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
705
706 CYAN, GREEN, YELLOW, RED, RESET = '\033[36m', '\033[32m', '\033[33m', '\033[31m', '\033[0m'
707
708 bar_color = RED if pct >= 90 else YELLOW if pct >= 70 else GREEN
709 filled = pct // 10
710 bar = '█' * filled + '░' * (10 - filled)
711
712 mins, secs = duration_ms // 60000, (duration_ms % 60000) // 1000
713
714 try:
715 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True, stderr=subprocess.DEVNULL).strip()
716 branch = f" | 🌿 {branch}" if branch else ""
717 except:
718 branch = ""
719
720 print(f"{CYAN}[{model}]{RESET} 📁 {directory}{branch}")
721 print(f"{bar_color}{bar}{RESET} {pct}% | {YELLOW}${cost:.2f}{RESET} | ⏱️ {mins}m {secs}s")
722 ```
723
724 ```javascript Node.js theme={null}
725 #!/usr/bin/env node
726 const { execSync } = require('child_process');
727 const path = require('path');
728
729 let input = '';
730 process.stdin.on('data', chunk => input += chunk);
731 process.stdin.on('end', () => {
732 const data = JSON.parse(input);
733 const model = data.model.display_name;
734 const dir = path.basename(data.workspace.current_dir);
735 const cost = data.cost?.total_cost_usd || 0;
736 const pct = Math.floor(data.context_window?.used_percentage || 0);
737 const durationMs = data.cost?.total_duration_ms || 0;
738
739 const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';
740
741 const barColor = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
742 const filled = Math.floor(pct / 10);
743 const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
744
745 const mins = Math.floor(durationMs / 60000);
746 const secs = Math.floor((durationMs % 60000) / 1000);
747
748 let branch = '';
749 try {
750 branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
751 branch = branch ? ` | 🌿 ${branch}` : '';
752 } catch {}
753
754 console.log(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}`);
755 console.log(`${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s`);
756 });
757 ```
758</CodeGroup>
645```bash Bash theme={null}
646#!/bin/bash
647input=$(cat)
648
649MODEL=$(echo "$input" | jq -r '.model.display_name')
650DIR=$(echo "$input" | jq -r '.workspace.current_dir')
651COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
652PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
653DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
654
655CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'
656
657# Pick bar color based on context usage
658if [ "$PCT" -ge 90 ]; then BAR_COLOR="$RED"
659elif [ "$PCT" -ge 70 ]; then BAR_COLOR="$YELLOW"
660else BAR_COLOR="$GREEN"; fi
661
662FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
663printf -v FILL "%${FILLED}s"; printf -v PAD "%${EMPTY}s"
664BAR="${FILL// /█}${PAD// /░}"
665
666MINS=$((DURATION_MS / 60000)); SECS=$(((DURATION_MS % 60000) / 1000))
667
668BRANCH=""
669git rev-parse --git-dir > /dev/null 2>&1 && BRANCH=" | 🌿 $(git branch --show-current 2>/dev/null)"
670
671echo -e "${CYAN}[$MODEL]${RESET} 📁 ${DIR##*/}$BRANCH"
672COST_FMT=$(printf '$%.2f' "$COST")
673echo -e "${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${YELLOW}${COST_FMT}${RESET} | ⏱️ ${MINS}m ${SECS}s"
674```
675
676```python Python theme={null}
677#!/usr/bin/env python3
678import json, sys, subprocess, os
679
680data = json.load(sys.stdin)
681model = data['model']['display_name']
682directory = os.path.basename(data['workspace']['current_dir'])
683cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
684pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
685duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0
686
687CYAN, GREEN, YELLOW, RED, RESET = '\033[36m', '\033[32m', '\033[33m', '\033[31m', '\033[0m'
688
689bar_color = RED if pct >= 90 else YELLOW if pct >= 70 else GREEN
690filled = pct // 10
691bar = '█' * filled + '░' * (10 - filled)
692
693mins, secs = duration_ms // 60000, (duration_ms % 60000) // 1000
694
695try:
696 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True, stderr=subprocess.DEVNULL).strip()
697 branch = f" | 🌿 {branch}" if branch else ""
698except:
699 branch = ""
700
701print(f"{CYAN}[{model}]{RESET} 📁 {directory}{branch}")
702print(f"{bar_color}{bar}{RESET} {pct}% | {YELLOW}${cost:.2f}{RESET} | ⏱️ {mins}m {secs}s")
703```
704
705```javascript Node.js theme={null}
706#!/usr/bin/env node
707const { execSync } = require('child_process');
708const path = require('path');
709
710let input = '';
711process.stdin.on('data', chunk => input += chunk);
712process.stdin.on('end', () => {
713 const data = JSON.parse(input);
714 const model = data.model.display_name;
715 const dir = path.basename(data.workspace.current_dir);
716 const cost = data.cost?.total_cost_usd || 0;
717 const pct = Math.floor(data.context_window?.used_percentage || 0);
718 const durationMs = data.cost?.total_duration_ms || 0;
719
720 const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';
721
722 const barColor = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
723 const filled = Math.floor(pct / 10);
724 const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
725
726 const mins = Math.floor(durationMs / 60000);
727 const secs = Math.floor((durationMs % 60000) / 1000);
728
729 let branch = '';
730 try {
731 branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
732 branch = branch ? ` | 🌿 ${branch}` : '';
733 } catch {}
734
735 console.log(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}`);
736 console.log(`${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s`);
737});
738```
759739
760740### Clickable links
761741
762742This example creates a clickable link to your GitHub repository. Hold Cmd (macOS) or Ctrl (Windows/Linux) and click to open the link in your browser.
763743
764<Frame>
765 <img src="https://mintcdn.com/claude-code/HDAmBwgbrZVk0pOt/images/statusline-links.png?fit=max&auto=format&n=HDAmBwgbrZVk0pOt&q=85&s=4778a144a28cb498c99d5fa018bb374a" alt="A status line showing a clickable link to a GitHub repository" width="1224" height="224" data-path="images/statusline-links.png" />
766</Frame>
744
767745
768746Each 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:
769747
770<CodeGroup>
771 ```bash Bash theme={null}
772 #!/bin/bash
773 input=$(cat)
774
775 MODEL=$(echo "$input" | jq -r '.model.display_name')
776
777 # Convert git SSH URL to HTTPS
778 REMOTE=$(git remote get-url origin 2>/dev/null | sed 's/[email protected]:/https:\/\/github.com\//' | sed 's/\.git$//')
779
780 if [ -n "$REMOTE" ]; then
781 REPO_NAME=$(basename "$REMOTE")
782 # OSC 8 format: \e]8;;URL\a then TEXT then \e]8;;\a
783 # printf %b interprets escape sequences reliably across shells
784 printf '%b' "[$MODEL] 🔗 \e]8;;${REMOTE}\a${REPO_NAME}\e]8;;\a\n"
785 else
786 echo "[$MODEL]"
787 fi
788 ```
789
790 ```python Python theme={null}
791 #!/usr/bin/env python3
792 import json, sys, subprocess, re, os
793
794 data = json.load(sys.stdin)
795 model = data['model']['display_name']
796
797 # Get git remote URL
798 try:
799 remote = subprocess.check_output(
800 ['git', 'remote', 'get-url', 'origin'],
801 stderr=subprocess.DEVNULL, text=True
802 ).strip()
803 # Convert SSH to HTTPS format
804 remote = re.sub(r'^git@github\.com:', 'https://github.com/', remote)
805 remote = re.sub(r'\.git$', '', remote)
806 repo_name = os.path.basename(remote)
807 # OSC 8 escape sequences
808 link = f"\033]8;;{remote}\a{repo_name}\033]8;;\a"
809 print(f"[{model}] 🔗 {link}")
810 except:
811 print(f"[{model}]")
812 ```
813
814 ```javascript Node.js theme={null}
815 #!/usr/bin/env node
816 const { execSync } = require('child_process');
817 const path = require('path');
818
819 let input = '';
820 process.stdin.on('data', chunk => input += chunk);
821 process.stdin.on('end', () => {
822 const data = JSON.parse(input);
823 const model = data.model.display_name;
824
825 try {
826 let remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
827 // Convert SSH to HTTPS format
828 remote = remote.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
829 const repoName = path.basename(remote);
830 // OSC 8 escape sequences
831 const link = `\x1b]8;;${remote}\x07${repoName}\x1b]8;;\x07`;
832 console.log(`[${model}] 🔗 ${link}`);
833 } catch {
834 console.log(`[${model}]`);
835 }
836 });
837 ```
838</CodeGroup>
748```bash Bash theme={null}
749#!/bin/bash
750input=$(cat)
751
752MODEL=$(echo "$input" | jq -r '.model.display_name')
753
754# Convert git SSH URL to HTTPS
755REMOTE=$(git remote get-url origin 2>/dev/null | sed 's/[email protected]:/https:\/\/github.com\//' | sed 's/\.git$//')
756
757if [ -n "$REMOTE" ]; then
758 REPO_NAME=$(basename "$REMOTE")
759 # OSC 8 format: \e]8;;URL\a then TEXT then \e]8;;\a
760 # printf %b interprets escape sequences reliably across shells
761 printf '%b' "[$MODEL] 🔗 \e]8;;${REMOTE}\a${REPO_NAME}\e]8;;\a\n"
762else
763 echo "[$MODEL]"
764fi
765```
766
767```python Python theme={null}
768#!/usr/bin/env python3
769import json, sys, subprocess, re, os
770
771data = json.load(sys.stdin)
772model = data['model']['display_name']
773
774# Get git remote URL
775try:
776 remote = subprocess.check_output(
777 ['git', 'remote', 'get-url', 'origin'],
778 stderr=subprocess.DEVNULL, text=True
779 ).strip()
780 # Convert SSH to HTTPS format
781 remote = re.sub(r'^git@github\.com:', 'https://github.com/', remote)
782 remote = re.sub(r'\.git$', '', remote)
783 repo_name = os.path.basename(remote)
784 # OSC 8 escape sequences
785 link = f"\033]8;;{remote}\a{repo_name}\033]8;;\a"
786 print(f"[{model}] 🔗 {link}")
787except:
788 print(f"[{model}]")
789```
790
791```javascript Node.js theme={null}
792#!/usr/bin/env node
793const { execSync } = require('child_process');
794const path = require('path');
795
796let input = '';
797process.stdin.on('data', chunk => input += chunk);
798process.stdin.on('end', () => {
799 const data = JSON.parse(input);
800 const model = data.model.display_name;
801
802 try {
803 let remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
804 // Convert SSH to HTTPS format
805 remote = remote.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
806 const repoName = path.basename(remote);
807 // OSC 8 escape sequences
808 const link = `\x1b]8;;${remote}\x07${repoName}\x1b]8;;\x07`;
809 console.log(`[${model}] 🔗 ${link}`);
810 } catch {
811 console.log(`[${model}]`);
812 }
813});
814```
839815
840816### Rate limit usage
841817
from line 819
843819
844820The `rate_limits` object is only present for claude.ai Pro and Max subscribers, or behind a Claude apps gateway with spend limits, and only after the first API response. Each script handles the absent fields gracefully, and behind a gateway prints `spend: $314.12 / $500`, or `spend: 63%` while the dollar fields are absent:
845821
846<CodeGroup>
847 ```bash Bash theme={null}
848 #!/bin/bash
849 input=$(cat)
850
851 MODEL=$(echo "$input" | jq -r '.model.display_name')
852 # "// empty" produces no output when rate_limits is absent
853 FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
854 WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
855
856 LIMITS=""
857 [ -n "$FIVE_H" ] && LIMITS="5h: $(printf '%.0f' "$FIVE_H")%"
858 [ -n "$WEEK" ] && LIMITS="${LIMITS:+$LIMITS }7d: $(printf '%.0f' "$WEEK")%"
859
860 # Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
861 SPEND_PCT=$(echo "$input" | jq -r '.rate_limits.spend_limit.used_percentage // empty')
862 SPEND_USD=$(echo "$input" | jq -r '.rate_limits.spend_limit | select(.used_usd != null) | "$\(.used_usd) / $\(.limit_usd)"')
863 [ -n "$SPEND_PCT" ] && LIMITS="${LIMITS:+$LIMITS }spend: ${SPEND_USD:-$(printf '%.0f' "$SPEND_PCT")%}"
864
865 [ -n "$LIMITS" ] && echo "[$MODEL] | $LIMITS" || echo "[$MODEL]"
866 ```
867
868 ```python Python theme={null}
869 #!/usr/bin/env python3
870 import json, sys
871
872 data = json.load(sys.stdin)
873 model = data['model']['display_name']
874
875 parts = []
876 rate = data.get('rate_limits', {})
877 five_h = rate.get('five_hour', {}).get('used_percentage')
878 week = rate.get('seven_day', {}).get('used_percentage')
879
880 if five_h is not None:
881 parts.append(f"5h: {five_h:.0f}%")
882 if week is not None:
883 parts.append(f"7d: {week:.0f}%")
884
885 # Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
886 spend = rate.get('spend_limit', {})
887 if spend.get('used_percentage') is not None:
888 if spend.get('used_usd') is not None:
889 parts.append(f"spend: ${spend['used_usd']} / ${spend['limit_usd']}")
890 else:
891 parts.append(f"spend: {spend['used_percentage']:.0f}%")
892
893 if parts:
894 print(f"[{model}] | {' '.join(parts)}")
895 else:
896 print(f"[{model}]")
897 ```
898
899 ```javascript Node.js theme={null}
900 #!/usr/bin/env node
901 let input = '';
902 process.stdin.on('data', chunk => input += chunk);
903 process.stdin.on('end', () => {
904 const data = JSON.parse(input);
905 const model = data.model.display_name;
906
907 const parts = [];
908 const fiveH = data.rate_limits?.five_hour?.used_percentage;
909 const week = data.rate_limits?.seven_day?.used_percentage;
910
911 if (fiveH != null) parts.push(`5h: ${Math.round(fiveH)}%`);
912 if (week != null) parts.push(`7d: ${Math.round(week)}%`);
913
914 // Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
915 const spend = data.rate_limits?.spend_limit;
916 if (spend?.used_percentage != null) {
917 parts.push(spend.used_usd != null
918 ? `spend: $${spend.used_usd} / $${spend.limit_usd}`
919 : `spend: ${Math.round(spend.used_percentage)}%`);
920 }
921
922 console.log(parts.length ? `[${model}] | ${parts.join(' ')}` : `[${model}]`);
923 });
924 ```
925</CodeGroup>
822```bash Bash theme={null}
823#!/bin/bash
824input=$(cat)
825
826MODEL=$(echo "$input" | jq -r '.model.display_name')
827# "// empty" produces no output when rate_limits is absent
828FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
829WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
830
831LIMITS=""
832[ -n "$FIVE_H" ] && LIMITS="5h: $(printf '%.0f' "$FIVE_H")%"
833[ -n "$WEEK" ] && LIMITS="${LIMITS:+$LIMITS }7d: $(printf '%.0f' "$WEEK")%"
834
835# Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
836SPEND_PCT=$(echo "$input" | jq -r '.rate_limits.spend_limit.used_percentage // empty')
837SPEND_USD=$(echo "$input" | jq -r '.rate_limits.spend_limit | select(.used_usd != null) | "$\(.used_usd) / $\(.limit_usd)"')
838[ -n "$SPEND_PCT" ] && LIMITS="${LIMITS:+$LIMITS }spend: ${SPEND_USD:-$(printf '%.0f' "$SPEND_PCT")%}"
839
840[ -n "$LIMITS" ] && echo "[$MODEL] | $LIMITS" || echo "[$MODEL]"
841```
842
843```python Python theme={null}
844#!/usr/bin/env python3
845import json, sys
846
847data = json.load(sys.stdin)
848model = data['model']['display_name']
849
850parts = []
851rate = data.get('rate_limits', {})
852five_h = rate.get('five_hour', {}).get('used_percentage')
853week = rate.get('seven_day', {}).get('used_percentage')
854
855if five_h is not None:
856 parts.append(f"5h: {five_h:.0f}%")
857if week is not None:
858 parts.append(f"7d: {week:.0f}%")
859
860# Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
861spend = rate.get('spend_limit', {})
862if spend.get('used_percentage') is not None:
863 if spend.get('used_usd') is not None:
864 parts.append(f"spend: ${spend['used_usd']} / ${spend['limit_usd']}")
865 else:
866 parts.append(f"spend: {spend['used_percentage']:.0f}%")
867
868if parts:
869 print(f"[{model}] | {' '.join(parts)}")
870else:
871 print(f"[{model}]")
872```
873
874```javascript Node.js theme={null}
875#!/usr/bin/env node
876let input = '';
877process.stdin.on('data', chunk => input += chunk);
878process.stdin.on('end', () => {
879 const data = JSON.parse(input);
880 const model = data.model.display_name;
881
882 const parts = [];
883 const fiveH = data.rate_limits?.five_hour?.used_percentage;
884 const week = data.rate_limits?.seven_day?.used_percentage;
885
886 if (fiveH != null) parts.push(`5h: ${Math.round(fiveH)}%`);
887 if (week != null) parts.push(`7d: ${Math.round(week)}%`);
888
889 // Behind a Claude apps gateway: dollars when the gateway reports them, else the percentage
890 const spend = data.rate_limits?.spend_limit;
891 if (spend?.used_percentage != null) {
892 parts.push(spend.used_usd != null
893 ? `spend: $${spend.used_usd} / $${spend.limit_usd}`
894 : `spend: ${Math.round(spend.used_percentage)}%`);
895 }
896
897 console.log(parts.length ? `[${model}] | ${parts.join(' ')}` : `[${model}]`);
898});
899```
926900
927901### Cache expensive operations
928902
from line 906
932906
933907Each script checks if the cache file is missing or older than 5 seconds before running git commands:
934908
935<CodeGroup>
936 ```bash Bash theme={null}
937 #!/bin/bash
938 input=$(cat)
939
940 MODEL=$(echo "$input" | jq -r '.model.display_name')
941 DIR=$(echo "$input" | jq -r '.workspace.current_dir')
942 SESSION_ID=$(echo "$input" | jq -r '.session_id')
943
944 CACHE_FILE="/tmp/statusline-git-cache-$SESSION_ID"
945 CACHE_MAX_AGE=5 # seconds
946
947 cache_is_stale() {
948 [ ! -f "$CACHE_FILE" ] || \
949 # stat -c %Y (Linux) or stat -f %m (macOS) prints the file's last-modified
950 # time. The Linux form must run first: on Linux, the macOS form prints a
951 # filesystem report to stdout before failing, and that output would be
952 # captured by the command substitution and break the arithmetic.
953 [ $(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
909```bash Bash theme={null}
910#!/bin/bash
911input=$(cat)
912
913MODEL=$(echo "$input" | jq -r '.model.display_name')
914DIR=$(echo "$input" | jq -r '.workspace.current_dir')
915SESSION_ID=$(echo "$input" | jq -r '.session_id')
916
917CACHE_FILE="/tmp/statusline-git-cache-$SESSION_ID"
918CACHE_MAX_AGE=5 # seconds
919
920cache_is_stale() {
921 [ ! -f "$CACHE_FILE" ] || \
922 # stat -c %Y (Linux) or stat -f %m (macOS) prints the file's last-modified
923 # time. The Linux form must run first: on Linux, the macOS form prints a
924 # filesystem report to stdout before failing, and that output would be
925 # captured by the command substitution and break the arithmetic.
926 [ $(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
927}
928
929if cache_is_stale; then
930 if git rev-parse --git-dir > /dev/null 2>&1; then
931 BRANCH=$(git branch --show-current 2>/dev/null)
932 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
933 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
934 echo "$BRANCH|$STAGED|$MODIFIED" > "$CACHE_FILE"
935 else
936 echo "||" > "$CACHE_FILE"
937 fi
938fi
939
940IFS='|' read -r BRANCH STAGED MODIFIED < "$CACHE_FILE"
941
942if [ -n "$BRANCH" ]; then
943 echo "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH +$STAGED ~$MODIFIED"
944else
945 echo "[$MODEL] 📁 ${DIR##*/}"
946fi
947```
948
949```python Python theme={null}
950#!/usr/bin/env python3
951import json, sys, subprocess, os, time
952
953data = json.load(sys.stdin)
954model = data['model']['display_name']
955directory = os.path.basename(data['workspace']['current_dir'])
956session_id = data['session_id']
957
958CACHE_FILE = f"/tmp/statusline-git-cache-{session_id}"
959CACHE_MAX_AGE = 5 # seconds
960
961def cache_is_stale():
962 if not os.path.exists(CACHE_FILE):
963 return True
964 return time.time() - os.path.getmtime(CACHE_FILE) > CACHE_MAX_AGE
965
966if cache_is_stale():
967 try:
968 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
969 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
970 staged = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
971 modified = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
972 staged_count = len(staged.split('\n')) if staged else 0
973 modified_count = len(modified.split('\n')) if modified else 0
974 with open(CACHE_FILE, 'w') as f:
975 f.write(f"{branch}|{staged_count}|{modified_count}")
976 except:
977 with open(CACHE_FILE, 'w') as f:
978 f.write("||")
979
980with open(CACHE_FILE) as f:
981 branch, staged, modified = f.read().strip().split('|')
982
983if branch:
984 print(f"[{model}] 📁 {directory} | 🌿 {branch} +{staged} ~{modified}")
985else:
986 print(f"[{model}] 📁 {directory}")
987```
988
989```javascript Node.js theme={null}
990#!/usr/bin/env node
991const { execSync } = require('child_process');
992const fs = require('fs');
993const path = require('path');
994
995let input = '';
996process.stdin.on('data', chunk => input += chunk);
997process.stdin.on('end', () => {
998 const data = JSON.parse(input);
999 const model = data.model.display_name;
1000 const dir = path.basename(data.workspace.current_dir);
1001 const sessionId = data.session_id;
1002
1003 const CACHE_FILE = `/tmp/statusline-git-cache-${sessionId}`;
1004 const CACHE_MAX_AGE = 5; // seconds
1005
1006 const cacheIsStale = () => {
1007 if (!fs.existsSync(CACHE_FILE)) return true;
1008 return (Date.now() / 1000) - fs.statSync(CACHE_FILE).mtimeMs / 1000 > CACHE_MAX_AGE;
1009 };
1010
1011 if (cacheIsStale()) {
1012 try {
1013 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
1014 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
1015 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
1016 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
1017 fs.writeFileSync(CACHE_FILE, `${branch}|${staged}|${modified}`);
1018 } catch {
1019 fs.writeFileSync(CACHE_FILE, '||');
1020 }
1021 }
1022
1023 const [branch, staged, modified] = fs.readFileSync(CACHE_FILE, 'utf8').trim().split('|');
1024
1025 if (branch) {
1026 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} +${staged} ~${modified}`);
1027 } else {
1028 console.log(`[${model}] 📁 ${dir}`);
1029 }
1030});
1031```
1032
1033### Windows configuration
1034
1035On Windows, Claude Code runs status line commands through Git Bash when Git Bash is installed, or through PowerShell when Git Bash is absent.
1036
1037Git Bash treats unquoted backslashes as escape characters, so a Windows-style path such as `C:\Users\username\script.mjs` reaches the script runner with its separators removed and the command fails without a visible error. Write file paths in the `command` string with forward slashes, as shown in the examples below. The `~` shorthand also works and expands to your Windows home directory.
1038
1039To run a PowerShell script as your status line, invoke it via `powershell`. This works whether Claude Code routes the command through Git Bash or PowerShell:
1040
1041```json settings.json theme={null}
1042{
1043 "statusLine": {
1044 "type": "command",
1045 "command": "powershell -NoProfile -File C:/Users/username/.claude/statusline.ps1"
9541046 }
955
956 if cache_is_stale; then
957 if git rev-parse --git-dir > /dev/null 2>&1; then
958 BRANCH=$(git branch --show-current 2>/dev/null)
959 STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
960 MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
961 echo "$BRANCH|$STAGED|$MODIFIED" > "$CACHE_FILE"
962 else
963 echo "||" > "$CACHE_FILE"
964 fi
965 fi
966
967 IFS='|' read -r BRANCH STAGED MODIFIED < "$CACHE_FILE"
968
969 if [ -n "$BRANCH" ]; then
970 echo "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH +$STAGED ~$MODIFIED"
971 else
972 echo "[$MODEL] 📁 ${DIR##*/}"
973 fi
974 ```
975
976 ```python Python theme={null}
977 #!/usr/bin/env python3
978 import json, sys, subprocess, os, time
979
980 data = json.load(sys.stdin)
981 model = data['model']['display_name']
982 directory = os.path.basename(data['workspace']['current_dir'])
983 session_id = data['session_id']
984
985 CACHE_FILE = f"/tmp/statusline-git-cache-{session_id}"
986 CACHE_MAX_AGE = 5 # seconds
987
988 def cache_is_stale():
989 if not os.path.exists(CACHE_FILE):
990 return True
991 return time.time() - os.path.getmtime(CACHE_FILE) > CACHE_MAX_AGE
992
993 if cache_is_stale():
994 try:
995 subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
996 branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
997 staged = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
998 modified = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
999 staged_count = len(staged.split('\n')) if staged else 0
1000 modified_count = len(modified.split('\n')) if modified else 0
1001 with open(CACHE_FILE, 'w') as f:
1002 f.write(f"{branch}|{staged_count}|{modified_count}")
1003 except:
1004 with open(CACHE_FILE, 'w') as f:
1005 f.write("||")
1006
1007 with open(CACHE_FILE) as f:
1008 branch, staged, modified = f.read().strip().split('|')
1009
1010 if branch:
1011 print(f"[{model}] 📁 {directory} | 🌿 {branch} +{staged} ~{modified}")
1012 else:
1013 print(f"[{model}] 📁 {directory}")
1014 ```
1015
1016 ```javascript Node.js theme={null}
1017 #!/usr/bin/env node
1018 const { execSync } = require('child_process');
1019 const fs = require('fs');
1020 const path = require('path');
1021
1022 let input = '';
1023 process.stdin.on('data', chunk => input += chunk);
1024 process.stdin.on('end', () => {
1025 const data = JSON.parse(input);
1026 const model = data.model.display_name;
1027 const dir = path.basename(data.workspace.current_dir);
1028 const sessionId = data.session_id;
1029
1030 const CACHE_FILE = `/tmp/statusline-git-cache-${sessionId}`;
1031 const CACHE_MAX_AGE = 5; // seconds
1032
1033 const cacheIsStale = () => {
1034 if (!fs.existsSync(CACHE_FILE)) return true;
1035 return (Date.now() / 1000) - fs.statSync(CACHE_FILE).mtimeMs / 1000 > CACHE_MAX_AGE;
1036 };
1037
1038 if (cacheIsStale()) {
1039 try {
1040 execSync('git rev-parse --git-dir', { stdio: 'ignore' });
1041 const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
1042 const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
1043 const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
1044 fs.writeFileSync(CACHE_FILE, `${branch}|${staged}|${modified}`);
1045 } catch {
1046 fs.writeFileSync(CACHE_FILE, '||');
1047 }
1048 }
1049
1050 const [branch, staged, modified] = fs.readFileSync(CACHE_FILE, 'utf8').trim().split('|');
1051
1052 if (branch) {
1053 console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} +${staged} ~${modified}`);
1054 } else {
1055 console.log(`[${model}] 📁 ${dir}`);
1056 }
1057 });
1058 ```
1059</CodeGroup>
1060
1061### Windows configuration
1062
1063On Windows, Claude Code runs status line commands through Git Bash when Git Bash is installed, or through PowerShell when Git Bash is absent.
1064
1065Git Bash treats unquoted backslashes as escape characters, so a Windows-style path such as `C:\Users\username\script.mjs` reaches the script runner with its separators removed and the command fails without a visible error. Write file paths in the `command` string with forward slashes, as shown in the examples below. The `~` shorthand also works and expands to your Windows home directory.
1066
1067To run a PowerShell script as your status line, invoke it via `powershell`. This works whether Claude Code routes the command through Git Bash or PowerShell:
1068
1069<CodeGroup>
1070 ```json settings.json theme={null}
1071 {
1072 "statusLine": {
1073 "type": "command",
1074 "command": "powershell -NoProfile -File C:/Users/username/.claude/statusline.ps1"
1075 }
1047}
1048```
1049
1050```powershell statusline.ps1 theme={null}
1051$input_json = $input | Out-String | ConvertFrom-Json
1052$cwd = $input_json.cwd
1053$model = $input_json.model.display_name
1054$used = $input_json.context_window.used_percentage
1055$dirname = Split-Path $cwd -Leaf
1056
1057if ($used) {
1058 Write-Host "$dirname [$model] ctx: $used%"
1059} else {
1060 Write-Host "$dirname [$model]"
1061}
1062```
1063
1064Or, when Git Bash is installed, run a Bash script directly:
1065
1066```json settings.json theme={null}
1067{
1068 "statusLine": {
1069 "type": "command",
1070 "command": "~/.claude/statusline.sh"
10761071 }
1077 ```
1078
1079 ```powershell statusline.ps1 theme={null}
1080 $input_json = $input | Out-String | ConvertFrom-Json
1081 $cwd = $input_json.cwd
1082 $model = $input_json.model.display_name
1083 $used = $input_json.context_window.used_percentage
1084 $dirname = Split-Path $cwd -Leaf
1085
1086 if ($used) {
1087 Write-Host "$dirname [$model] ctx: $used%"
1088 } else {
1089 Write-Host "$dirname [$model]"
1090 }
1091 ```
1092</CodeGroup>
1093
1094Or, when Git Bash is installed, run a Bash script directly:
1095
1096<CodeGroup>
1097 ```json settings.json theme={null}
1098 {
1099 "statusLine": {
1100 "type": "command",
1101 "command": "~/.claude/statusline.sh"
1102 }
1103 }
1104 ```
1105
1106 ```bash statusline.sh theme={null}
1107 #!/usr/bin/env bash
1108 input=$(cat)
1109 cwd=$(echo "$input" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
1110 model=$(echo "$input" | grep -o '"display_name":"[^"]*"' | cut -d'"' -f4)
1111 dirname="${cwd##*[/\\]}"
1112 echo "$dirname [$model]"
1113 ```
1114</CodeGroup>
1072}
1073```
1074
1075```bash statusline.sh theme={null}
1076#!/usr/bin/env bash
1077input=$(cat)
1078cwd=$(echo "$input" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
1079model=$(echo "$input" | grep -o '"display_name":"[^"]*"' | cut -d'"' -f4)
1080dirname="${cwd##*[/\\]}"
1081echo "$dirname [$model]"
1082```
11151083
11161084## Subagent status lines
11171085
11181086
No line in this hunk matches that.