从"能跑"到"好修":我的 Claude Code statusLine 脚本第二次进化
两个月前,我写了第一版 statusLine 脚本。
那时候的需求很直接:能在 Claude Code 的状态栏看到当前目录、模型和 token 用量就够了。代码是一次性写出来的,能用,但说实话——所有逻辑都堆在 main() 里,想改一个展示格式要翻半天代码。
上周我重写了它。
不是因为我闲着没事,而是因为我在修一个 bug 时发现:想加一个"把环境变量也拍个快照"的功能,居然要在主流程里插三行代码。我觉得这不对。
这篇文章不讲 hooks 是什么、statusLine 怎么配——这些我在之前的文章里已经拆过了。这次只讲一件事:这段新代码是怎么设计的,为什么这么设计,以及它比第一版好在哪里。
先看新代码
这是现在跑在我 Claude Code 底栏的脚本:
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
import json
import os
import shlex
import sys
from pathlib import Path
from typing import TypeGuard
STATUS_INPUT_PATH = Path("/tmp/9999-claude-statusLine.json")
ENV_OUTPUT_PATH = Path("/tmp/9999-claude.env")
def is_number(value: object) -> TypeGuard[int | float]:
return isinstance(value, int | float) and not isinstance(value, bool)
def compact(value: int | float, lower: bool = False) -> str:
if value >= 1_000_000:
text = f"{value / 1_000_000:.2f}"
return text.rstrip("0").rstrip(".") + "m" if lower else text + "M"
if value >= 1_000:
text = f"{value / 1_000:.1f}" if lower else f"{value / 1_000:.2f}"
return text.rstrip("0").rstrip(".") + "k" if lower else text + "K"
return str(value)
def write_status_snapshot(payload):
STATUS_INPUT_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def write_env_snapshot():
lines = [f"{key}={shlex.quote(value)}" for key, value in sorted(os.environ.items())]
ENV_OUTPUT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
def build_status_parts(payload):
parts = []
model = payload.get("model")
if type(model) is dict:
display_name = model.get("display_name")
if type(display_name) is str and display_name:
parts.append(display_name)
context = payload.get("context_window")
if type(context) is dict:
input_tokens = context.get("total_input_tokens")
output_tokens = context.get("total_output_tokens")
if not is_number(input_tokens):
input_tokens = None
if not is_number(output_tokens):
output_tokens = None
if input_tokens and output_tokens:
parts.append(
f"Input {compact(input_tokens)} / Output {compact(output_tokens)}"
)
elif input_tokens:
parts.append(f"Input {compact(input_tokens)}")
elif output_tokens:
parts.append(f"Output {compact(output_tokens)}")
used_percentage = context.get("used_percentage")
if is_number(used_percentage):
window_size = context.get("context_window_size")
if is_number(window_size) and window_size > 0:
parts.append(
f"window[{compact(window_size, True)}] {used_percentage:.0f}%"
)
else:
parts.append(f"window {used_percentage:.0f}%")
return parts
def main():
data = sys.stdin.buffer.read()
write_env_snapshot()
payload = json.loads(data)
write_status_snapshot(payload)
if type(payload) is not dict:
return
parts = build_status_parts(payload)
if parts:
sys.stdout.write(" | ".join(parts) + "\n")
if __name__ == "__main__":
main()看一遍感觉:不复杂,但和第一版已经不太像同一段代码了。
拆开来看
1. 导入与常量——这次多了两样东西
import shlex
from pathlib import Path
from typing import TypeGuard第一版里所有文件操作都是用 open() + 字符串路径,这次换成了 Path.write_text()。原因很简单:后面 write_status_snapshot() 和 write_env_snapshot() 都是"写文件"一个动作,Path 对象可以直接 .write_text(),省去 with open() as f: 的样板代码。
shlex 是为了给环境变量的值做 shell-safe 引用——后面会看到。
TypeGuard 是 Python 3.10+ 引入的类型 narrowing 工具,用在 is_number() 的类型守卫中。
两个路径常量:
STATUS_INPUT_PATH = Path("/tmp/9999-claude-statusLine.json")
ENV_OUTPUT_PATH = Path("/tmp/9999-claude.env")第一版只落盘了一份 stdin 输入快照(/tmp/claude-statusLine-9999.json),现在又多了一个环境变量快照。后面聊 write_env_snapshot() 时再展开为什么。
2. is_number()——捍卫类型安全
def is_number(value: object) -> TypeGuard[int | float]:
return isinstance(value, int | float) and not isinstance(value, bool)一个小函数,但解决了第一版里的一个隐含问题。
第一版直接从 payload 取字段时,假设 total_input_tokens 一定是个 int:
input_tokens = context_window["total_input_tokens"]如果 Claude Code 改了 payload 结构,或者某个字段变成了 None 或 str,脚本就直接崩了。
新版本用 is_number() 做守卫:
input_tokens = context.get("total_input_tokens")
output_tokens = context.get("total_output_tokens")
if not is_number(input_tokens):
input_tokens = None这样即使字段缺失或类型不对,也只是显示不了对应信息,不崩。
TypeGuard[int | float] 的作用是告诉类型检查器:如果 is_number(x) 返回 True,那 x 就一定是 int | float。后续用 compact(input_tokens) 时,类型系统不会报错。
还顺手排除了 bool:在 Python 里 isinstance(True, int) 是 True——Bool 是 int 的子类,但 True/False 不应被当作数字处理。
3. compact()——数字缩写的细节打磨
def compact(value: int | float, lower: bool = False) -> str:
if value >= 1_000_000:
text = f"{value / 1_000_000:.2f}"
return text.rstrip("0").rstrip(".") + "m" if lower else text + "M"
if value >= 1_000:
text = f"{value / 1_000:.1f}" if lower else f"{value / 1_000:.2f}"
return text.rstrip("0").rstrip(".") + "k" if lower else text + "K"
return str(value)和第一版相比,这次做了三个改进:
改进 1:lower 参数
第一版只有大写后缀(1.23K、2.83M)。新版支持 lower=True 时输出小写(1.23k、2.83m)。这是为了 context_window_size 的显示——它在第二部分展示时用小写后缀,视觉上区分"总量"和"当前用量"。
改进 2:小数精度分离
- 百万级:固定两位小数(
.2f),不区分大小写 - 千级:大写时两位小数,小写时一位小数
这个差异不是随手选的。看实际场景:
compact(128000)→128.00K(展示 input/output tokens,整齐)compact(200000)→200k(lower=True,展示 window_size,不占空间)
改进 3:去尾零
text.rstrip("0").rstrip(".")150000 → 1.2M,而不是 1.20M。省一个字符也是省,在状态栏这种寸土寸金的地方很有价值。
4. 两个持久化函数——为排错铺路
def write_status_snapshot(payload):
STATUS_INPUT_PATH.write_text(
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
def write_env_snapshot():
lines = [f"{key}={shlex.quote(value)}" for key, value in sorted(os.environ.items())]
ENV_OUTPUT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")第一版也做落盘,但用的是 open() + write()。这次用 Path.write_text() 一行搞定。
write_status_snapshot() 比第一版多做了两件事:
ensure_ascii=False:中文不会被转义成\uxxxx,直接可读sort_keys=True:JSON 字段按字母排序,diff 时更容易对比
write_env_snapshot() 是全新的。它在脚本一启动就执行,把当前所有环境变量拍成快照:
ANTHROPIC_BASE_URL=...
CLAUDE_CODE_PROJECT_ROOT=/home/forty-wsl/...
PATH=/usr/local/bin:...用 shlex.quote() 处理值,确保值里有空格或特殊字符时也能被 shell 正确解析。
为什么需要这个?因为 Claude Code 的行为有时受环境变量影响(代理、API URL、项目路径等),而排查问题时你往往不确定环境变量到底是什么。拍一份快照在 /tmp/ 里,出问题了直接 diff 两次运行之间的环境变化。
5. build_status_parts()——展示与逻辑分离
这是和第一版最大的区别。
第一版的 main() 里同时做了三件事:读输入、组装展示文本、输出。新版把它拆成了两部分——build_status_parts() 只负责"从 payload 里提取需要展示的内容,拼成一个列表",main() 只负责协调流程和输出。
def build_status_parts(payload):
parts = []
model = payload.get("model")
if type(model) is dict:
display_name = model.get("display_name")
if type(display_name) is str and display_name:
parts.append(display_name)
context = payload.get("context_window")
if type(context) is dict:
input_tokens = context.get("total_input_tokens")
output_tokens = context.get("total_output_tokens")
...
used_percentage = context.get("used_percentage")
if is_number(used_percentage):
window_size = context.get("context_window_size")
...
parts.append(
f"window[{compact(window_size, True)}] {used_percentage:.0f}%"
)
return parts这个函数返回字符串列表,最终由 main() 用 " | " 拼成一行输出。
展示内容变化
和第一版相比,新版 statusLine 展示的信息有调整:
模型名:从
model.id变为model.display_name。model.id是claude-sonnet-4-6这种标识符,display_name是Claude Sonnet 4.6 Addendum这种可读名称。在状态栏里,可读性比精确 ID 更重要。目录信息:不再展示工作目录。我的终端提示符和 tmux pane 标题已经显示了路径,不需要在 statusLine 里重复。
Token 信息:输入/输出分别展示,格式为
Input 1.2K / Output 3.4K,比中文标签更紧凑。新增窗口信息:当 Claude Code 提供了
used_percentage和context_window_size时,展示如window[200k] 45%。这是第一版没有的——它用百分比告诉你上下文窗口用了多少,搭配窗口总量让你判断是否接近上限。
payload.get() vs payload["key"]
第一版直接使用 payload["key"] 访问字段,如果字段不存在会抛出 KeyError。新版全部改用 .get(),配合类型检查:
model = payload.get("model")
if type(model) is dict:
display_name = model.get("display_name")这是"防御式编程"——不假设 payload 结构一定如预期,而是先检查再使用。对于 Claude Code 这种还在快速演进的工具来说,这个习惯能省很多排错时间。
6. main()——只做编排
def main():
data = sys.stdin.buffer.read()
write_env_snapshot()
payload = json.loads(data)
write_status_snapshot(payload)
if type(payload) is not dict:
return
parts = build_status_parts(payload)
if parts:
sys.stdout.write(" | ".join(parts) + "\n")主流程非常清晰:
- 读 stdin
- 拍环境变量快照
- 解析 JSON
- 拍输入快照
- 防御检查:如果 payload 不是 dict,直接结束
- 构建展示内容
- 如果有内容,用
|拼成一行输出
注意 write_env_snapshot() 在 JSON 解析之前执行。这意味着即使 JSON 解析失败,环境变量快照也已经保存了。排错时可以回答"这次运行的环境到底是什么"。
还有一个小设计:if not parts: return 而不是输出空行。如果所有字段都缺失或者类型不对,状态栏就保持空白,不输出无意义的分隔符。
跟第一版比,到底改了什么?
| 方面 | 第一版 | 第二版 |
|---|---|---|
| 字段访问 | payload["key"] | payload.get("key") + 类型检查 |
| 类型守卫 | 无 | is_number() + TypeGuard |
| token 缩写 | 固定大写 + 两位小数 | 可切换大小写 + 去尾零 |
| 展示内容 | 目录、模型 ID、token | 模型展示名、token、窗口百分比 |
| 架构 | 一切在 main() | 分离为持久化、展示、编排三层 |
| 落盘 | 只落 JSON | JSON + 环境变量 |
| 快照格式 | 原始二进制 | 格式化 JSON(排序 + 中文不转义) |
| 环境变量 | 无 | shlex.quote() 全量快照 |
这条 statusLine 现在长什么样
当一切正常运行时,状态栏显示类似:
Claude Sonnet 4.6 Addendum | Input 1.2K / Output 3.4K | window[200k] 45%全部拼在一行,用 | 分隔。如果某个信息源不可用(比如 context_window 没有返回 used_percentage),那一部分就自动隐藏,不影响其他内容展示。
一些我觉得值得分享的思考
分层不是为扩展性,是为改得动
这可能是重构时最重要的认知转变。
第一版我写的时候想的是"怎么尽快拿到我要的信息"。第二版我想的是"如果三个月后我要改一个展示字段,我要翻几处代码"。
两版都 work,但第二版让我更敢改代码——因为我知道每个函数只负责一件事,改了 compact() 不会影响 write_env_snapshot()。
TypeGuard 值得多用
Python 的类型系统一直被诟病"可加可不加",但在这种数据管道脚本里,TypeGuard 的价值特别明显——它让函数既做运行时检查,又做了编译时类型 narrowing。一个函数解决两个问题。
快照是性价比最高的排错手段
往 /tmp/ 写一份 JSON 和环境变量,代码加起来不到十行,但它覆盖了 80% 的运行时排查场景。这种"写文件"的排错方式虽然不酷,但非常可靠——不依赖日志系统、不依赖网络、不依赖特定工具。
结语
这段脚本不算长,90 多行。但它是我在"真实用"和"好维护"之间找到一个平衡点的结果。
我在第一版跑了一个多月,改过三次:第一次改输出格式,第二次加 host 显示,第三次发现 token 字段有时候是 None 会崩。每次改都只加代码不重构,直到我意识到:该重新想一想了。
于是有了这一版。
重写之后到现在又跑了三周,一次没修过。不是因为没 bug——而是因为当初为"排错"铺的路(类型守卫、快照、分层)恰好覆盖了所有我踩过和可能踩的坑。
这正是我想在这篇文章里记录的东西:不是一段代码,而是一种从使用中学会设计的过程。