Claude Code 状态栏脚本报错、空白或卡顿 —— 排查与修复

Claude Code 的 statusLine 报错、空白,或拖慢提示。修复 JSON-on-stdin 输入约定、缺失的 jq、退出码、缓慢的 git 调用,以及工作区信任。

你给 Claude Code 接了一个自定义 statusLine,在提示里显示 git 分支、模型名、上下文用量。第一次跑很好,然后就坏了:栏变空、出现字面文本,或者每轮回复前提示明显卡住。截至 2026 年 6 月,最常见的原因是脚本根本没拿到它期望的输入 —— 因为 Claude Code 通过 $CLAUDE_MODEL 这类环境变量传数据。它把一个 JSON 对象通过 stdin 喂给脚本,脚本必须读 stdin 并解析它(几乎都用 jq)。如果 jq 没装,或者脚本去读根本不存在的环境变量,栏就会静默变空,没有任何报错。

最快修复: 先确认装了 jq(which jq),然后用真实的输入形状喂给脚本,看它打印什么:

echo '{"model":{"display_name":"Opus 4.7"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":25},"session_id":"test"}' | ~/.claude/statusline.sh

如果这条什么都不打印、报错或卡住,bug 就在你的脚本里,不在 Claude Code。下面逐一拆解每种情况。

状态栏到底是怎么工作的

大多数坏掉的脚本就栽在这里。Claude Code 运行你的命令,把 JSON 会话数据通过 stdin 喂进去。脚本读这段 JSON,取出想要的字段,把一行打印到 stdout。落到 stdout 上的内容就是栏。

一份正确的最小脚本(出自官方文档):

#!/bin/bash
input=$(cat)                                            # 从 stdin 读 JSON
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
echo "[$MODEL] ${DIR##*/} | ${PCT}% context"

settings 块必须把 type 设为 "command":

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

你会用到的关键字段名(完整 schema 见官方 statusline 文档):

字段含义
.model.display_name人类可读的模型名,例如 Opus 4.7
.model.id模型标识符,例如 claude-opus-4-7
.workspace.current_dir当前工作目录(.cwd 是同一个值)
.workspace.project_dirClaude Code 启动时所在的目录
.context_window.used_percentage预先算好的上下文已用百分比(早期可能为 null)
.cost.total_cost_usd本次会话的估算成本(美元)
.session_id稳定的单会话 ID,适合做缓存文件名
.versionClaude Code 版本

什么时候运行: 每条新的助手消息之后、/compact 完成之后、权限模式变化时、vim 模式切换时。更新有 300ms 的去抖(debounced at 300ms)。并没有一个单独的”软超时”在 1-2 秒后杀掉慢脚本。实际机制是:如果脚本还在跑时来了新的更新,正在执行的那次会被取消(in-flight run is cancelled)。所以一个持续偏慢的脚本给你的是过期或空白的栏,而不是报错。

常见原因

按实际命中频率排序,截至 2026 年 6 月。

1. 没装 jq(或 bc)

/statusline 生成器和几乎所有示例脚本都会调 jq。如果 jq 缺失,脚本第一条命令就失败,栏直接空白且没有任何警告。这在 Windows(Git Bash)和精简版 Linux / Docker 镜像上是最常见的静默失败 —— 那些环境默认不带 jqbc

怎么判断: which jq 没输出,或者手动跑脚本时 stderr 打出 jq: command not found

2. 脚本读环境变量,而不是 stdin

老脚本或复制粘贴来的脚本会引用 $CLAUDE_MODEL$CLAUDE_SESSION_ID 之类。这些根本没提供。数据只通过 stdin 以 JSON 形式到达。一个从不执行 input=$(cat)(或等价操作)的脚本拿到的全是空值,打印出空白栏或 unknown

怎么判断: 脚本从不读 stdin;无论什么模型、什么目录,栏永远显示你的兜底串(unknown、空)。

3. 脚本非零退出或什么都不打印

非零退出或空 stdout 都会让栏变空。经典触发:set -e 加上在非 git 目录跑 git rev-parse --abbrev-ref HEAD,以 128 退出。

怎么判断: 手动跑完脚本 echo $? 是非零;或 claude --debug 日志里有非零的 statusline 退出和 stderr。

4. 脚本太慢(卡顿 / 被取消)

curl 远端 API、git fetch、在大仓库里递归 find/git status,耗时一长,下一个 300ms 更新就把正在跑的这次取消掉。栏显示过期内容或干脆不更新。

怎么判断: 喂入模拟输入后 time ./statusline.sh 超过约 300ms;你能感到每轮前提示有延迟。

5. 首次 API 响应前字段为 null

.context_window.used_percentage.context_window.remaining_percentage.context_window.current_usage 在首次 API 调用落地前(以及 /compact 之后那一下)可能是 null。对 null 做运算的脚本会报错或打出 --

怎么判断: 启动后栏显示 --null 或空数字,等你发出第一条消息后自己就好了。

6. 没接受工作区信任(workspace trust)

因为 statusLine 会运行一条 shell 命令,它跟 hooks 一样受同一个工作区信任接受门控。如果当前目录没接受过信任,命令不会运行。

怎么判断: 你看到的不是脚本输出,而是提示 statusline skipped · restart to fix

7. 设了 disableAllHooks,或文件/权限不对

settings 里 disableAllHooks: true 会连带禁用状态栏。另外,statusLine.command 可能指向不存在的文件,或文件没有执行位。在 Windows 上用 Git Bash 时,command 路径里的反斜杠会在脚本跑之前被当成转义吃掉。

怎么判断: ls -l <path> 显示文件缺失或没 +x;claude --debug 显示 no-such-file 错误;或 Windows 上路径用了反斜杠。

8. 转义序列以字面文本漏出来

裸 ANSI 颜色码在有些终端能用,在有些终端里就显示成字面 \033[31m(或 \e]8;;)。嵌入的换行会把栏拆成多行。

怎么判断: 栏里出现字面转义码,或本该一行的地方断了行。

你属于哪一类?

症状最可能的原因跳到
栏空白,which jq 失败jq/bc第 1 步
不管上下文如何,栏总显示 unknown/空脚本读环境变量,没读 stdin第 2 步
只在启动时显示 --null,之后恢复首次响应前字段为 null第 3 步
出现 statusline skipped · restart to fix 提示没接受工作区信任第 4 步
每轮前提示卡顿git/网络调用太慢第 5 步
字面 \033[ 或断行转义序列处理第 6 步
栏根本不出现文件缺失、没 +x,或 disableAllHooks第 7 步

开始前

  • 记录栏是空白、出现报错/字面文本,还是内容错。
  • 看是持续的还是间歇的(间歇通常意味着脚本里有网络调用)。
  • ~/.claude/settings.json(或项目级 .claude/settings.json)的 statusLine.command 找到脚本路径。
  • 确认依赖:which jq(如果脚本用了 bc,再 which bc)。

一步步修复

永远用喂入模拟 JSON 的方式测试。单独跑不起来,在提示里也绝对跑不起来。

第 1 步:装好依赖,然后用模拟输入跑

which jq || echo "jq is MISSING"

echo '{"model":{"display_name":"Opus 4.7"},"workspace":{"current_dir":"'"$PWD"'"},"context_window":{"used_percentage":25},"cost":{"total_cost_usd":0.12},"session_id":"test"}' \
  | ~/.claude/statusline.sh 1>/tmp/sl.out 2>/tmp/sl.err
echo "exit=$?"
echo "--- stdout ---"; cat /tmp/sl.out
echo "--- stderr ---"; cat /tmp/sl.err

如果缺 jq,装上(macOS 用 brew install jq,Debian/Ubuntu 用 apt-get install jq,Windows 用 winget install jqlang.jqchoco install jq)。对输出做三项检查:退出码 0、stdout 非空、stderr 安静。任何一项不过,先修脚本,别动 Claude Code。

第 2 步:把 stdin 当 JSON 读,而不是读环境变量

确保脚本做的第一件事就是消费 stdin,而且每个字段都来自 jq:

#!/usr/bin/env bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name // "unknown"')
dir=$(echo "$input" | jq -r '.workspace.current_dir // "?"')
branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")
printf "%s | %s | %s" "$model" "${dir##*/}" "$branch"
exit 0

要点:// "unknown" 这种 jq 兜底即使字段缺失也能让栏保持有内容;显式 exit 0 防止最后一条命令失败把脚本带挂;给 git-C "$dir",这样无论 Claude 在哪儿启动这个 shell 都能工作。

第 3 步:优雅处理 null 字段

// 0(数值)或 // empty(可选字段),让首次响应前的 null 永远不会把运算搞砸:

pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
five_h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')

rate_limits 只在 Claude.ai Pro/Max 会话首次 API 响应之后才出现,所以那里用 // empty 是对的。

第 4 步:接受工作区信任

如果看到 statusline skipped · restart to fix,说明该目录从没被信任过。在该目录里重启 Claude Code 并接受信任提示。同时确认 settings 里 disableAllHooks 不是 true,因为它会连带 hooks 一起把状态栏禁掉。

第 5 步:保持快 —— 给阻塞调用设上限并做缓存

把任何可能阻塞的命令包起来,绝对不要同步调网络 API:

branch=$(timeout 0.3s git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?")

对慢的 git 或远端数据,缓存到一个用 .session_id(单会话内稳定,不像 $$)命名的临时文件:

SESSION_ID=$(echo "$input" | jq -r '.session_id')
CACHE="/tmp/statusline-git-cache-$SESSION_ID"
if [ ! -f "$CACHE" ] || [ -n "$(find "$CACHE" -mmin +0.1 2>/dev/null)" ]; then
  git -C "$dir" branch --show-current 2>/dev/null > "$CACHE.tmp" && mv "$CACHE.tmp" "$CACHE"
fi
branch=$(cat "$CACHE" 2>/dev/null || echo "?")

由于更新有 300ms 去抖、且新触发会取消正在跑的那次,任何比几百毫秒更慢的东西都会给你一个过期的栏。

第 6 步:修转义序列输出

如果不需要颜色,把控制字符剥掉,什么都别漏出来:

printf "%s | %s" "$model" "$branch" | tr -d '\n\r\t'

如果确实想要颜色或可点击的 OSC 8 链接,跨 shell 时优先用 printf '%b' 而不是 echo -e,转义解释更可靠;并确认你的终端支持(比如 Terminal.app 就不渲染 OSC 8 链接)。多行栏方面,每个独立的 echo/print 按设计会成为单独一行。

第 7 步:修权限、shebang 和路径

chmod +x ~/.claude/statusline.sh
head -1 ~/.claude/statusline.sh    # 应当是 #!/usr/bin/env bash 或 #!/bin/bash

用解释器显式测一下;如果这样能跑、直接调脚本不行,就是 shebang 错了:

echo '{"model":{"display_name":"Opus 4.7"}}' | /usr/bin/env bash ~/.claude/statusline.sh

在 Windows 上,command 路径要用正斜杠(C:/Users/you/.claude/statusline.sh),因为 Git Bash 会把反斜杠当转义吃掉,命令就静默失败了。

如何确认已修好

  • 重启 Claude Code,栏应在几百毫秒内填满,并在你发出第一条消息后更新。
  • cd 进一个不是 git 仓库的目录,确认栏仍能渲染(你的 no-git 兜底路径有效)。
  • 断网,确认栏仍能渲染(缓存 / 不同步路径有效)。
  • 从好几个不同的 cwd 喂入模拟输入;每次都应退出 0、打印少于 200 字符、远低于 300ms 内完成。

长期预防

  • 脚本永远以 input=$(cat) 开头,每个值都通过 jq 从这段 JSON 取;绝不依赖环境变量。
  • 脚本保持短小(50 行以内)。再大就该独立成单独工具,由它写缓存文件。
  • 绝不同步调网络或 git fetch;后台刷新、读一个用 .session_id 命名的缓存即可。
  • 结尾防御性写 exit 0,每个 jq 取值都给 // 兜底
  • /tmp(无 git、无 node_modules)下用模拟 JSON 测 —— 它在那儿不应挂掉。
  • 脚本进 dotfiles 版本管理,按生产脚本一个标准对待。一个 CI 冒烟测试(喂模拟 JSON、断言退出 0、输出小于 200 字符、耗时低于 300ms)能早早抓住回归。

常见坑

  • $CLAUDE_MODEL / $CLAUDE_SESSION_ID 而不解析 stdin JSON —— 这些环境变量根本不存在。
  • statusLine 块里忘了写 "type": "command";不写这条整个配置会被忽略。
  • git status(递归、慢),而本可以用 git rev-parsegit branch --show-current(一次廉价调用)。
  • 在首次 API 响应前对 null 上下文字段做运算 —— 用 // 0 守住。
  • 在 Windows 的 command 串里硬编码 ~ 路径,或用了被 Git Bash 吃掉的反斜杠。
  • 让某个慢的第三方命令(docker pskubectl)卡住整条栏 —— 缓存它或直接去掉。
  • 如果问题本身出在环境注入,相关的配置加载问题见 Claude Code settings.json 未加载

FAQ

Q:手动跑脚本好用,在 Claude 里栏却空白,为什么?

多半是你交互式地跑它却没把 JSON 喂进去,于是它卡在 cat 上等 stdin,你没注意到而已。Claude 只通过 stdin 提供数据。测真实路径:echo '{"model":{"display_name":"Opus 4.7"}}' | ~/.claude/statusline.sh。再确认 jq 在 Claude 看到的 PATH 上,且你读的是 stdin 而不是环境变量。

Q:栏在启动后显示 --null

context_window 里好几个字段在首次 API 响应前(以及 /compact 之后那一下)是 null。加上像 .context_window.used_percentage // 0 这样的 jq 兜底。等你的第一条消息返回后这些值就填好了。

Q:我看到 statusline skipped · restart to fix,这是什么意思?

该目录没被授予工作区信任,而 statusLine 会运行一条 shell 命令,所以它跟 hooks 一样受信任门控。在该目录里重启 Claude Code 并接受信任提示。也检查一下 disableAllHooks 没被设成 true

Q:输出有最长限制吗?

没有硬上限,但栏跟系统通知(MCP 报错、自动更新、上下文低警告)共用一行,所以在窄终端上过长的输出会被截断。保持紧凑 —— 大致控制在 200 字符以内。

Q:状态栏能调用工具或读取 agent 状态吗?

不能。它就是一条 shell 命令,在 agent loop 之外运行,拿不到代理状态。要动态数据,跑一个后台进程往缓存文件写,然后状态栏读这个文件 —— 相关的阻塞调用模式见 Claude Code 工具执行卡死

Q:怎么让 Claude Code 帮我生成或删除脚本?

用自然语言描述跑 /statusline(例如 /statusline show model name and context percentage with a progress bar),它会把脚本和 settings 条目都写好。要删除,运行 /statusline delete,或从 settings.json 里删掉 statusLine 键。

标签: #Claude Code #statusline #排查 #configuration #scripting