站长自营 API 中转

正在比较模型套餐?可以把多个 AI API 接到一个网关里

ZZSwitch 是我自己运营的统一 API 网关,适合需要国内支付、兑换码充值、多模型切换和 OpenAI 兼容接口的开发者。不是 OpenCode 官方服务。

Status Line 是 Claude Code 底部可自定义的状态栏,适合实时查看上下文窗口使用量、会话费用、git 分支和权限模式。可用 /statusline 自动生成脚本,也可在 settings.json 里手动指定 statusLine,脚本通过 stdin 接收 JSON,输出到 stdout 即可生效。

Claude Code Status Line:怎么配置底部状态栏

Status Line 是 Claude Code 底部可以自定义的状态栏。它运行你配置的 shell 脚本,把 JSON 会话数据传给脚本,再把脚本输出显示在界面底部。

常见用途包括:

  • 监控上下文窗口使用量,避免接近上限
  • 追踪会话费用和耗时
  • 多会话并行时区分当前会话
  • 始终显示 git 分支和仓库状态

如果你要的只是“把某些会话信息固定显示在 Claude Code 底部”,这页就是配置入口。

怎么设置 Status Line

你可以用 /statusline 让 Claude Code 自动生成脚本,也可以手动写脚本并写入设置。

用 /statusline 命令生成

/statusline 接受自然语言描述,Claude Code 会生成脚本并更新设置:

/statusline show model name and context percentage with a progress bar

手动配置 Status Line

在用户设置 ~/.claude/settings.json 中,或者在项目设置里添加 statusLine 字段。把 type 设为 "command",再把 command 指向脚本路径或内联 shell 命令。

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

command 会在 shell 中执行,所以也可以直接写内联命令,而不一定要单独脚本文件。下面这个例子用 jq 解析 JSON 输入,显示模型名和上下文百分比:

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

可选字段说明:

  • padding:给状态栏内容增加额外的水平空白,单位是字符。默认值是 0。它是在界面自带间距之外再加的缩进,不是到终端边缘的绝对距离。
  • refreshInterval:每隔 N 秒重新运行命令,同时保留事件触发更新。最小值是 1。适合显示时钟,或者主会话空闲、后台代理还在改 git 状态的场景。未设置时,只在事件触发时运行。
  • hideVimModeIndicator:隐藏提示符下方内置的 -- INSERT -- 文本。当你的脚本自己已经渲染了 vim.mode 时,把它设为 true,避免显示两次。

关闭 Status Line

运行 /statusline,让 Claude Code 删除或清空状态栏,例如:

/statusline delete
/statusline clear
/statusline remove it

也可以直接从 settings.json 中删除 statusLine 字段。

怎么手动一步步搭建 Status Line

下面这段演示的是底层流程:手动创建一个状态栏,显示当前模型、工作目录和上下文窗口使用百分比。

直接运行 /statusline 并描述你想显示的内容,Claude Code 会自动把这些步骤都配置好。

这些示例使用 Bash,适用于 macOS 和 Linux。Windows 下请看Windows 配置,里面有 PowerShell 和 Git Bash 的写法。

Claude Code 会通过 stdin 把 JSON 数据发给脚本。脚本用 [`jq`](https://jqlang.github.io/jq/) 解析模型名、目录和上下文百分比,再把格式化结果打印出来。

把下面脚本保存到 `~/.claude/statusline.sh`,其中 `~` 是你的 home 目录,比如 macOS 上的 `/Users/username`,Linux 上的 `/home/username`:

```bash
#!/bin/bash
# Read JSON data that Claude Code sends to stdin
input=$(cat)

# Extract fields using jq
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
# The "// 0" provides a fallback if the field is null
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)

# Output the status line - ${DIR##*/} extracts just the folder name
echo "[$MODEL] 📁 ${DIR##*/} | ${PCT}% context"
```



给脚本加上可执行权限,这样 shell 才能运行它:

```bash
chmod +x ~/.claude/statusline.sh
```



把 Claude Code 配置为运行这个脚本作为状态栏。把下面内容写入 `~/.claude/settings.json`。这里的 `type` 设为 `"command"`,意思是执行这个 shell 命令;`command` 指向你的脚本:

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

状态栏会出现在界面底部。设置会自动重新加载,但变化要等你下一次和 Claude Code 交互后才会显示。

Status Line 的工作方式

Claude Code 会运行你的脚本,并通过 stdin 把 JSON 会话数据传进去。脚本读取 JSON,提取需要的字段,然后把文本打印到 stdout。Claude Code 显示的就是脚本打印出来的内容。

更新时机

脚本会在每次新的 assistant 消息后运行,在 /compact 完成后运行,在权限模式变化时运行,也会在 vim 模式切换时运行。更新有 300ms 防抖,短时间内多次变化会合并,等状态稳定后只跑一次。如果脚本还在运行,又触发了新的更新,这次执行会被取消。你编辑脚本后,改动不会立刻显示,直到下一次 Claude Code 交互触发更新。

当主会话处于空闲时,这些触发也会安静下来,比如协调器正在等待后台子代理。这种情况下,如果你要显示时钟或外部数据,可以设置 refreshInterval,让命令按固定间隔重跑。

脚本可以输出什么

Status Line 在本地运行,不消耗 API token。在某些界面交互期间它会临时隐藏,包括自动补全建议、帮助菜单和权限提示。

可用数据

Claude Code 会通过 stdin 把下面这些 JSON 字段发给脚本:

字段 说明
model.id, model.display_name 当前模型标识和显示名称
cwd, workspace.current_dir 当前工作目录。两个字段值相同;为了和 workspace.project_dir 保持一致,优先用 workspace.current_dir
workspace.project_dir Claude Code 启动时所在目录;如果会话中工作目录变化,它可能和 cwd 不同
workspace.added_dirs 通过 /add-dir--add-dir 额外添加的目录;如果没添加过就是空数组
workspace.git_worktree 当前目录位于 git worktree add 创建的关联 worktree 中时的 worktree 名称。主工作树里不会出现该字段。任何 git worktree 都会有这个字段,不同于只适用于 --worktree 会话的 worktree.*
workspace.repo.host, workspace.repo.owner, workspace.repo.name origin remote 解析出来的仓库身份,例如 "github.com""anthropics""claude-code"。如果不在 git 仓库里,或者没有配置 origin remote,就不会出现
cost.total_cost_usd 估算的会话费用,单位 USD,客户端计算,可能和实际账单有差异
cost.total_duration_ms 从会话开始到现在的总墙钟时间,单位毫秒
cost.total_api_duration_ms 仅等待 API 响应的总时间,单位毫秒
cost.total_lines_added, cost.total_lines_removed 代码增删行数
context_window.total_input_tokens, context_window.total_output_tokens 最近一次 API 响应时,当前上下文窗口里的 token 数。输入包含 cache read 和 cache write。{/* min-version: 2.1.132 */}在 v2.1.132 之前,这两个值是累计会话总数
context_window.context_window_size 最大上下文窗口大小,单位 token。默认是 200000;扩展上下文模型是 1000000
context_window.used_percentage 预先计算好的上下文窗口使用百分比
context_window.remaining_percentage 预先计算好的上下文窗口剩余百分比
context_window.current_usage 最近一次 API 调用的 token 明细,见上下文字段
exceeds_200k_tokens 最近一次 API 响应的总 token 数(input、cache、output 合计)是否超过 200k。这个阈值是固定的,不管实际上下文窗口多大
effort.level 当前 reasoning effort,取值为 lowmediumhighxhighmax。它反映的是实时会话值,包括会话中途 /effort 的变化。如果当前模型不支持 effort 参数,这个字段不会出现
thinking.enabled 当前会话是否启用了 extended thinking
rate_limits.five_hour.used_percentage, rate_limits.seven_day.used_percentage 5 小时或 7 天限额的使用百分比,范围 0 到 100
rate_limits.five_hour.resets_at, rate_limits.seven_day.resets_at 5 小时或 7 天限额窗口重置的 Unix epoch 秒数
session_id 唯一会话 ID
session_name --name/rename 设置的自定义会话名;没设过就不会出现
transcript_path 对话记录文件路径
version Claude Code 版本
output_style.name 当前输出风格名称
vim.mode 启用 vim mode 时的当前 vim 模式,取值为 NORMALINSERTVISUALVISUAL LINE
agent.name 通过 --agent 启动,或配置了 agent 设置时的 agent 名称
pr.number, pr.url 当前分支对应的 open pull request。显示效果和底部状态栏里的 PR 徽标一致。找不到 PR、当前不在 git 仓库中,或者 PR 已 merge/close 后,这些字段就不存在
pr.review_state open PR 的审核状态:approvedpendingchanges_requesteddraft。即使 pr 存在,pr.review_state 也可能单独缺失
worktree.name 当前激活 worktree 的名称。仅在 --worktree 会话中存在
worktree.path worktree 目录的绝对路径
worktree.branch worktree 的 git 分支名,例如 "worktree-my-feature"。基于 hook 的 worktree 不会有这个字段
worktree.original_cwd 进入 worktree 之前 Claude 所在的目录
worktree.original_branch 进入 worktree 之前检出的 git 分支。基于 hook 的 worktree 不会有这个字段

完整 JSON 结构

你的状态栏命令会通过 stdin 接收下面这个 JSON:

{
  "cwd": "/current/working/directory",
  "session_id": "abc123...",
  "session_name": "my-session",
  "transcript_path": "/path/to/transcript.jsonl",
  "model": {
    "id": "claude-opus-4-7",
    "display_name": "Opus"
  },
  "workspace": {
    "current_dir": "/current/working/directory",
    "project_dir": "/original/project/directory",
    "added_dirs": [],
    "git_worktree": "feature-xyz",
    "repo": {
      "host": "github.com",
      "owner": "anthropics",
      "name": "claude-code"
    }
  },
  "version": "2.1.90",
  "output_style": {
    "name": "default"
  },
  "cost": {
    "total_cost_usd": 0.01234,
    "total_duration_ms": 45000,
    "total_api_duration_ms": 2300,
    "total_lines_added": 156,
    "total_lines_removed": 23
  },
  "context_window": {
    "total_input_tokens": 15500,
    "total_output_tokens": 1200,
    "context_window_size": 200000,
    "used_percentage": 8,
    "remaining_percentage": 92,
    "current_usage": {
      "input_tokens": 8500,
      "output_tokens": 1200,
      "cache_creation_input_tokens": 5000,
      "cache_read_input_tokens": 2000
    }
  },
  "exceeds_200k_tokens": false,
  "effort": {
    "level": "high"
  },
  "thinking": {
    "enabled": true
  },
  "rate_limits": {
    "five_hour": {
      "used_percentage": 23.5,
      "resets_at": 1738425600
    },
    "seven_day": {
      "used_percentage": 41.2,
      "resets_at": 1738857600
    }
  },
  "vim": {
    "mode": "NORMAL"
  },
  "agent": {
    "name": "security-reviewer"
  },
  "pr": {
    "number": 1234,
    "url": "https://github.com/anthropics/claude-code/pull/1234",
    "review_state": "pending"
  },
  "worktree": {
    "name": "my-feature",
    "path": "/path/to/.claude/worktrees/my-feature",
    "branch": "worktree-my-feature",
    "original_cwd": "/path/to/project",
    "original_branch": "main"
  }
}

可能缺失的字段(JSON 中不会出现):

  • session_name:仅当通过 --name/rename 设置后出现
  • workspace.git_worktree:仅在关联 git worktree 中出现
  • workspace.repo:仅在带 origin remote 的 git 仓库内出现
  • effort:仅当前模型支持 reasoning effort 参数时出现
  • vim:仅启用 vim mode 时出现
  • agent:仅使用 --agent 启动或配置了 agent 时出现
  • pr:仅当前分支找到 open PR 时出现;PR merge 或 close 后会移除。pr.review_state 也可能单独缺失
  • worktree:仅在 --worktree 会话中出现。出现时,branchoriginal_branch 在基于 hook 的 worktree 中也可能缺失
  • rate_limits:仅 Claude.ai 订阅用户(Pro/Max)在会话首次 API 响应后出现。five_hourseven_day 两个窗口可以独立缺失。用 jq -r '.rate_limits.five_hour.used_percentage // empty' 可以优雅处理缺失

可能为 null 的字段

  • context_window.current_usage:会话第一次 API 调用前为 null/compact 之后到下一次 API 调用前也会再次变成 null
  • context_window.used_percentage, context_window.remaining_percentage:会话早期可能为 null

脚本里要同时处理“字段缺失”和 null 值,建议用条件访问和回退默认值。

上下文字段

context_window 对象描述的是最近一次 API 响应对应的实时上下文窗口。自 v2.1.132 起,total_input_tokenstotal_output_tokens 反映的是当前上下文使用量,不再是累计会话总数。

  • 合计值total_input_tokens, total_output_tokens):当前上下文窗口里的 token 数。total_input_tokensinput_tokenscache_creation_input_tokenscache_read_input_tokens 之和;total_output_tokens 是最近一次响应的输出 token 数。第一次 API 响应前这两个值都是 0
  • 分项用量current_usage):按类别拆开的同一组 token 数。如果你需要把 cache 命中和新输入区分开,就用这个对象。

current_usage 包含:

  • input_tokens:当前上下文里的输入 token
  • output_tokens:生成的输出 token
  • cache_creation_input_tokens:写入缓存的 token
  • cache_read_input_tokens:从缓存读取的 token

缓存字段的含义和计费方式,见检查缓存性能

used_percentage 的计算只基于输入 token:input_tokens + cache_creation_input_tokens + cache_read_input_tokens,不包含 output_tokens

如果你从 current_usage 手动算上下文百分比,要用同样的输入-only 公式,才能和 used_percentage 一致。

current_usage 在会话第一次 API 调用前为 null/compact 之后也会一直是 null,直到下一次 API 调用重新填充。

示例

下面这些示例展示了常见的状态栏模式。使用任意示例时:

  1. 把脚本保存到 ~/.claude/statusline.sh 之类的文件里,也可以是 .py.js
  2. 赋予可执行权限:chmod +x ~/.claude/statusline.sh
  3. 把路径写入你的设置

Bash 示例使用 jq 解析 JSON。Python 和 Node.js 可以直接解析 JSON。

上下文窗口使用量

显示当前模型和上下文窗口使用量,并带一个可视化进度条。每个脚本都从 stdin 读取 JSON,提取 used_percentage 字段,再构造一个 10 字符的条形图,填充块(▓)表示已用部分:

#!/bin/bash
# Read all of stdin into a variable
input=$(cat)

# Extract fields with jq, "// 0" provides fallback for null
MODEL=$(echo "$input" | jq -r '.model.display_name')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)

# Build progress bar: printf -v creates a run of spaces, then
# ${var// /▓} replaces each space with a block character
BAR_WIDTH=10
FILLED=$((PCT * BAR_WIDTH / 100))
EMPTY=$((BAR_WIDTH - FILLED))
BAR=""
[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /▓}"
[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"

echo "[$MODEL] $BAR $PCT%"
#!/usr/bin/env python3
import json, sys

# json.load reads and parses stdin in one step
data = json.load(sys.stdin)
model = data['model']['display_name']
# "or 0" handles null values
pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)

# String multiplication builds the bar
filled = pct * 10 // 100
bar = '▓' * filled + '░' * (10 - filled)

print(f"[{model}] {bar} {pct}%")
#!/usr/bin/env node
// Node.js reads stdin asynchronously with events
let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;
    // Optional chaining (?.) safely handles null fields
    const pct = Math.floor(data.context_window?.used_percentage || 0);

    // String.repeat() builds the bar
    const filled = Math.floor(pct * 10 / 100);
    const bar = '▓'.repeat(filled) + '░'.repeat(10 - filled);

    console.log(`[${model}] ${bar} ${pct}%`);
});

带颜色的 git 状态

显示 git 分支,并用颜色区分已暂存和已修改文件。这个脚本使用 ANSI 转义码\033[32m 是绿色,\033[33m 是黄色,\033[0m 重置为默认颜色。

每个脚本都会检查当前目录是否是 git 仓库,统计 staged 和 modified 文件,并显示颜色标识:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')

GREEN='\033[32m'
YELLOW='\033[33m'
RESET='\033[0m'

if git rev-parse --git-dir > /dev/null 2>&1; then
    BRANCH=$(git branch --show-current 2>/dev/null)
    STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
    MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')

    GIT_STATUS=""
    [ "$STAGED" -gt 0 ] && GIT_STATUS="${GREEN}+${STAGED}${RESET}"
    [ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${YELLOW}~${MODIFIED}${RESET}"

    echo -e "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH $GIT_STATUS"
else
    echo "[$MODEL] 📁 ${DIR##*/}"
fi
#!/usr/bin/env python3
import json, sys, subprocess, os

data = json.load(sys.stdin)
model = data['model']['display_name']
directory = os.path.basename(data['workspace']['current_dir'])

GREEN, YELLOW, RESET = '\033[32m', '\033[33m', '\033[0m'

try:
    subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
    branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
    staged_output = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
    modified_output = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
    staged = len(staged_output.split('\n')) if staged_output else 0
    modified = len(modified_output.split('\n')) if modified_output else 0

    git_status = f"{GREEN}+{staged}{RESET}" if staged else ""
    git_status += f"{YELLOW}~{modified}{RESET}" if modified else ""

    print(f"[{model}] 📁 {directory} | 🌿 {branch} {git_status}")
except:
    print(f"[{model}] 📁 {directory}")
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');

let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;
    const dir = path.basename(data.workspace.current_dir);

    const GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RESET = '\x1b[0m';

    try {
        execSync('git rev-parse --git-dir', { stdio: 'ignore' });
        const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
        const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
        const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;

        let gitStatus = staged ? `${GREEN}+${staged}${RESET}` : '';
        gitStatus += modified ? `${YELLOW}~${modified}${RESET}` : '';

        console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} ${gitStatus}`);
    } catch {
        console.log(`[${model}] 📁 ${dir}`);
    }
});

费用和时长追踪

跟踪会话的 API 费用和已用时间。cost.total_cost_usd 字段累计当前会话里所有 API 调用的估算费用。cost.total_duration_ms 记录会话开始后的总时长,cost.total_api_duration_ms 只统计等待 API 响应的时间。

每个脚本都会把费用格式化成货币,并把毫秒转换成分钟和秒:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')

COST_FMT=$(printf '$%.2f' "$COST")
DURATION_SEC=$((DURATION_MS / 1000))
MINS=$((DURATION_SEC / 60))
SECS=$((DURATION_SEC % 60))

echo "[$MODEL] 💰 $COST_FMT | ⏱️ ${MINS}m ${SECS}s"
#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
model = data['model']['display_name']
cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0

duration_sec = duration_ms // 1000
mins, secs = duration_sec // 60, duration_sec % 60

print(f"[{model}] 💰 ${cost:.2f} | ⏱️ {mins}m {secs}s")
#!/usr/bin/env node
let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;
    const cost = data.cost?.total_cost_usd || 0;
    const durationMs = data.cost?.total_duration_ms || 0;

    const durationSec = Math.floor(durationMs / 1000);
    const mins = Math.floor(durationSec / 60);
    const secs = durationSec % 60;

    console.log(`[${model}] 💰 $${cost.toFixed(2)} | ⏱️ ${mins}m ${secs}s`);
});

多行状态栏

脚本可以输出多行,做出更丰富的展示。每个 echo 都会变成状态区域的一行。

这个例子把几种技巧组合在一起:根据上下文使用率设置阈值颜色(70% 以下绿色、70%-89% 黄色、90% 以上红色)、进度条,以及 git 分支信息。每个 printecho 都会输出一行:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')

CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'

# Pick bar color based on context usage
if [ "$PCT" -ge 90 ]; then BAR_COLOR="$RED"
elif [ "$PCT" -ge 70 ]; then BAR_COLOR="$YELLOW"
else BAR_COLOR="$GREEN"; fi

FILLED=$((PCT / 10)); EMPTY=$((10 - FILLED))
printf -v FILL "%${FILLED}s"; printf -v PAD "%${EMPTY}s"
BAR="${FILL// /█}${PAD// /░}"

MINS=$((DURATION_MS / 60000)); SECS=$(((DURATION_MS % 60000) / 1000))

BRANCH=""
git rev-parse --git-dir > /dev/null 2>&1 && BRANCH=" | 🌿 $(git branch --show-current 2>/dev/null)"

echo -e "${CYAN}[$MODEL]${RESET} 📁 ${DIR##*/}$BRANCH"
COST_FMT=$(printf '$%.2f' "$COST")
echo -e "${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${YELLOW}${COST_FMT}${RESET} | ⏱️ ${MINS}m ${SECS}s"
#!/usr/bin/env python3
import json, sys, subprocess, os

data = json.load(sys.stdin)
model = data['model']['display_name']
directory = os.path.basename(data['workspace']['current_dir'])
cost = data.get('cost', {}).get('total_cost_usd', 0) or 0
pct = int(data.get('context_window', {}).get('used_percentage', 0) or 0)
duration_ms = data.get('cost', {}).get('total_duration_ms', 0) or 0

CYAN, GREEN, YELLOW, RED, RESET = '\033[36m', '\033[32m', '\033[33m', '\033[31m', '\033[0m'

bar_color = RED if pct >= 90 else YELLOW if pct >= 70 else GREEN
filled = pct // 10
bar = '█' * filled + '░' * (10 - filled)

mins, secs = duration_ms // 60000, (duration_ms % 60000) // 1000

try:
    branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True, stderr=subprocess.DEVNULL).strip()
    branch = f" | 🌿 {branch}" if branch else ""
except:
    branch = ""

print(f"{CYAN}[{model}]{RESET} 📁 {directory}{branch}")
print(f"{bar_color}{bar}{RESET} {pct}% | {YELLOW}${cost:.2f}{RESET} | ⏱️ {mins}m {secs}s")
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');

let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;
    const dir = path.basename(data.workspace.current_dir);
    const cost = data.cost?.total_cost_usd || 0;
    const pct = Math.floor(data.context_window?.used_percentage || 0);
    const durationMs = data.cost?.total_duration_ms || 0;

    const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', RED = '\x1b[31m', RESET = '\x1b[0m';

    const barColor = pct >= 90 ? RED : pct >= 70 ? YELLOW : GREEN;
    const filled = Math.floor(pct / 10);
    const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);

    const mins = Math.floor(durationMs / 60000);
    const secs = Math.floor((durationMs % 60000) / 1000);

    let branch = '';
    try {
        branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
        branch = branch ? ` | 🌿 ${branch}` : '';
    } catch {}

    console.log(`${CYAN}[${model}]${RESET} 📁 ${dir}${branch}`);
    console.log(`${barColor}${bar}${RESET} ${pct}% | ${YELLOW}$${cost.toFixed(2)}${RESET} | ⏱️ ${mins}m ${secs}s`);
});

可点击链接

这个例子会创建一个指向 GitHub 仓库的可点击链接。它读取 git remote URL,把 SSH 格式转换成 HTTPS,然后用 OSC 8 转义码把仓库名包起来。按住 Cmd(macOS)或 Ctrl(Windows/Linux)点击即可在浏览器打开。

每个脚本都会读取 git remote URL,转换 SSH 格式为 HTTPS,然后用 OSC 8 转义码包裹仓库名。Bash 版本使用 printf '%b',它比跨 shell 使用 echo -e 更可靠地处理反斜杠转义:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')

# Convert git SSH URL to HTTPS
REMOTE=$(git remote get-url origin 2>/dev/null | sed 's/git@github.com:/https:\/\/github.com\//' | sed 's/\.git$//')

if [ -n "$REMOTE" ]; then
    REPO_NAME=$(basename "$REMOTE")
    # OSC 8 format: \e]8;;URL\a then TEXT then \e]8;;\a
    # printf %b interprets escape sequences reliably across shells
    printf '%b' "[$MODEL] 🔗 \e]8;;${REMOTE}\a${REPO_NAME}\e]8;;\a\n"
else
    echo "[$MODEL]"
fi
#!/usr/bin/env python3
import json, sys, subprocess, re, os

data = json.load(sys.stdin)
model = data['model']['display_name']

# Get git remote URL
try:
    remote = subprocess.check_output(
        ['git', 'remote', 'get-url', 'origin'],
        stderr=subprocess.DEVNULL, text=True
    ).strip()
    # Convert SSH to HTTPS format
    remote = re.sub(r'^git@github\.com:', 'https://github.com/', remote)
    remote = re.sub(r'\.git$', '', remote)
    repo_name = os.path.basename(remote)
    # OSC 8 escape sequences
    link = f"\033]8;;{remote}\a{repo_name}\033]8;;\a"
    print(f"[{model}] 🔗 {link}")
except:
    print(f"[{model}]")
#!/usr/bin/env node
const { execSync } = require('child_process');
const path = require('path');

let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;

    try {
        let remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
        // Convert SSH to HTTPS format
        remote = remote.replace(/^git@github\.com:/, 'https://github.com/').replace(/\.git$/, '');
        const repoName = path.basename(remote);
        // OSC 8 escape sequences
        const link = `\x1b]8;;${remote}\x07${repoName}\x1b]8;;\x07`;
        console.log(`[${model}] 🔗 ${link}`);
    } catch {
        console.log(`[${model}]`);
    }
});

速率限制使用量

在状态栏里显示 Claude.ai 订阅限额使用情况。rate_limits 对象包含 five_hour(5 小时滚动窗口)和 seven_day(周窗口)。每个窗口提供 used_percentage(0-100)和 resets_at(Unix epoch 秒)。

这个字段只会在 Claude.ai 订阅用户(Pro/Max)第一次 API 响应之后出现。每个脚本都要优雅处理字段缺失:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')
# "// empty" produces no output when rate_limits is absent
FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')

LIMITS=""
[ -n "$FIVE_H" ] && LIMITS="5h: $(printf '%.0f' "$FIVE_H")%"
[ -n "$WEEK" ] && LIMITS="${LIMITS:+$LIMITS }7d: $(printf '%.0f' "$WEEK")%"

[ -n "$LIMITS" ] && echo "[$MODEL] | $LIMITS" || echo "[$MODEL]"
#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
model = data['model']['display_name']

parts = []
rate = data.get('rate_limits', {})
five_h = rate.get('five_hour', {}).get('used_percentage')
week = rate.get('seven_day', {}).get('used_percentage')

if five_h is not None:
    parts.append(f"5h: {five_h:.0f}%")
if week is not None:
    parts.append(f"7d: {week:.0f}%")

if parts:
    print(f"[{model}] | {' '.join(parts)}")
else:
    print(f"[{model}]")
#!/usr/bin/env node
let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;

    const parts = [];
    const fiveH = data.rate_limits?.five_hour?.used_percentage;
    const week = data.rate_limits?.seven_day?.used_percentage;

    if (fiveH != null) parts.push(`5h: ${Math.round(fiveH)}%`);
    if (week != null) parts.push(`7d: ${Math.round(week)}%`);

    console.log(parts.length ? `[${model}] | ${parts.join(' ')}` : `[${model}]`);
});

缓存耗时操作

Status Line 脚本在活跃会话里会频繁运行。像 git statusgit diff 这种命令在大仓库里可能很慢。这个示例把 git 信息缓存到临时文件里,只每 5 秒刷新一次。

缓存文件名必须满足两件事:在同一个会话里多次运行时保持稳定;在不同会话之间又不能互相覆盖。像 $$os.getpid()process.pid 这类进程 ID 每次调用都会变,反而会让缓存失效。要用 JSON 输入里的 session_id,它在整个会话期间稳定,而且每个会话都唯一。

每个脚本都会先检查缓存文件是否不存在或已超过 5 秒,再决定是否运行 git 命令:

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

MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
SESSION_ID=$(echo "$input" | jq -r '.session_id')

CACHE_FILE="/tmp/statusline-git-cache-$SESSION_ID"
CACHE_MAX_AGE=5  # seconds

cache_is_stale() {
    [ ! -f "$CACHE_FILE" ] || \
    # stat -f %m is macOS, stat -c %Y is Linux
    [ $(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0))) -gt $CACHE_MAX_AGE ]
}

if cache_is_stale; then
    if git rev-parse --git-dir > /dev/null 2>&1; then
        BRANCH=$(git branch --show-current 2>/dev/null)
        STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
        MODIFIED=$(git diff --numstat 2>/dev/null | wc -l | tr -d ' ')
        echo "$BRANCH|$STAGED|$MODIFIED" > "$CACHE_FILE"
    else
        echo "||" > "$CACHE_FILE"
    fi
fi

IFS='|' read -r BRANCH STAGED MODIFIED < "$CACHE_FILE"

if [ -n "$BRANCH" ]; then
    echo "[$MODEL] 📁 ${DIR##*/} | 🌿 $BRANCH +$STAGED ~$MODIFIED"
else
    echo "[$MODEL] 📁 ${DIR##*/}"
fi
#!/usr/bin/env python3
import json, sys, subprocess, os, time

data = json.load(sys.stdin)
model = data['model']['display_name']
directory = os.path.basename(data['workspace']['current_dir'])
session_id = data['session_id']

CACHE_FILE = f"/tmp/statusline-git-cache-{session_id}"
CACHE_MAX_AGE = 5  # seconds

def cache_is_stale():
    if not os.path.exists(CACHE_FILE):
        return True
    return time.time() - os.path.getmtime(CACHE_FILE) > CACHE_MAX_AGE

if cache_is_stale():
    try:
        subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL)
        branch = subprocess.check_output(['git', 'branch', '--show-current'], text=True).strip()
        staged = subprocess.check_output(['git', 'diff', '--cached', '--numstat'], text=True).strip()
        modified = subprocess.check_output(['git', 'diff', '--numstat'], text=True).strip()
        staged_count = len(staged.split('\n')) if staged else 0
        modified_count = len(modified.split('\n')) if modified else 0
        with open(CACHE_FILE, 'w') as f:
            f.write(f"{branch}|{staged_count}|{modified_count}")
    except:
        with open(CACHE_FILE, 'w') as f:
            f.write("||")

with open(CACHE_FILE) as f:
    branch, staged, modified = f.read().strip().split('|')

if branch:
    print(f"[{model}] 📁 {directory} | 🌿 {branch} +{staged} ~{modified}")
else:
    print(f"[{model}] 📁 {directory}")
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

let input = '';
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
    const data = JSON.parse(input);
    const model = data.model.display_name;
    const dir = path.basename(data.workspace.current_dir);
    const sessionId = data.session_id;

    const CACHE_FILE = `/tmp/statusline-git-cache-${sessionId}`;
    const CACHE_MAX_AGE = 5; // seconds

    const cacheIsStale = () => {
        if (!fs.existsSync(CACHE_FILE)) return true;
        return (Date.now() / 1000) - fs.statSync(CACHE_FILE).mtimeMs / 1000 > CACHE_MAX_AGE;
    };

    if (cacheIsStale()) {
        try {
            execSync('git rev-parse --git-dir', { stdio: 'ignore' });
            const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
            const staged = execSync('git diff --cached --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
            const modified = execSync('git diff --numstat', { encoding: 'utf8' }).trim().split('\n').filter(Boolean).length;
            fs.writeFileSync(CACHE_FILE, `${branch}|${staged}|${modified}`);
        } catch {
            fs.writeFileSync(CACHE_FILE, '||');
        }
    }

    const [branch, staged, modified] = fs.readFileSync(CACHE_FILE, 'utf8').trim().split('|');

    if (branch) {
        console.log(`[${model}] 📁 ${dir} | 🌿 ${branch} +${staged} ~${modified}`);
    } else {
        console.log(`[${model}] 📁 ${dir}`);
    }
});

Windows 配置

在 Windows 上,如果安装了 Git Bash,Claude Code 会通过 Git Bash 运行状态栏命令;如果没有安装 Git Bash,则通过 PowerShell 运行。

Git Bash 会把未加引号的反斜杠当成转义字符,所以像 C:\Users\username\script.mjs 这样的 Windows 风格路径,传给脚本运行器时可能会把分隔符吃掉,导致命令失败,而且通常没有明显报错。请在 command 字符串里使用正斜杠,下面的示例就是这样写的。~ 这种缩写也可用,会展开到 Windows 的 home 目录。

要把 PowerShell 脚本作为状态栏运行,可以通过 powershell 调用。无论 Claude Code 把命令路由到 Git Bash 还是 PowerShell,这种写法都能工作:

{
  "statusLine": {
    "type": "command",
    "command": "powershell -NoProfile -File C:/Users/username/.claude/statusline.ps1"
  }
}
$input_json = $input | Out-String | ConvertFrom-Json
$cwd = $input_json.cwd
$model = $input_json.model.display_name
$used = $input_json.context_window.used_percentage
$dirname = Split-Path $cwd -Leaf

if ($used) {
    Write-Host "$dirname [$model] ctx: $used%"
} else {
    Write-Host "$dirname [$model]"
}

或者,在安装了 Git Bash 时,也可以直接运行 Bash 脚本:

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh"
  }
}
#!/usr/bin/env bash
input=$(cat)
cwd=$(echo "$input" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4)
model=$(echo "$input" | grep -o '"display_name":"[^"]*"' | cut -d'"' -f4)
dirname="${cwd##*[/\\]}"
echo "$dirname [$model]"

子代理状态栏

subagentStatusLine 设置会给提示符下方 agent 面板里显示的每个子代理渲染自定义行内容。你可以用它替换默认的 name · description · token count 行格式。

{
  "subagentStatusLine": {
    "type": "command",
    "command": "~/.claude/subagent-statusline.sh"
  }
}

这个命令会在每次刷新时运行一次,stdin 里传入所有可见的子代理行,作为一个 JSON 对象。输入包含 base hook fields 以及 columns(可用行宽)和 tasks 数组;每个 task 都有 idnametypestatusdescriptionlabelstartTimetokenCounttokenSamplescwd

你需要为想覆盖的每一行输出一条 JSON line,格式如下:{"id": "<task id>", "content": "<row body>"}content 会原样渲染,包括 ANSI 颜色和 OSC 8 超链接。省略某个 task 的 id,就保留默认渲染;把 content 留空字符串,则隐藏这一行。

适用于 statusLine 的同一套 trust 和 disableAllHooks 限制,也适用于这里。插件也可以在它们的 settings.json 里提供默认的 subagentStatusLine

提示

  • 用 mock 输入测试echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/home/user/project"},"context_window":{"used_percentage":25},"session_id":"test-session-abc"}' | ./statusline.sh
  • 输出尽量短:状态栏宽度有限,内容太长会被截断或换行得很难看
  • 缓存慢操作:脚本在活跃会话中会频繁运行,git status 之类的命令可能拖慢刷新。可参考缓存耗时操作

社区项目例如 ccstatuslinestarship-claude 提供了预配置方案、主题和额外功能。

排查问题

状态栏不显示怎么解决

  • 确认脚本有可执行权限:chmod +x ~/.claude/statusline.sh
  • 检查脚本是不是输出到 stdout,而不是 stderr
  • 手动运行脚本,确认它确实有输出
  • 如果在安装了 Git Bash 的 Windows 上使用,command 路径里的反斜杠很可能在脚本运行前就被当成转义字符吃掉。请改用正斜杠。见Windows 配置
  • 如果设置里 disableAllHookstrue,状态栏也会被禁用。删除这个设置,或把它改成 false 再启用。
  • 运行 claude --debug,查看本次会话第一次状态栏调用的退出码和 stderr
  • 让 Claude 直接读取你的设置文件,并手动执行 statusLine 命令,把错误暴露出来

状态栏显示 -- 或空值怎么解决

  • 在第一次 API 响应完成前,某些字段可能是 null
  • 在脚本里处理 null 值,比如用 jq// 0
  • 如果多条消息后仍然为空,重启 Claude Code

上下文百分比显示不对怎么解决

  • 优先使用 used_percentage 来表示最直接、最准确的上下文状态
  • 上下文百分比可能和 /context 的输出略有差异,因为它们的计算时机不同

OSC 8 链接不可点击怎么解决

  • 确认终端支持 OSC 8 超链接,比如 iTerm2、Kitty、WezTerm

  • Terminal.app 不支持可点击链接

  • 如果链接文字出现了,但点不动,Claude Code 可能没有识别你的终端支持超链接。这在 Windows Terminal 和一些未被自动识别的终端模拟器里很常见。可以在启动 Claude Code 前设置 FORCE_HYPERLINK 环境变量覆盖检测:

    FORCE_HYPERLINK=1 claude

    在 PowerShell 里,先在当前会话设置变量:

    $env:FORCE_HYPERLINK = "1"; claude
  • SSH 和 tmux 会话可能会根据配置剥离 OSC 序列

  • 如果看到的还是 \e]8;; 这样的字面文本,改用 printf '%b',不要用 echo -e,转义处理更稳定

转义序列显示乱码怎么解决

  • ANSI 颜色、OSC 8 链接这类复杂转义序列,偶尔会和其他 UI 更新冲突,导致输出乱码
  • 如果文本损坏,先把脚本简化成纯文本
  • 带转义码的多行状态栏,比单行纯文本更容易出现渲染问题

工作区信任怎么处理

  • 只有你为当前目录接受了 workspace trust 对话框,状态栏命令才会运行。因为 statusLine 会执行 shell 命令,所以它和 hooks 以及其他会执行 shell 的设置一样,需要先完成信任确认。
  • 如果还没接受信任,你会看到 statusline skipped · restart to fix,而不是状态栏输出。重启 Claude Code 并接受信任提示即可启用。

脚本报错或卡住怎么解决

  • 退出码非 0,或者没有输出,都会让状态栏变空
  • 慢脚本会阻塞状态栏更新,直到它执行完
  • 如果慢脚本还在跑,又触发了新更新,这次执行会被取消
  • 在配置前先用 mock 输入独立测试脚本

通知和状态栏占同一行怎么处理

  • MCP server error、自动更新这类系统通知,会显示在状态栏同一行的右侧。像 context-low 警告这样的短暂通知,也会轮流占用这块区域。
  • 开启 verbose mode 会在这里增加 token 计数器
  • 终端太窄时,这些通知可能会截断你的状态栏输出

常见问题

Claude Code 的 Status Line 怎么开启?

/statusline 直接生成脚本最省事,或者在 settings.json 里配置 "statusLine": {"type": "command", "command": "~/.claude/statusline.sh"}。如果脚本没显示,先检查执行权限、stdout 输出和 workspace trust。

状态栏可以显示哪些信息?

可以显示脚本能从 stdin 的 JSON 里读到的任何字段,比如模型名、工作目录、上下文窗口使用量、费用、git 状态、速率限制、会话名和 PR 信息。你也可以输出多行、颜色和可点击链接。

Claude Code Status Line 和上下文百分比不一致怎么办?

优先用 context_window.used_percentage。如果你自己用 current_usage 计算,要按 input_tokens + cache_creation_input_tokens + cache_read_input_tokens 这个输入-only 公式算,不要把 output_tokens 算进去。

站长自营 API 中转

ZZSwitch API 中转

统一接入多家模型,支持兑换码充值。

打开 ZZSwitch