Skip to content

从"能跑"到"好修":我的 Claude Code statusLine 脚本第二次进化 ​

两个月前,我写了第一版 statusLine 脚本。

那时候的需求很直接:能在 Claude Code 的状态栏看到当前目录、模型和 token 用量就够了。代码是一次性写出来的,能用,但说实话——所有逻辑都堆在 main() 里,想改一个展示格式要翻半天代码。

上周我重写了它。

不是因为我闲着没事,而是因为我在修一个 bug 时发现:想加一个"把环境变量也拍个快照"的功能,居然要在主流程里插三行代码。我觉得这不对。

这篇文章不讲 hooks 是什么、statusLine 怎么配——这些我在之前的文章里已经拆过了。这次只讲一件事:这段新代码是怎么设计的,为什么这么设计,以及它比第一版好在哪里。

先看新代码 ​

这是现在跑在我 Claude Code 底栏的脚本:

python
# /// 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. 导入与常量——这次多了两样东西 ​

python
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() 的类型守卫中。

两个路径常量:

python
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()——捍卫类型安全 ​

python
def is_number(value: object) -> TypeGuard[int | float]:
    return isinstance(value, int | float) and not isinstance(value, bool)

一个小函数,但解决了第一版里的一个隐含问题。

第一版直接从 payload 取字段时,假设 total_input_tokens 一定是个 int:

python
input_tokens = context_window["total_input_tokens"]

如果 Claude Code 改了 payload 结构,或者某个字段变成了 None 或 str,脚本就直接崩了。

新版本用 is_number() 做守卫:

python
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()——数字缩写的细节打磨 ​

python
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:去尾零

python
text.rstrip("0").rstrip(".")

150000 → 1.2M,而不是 1.20M。省一个字符也是省,在状态栏这种寸土寸金的地方很有价值。

4. 两个持久化函数——为排错铺路 ​

python
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() 只负责协调流程和输出。

python
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 展示的信息有调整:

  1. 模型名:从 model.id 变为 model.display_name。model.id 是 claude-sonnet-4-6 这种标识符,display_name 是 Claude Sonnet 4.6 Addendum 这种可读名称。在状态栏里,可读性比精确 ID 更重要。

  2. 目录信息:不再展示工作目录。我的终端提示符和 tmux pane 标题已经显示了路径,不需要在 statusLine 里重复。

  3. Token 信息:输入/输出分别展示,格式为 Input 1.2K / Output 3.4K,比中文标签更紧凑。

  4. 新增窗口信息:当 Claude Code 提供了 used_percentage 和 context_window_size 时,展示如 window[200k] 45%。这是第一版没有的——它用百分比告诉你上下文窗口用了多少,搭配窗口总量让你判断是否接近上限。

payload.get() vs payload["key"]

第一版直接使用 payload["key"] 访问字段,如果字段不存在会抛出 KeyError。新版全部改用 .get(),配合类型检查:

python
model = payload.get("model")
if type(model) is dict:
    display_name = model.get("display_name")

这是"防御式编程"——不假设 payload 结构一定如预期,而是先检查再使用。对于 Claude Code 这种还在快速演进的工具来说,这个习惯能省很多排错时间。

6. main()——只做编排 ​

python
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")

主流程非常清晰:

  1. 读 stdin
  2. 拍环境变量快照
  3. 解析 JSON
  4. 拍输入快照
  5. 防御检查:如果 payload 不是 dict,直接结束
  6. 构建展示内容
  7. 如果有内容,用 | 拼成一行输出

注意 write_env_snapshot() 在 JSON 解析之前执行。这意味着即使 JSON 解析失败,环境变量快照也已经保存了。排错时可以回答"这次运行的环境到底是什么"。

还有一个小设计:if not parts: return 而不是输出空行。如果所有字段都缺失或者类型不对,状态栏就保持空白,不输出无意义的分隔符。

跟第一版比,到底改了什么? ​

方面第一版第二版
字段访问payload["key"]payload.get("key") + 类型检查
类型守卫无is_number() + TypeGuard
token 缩写固定大写 + 两位小数可切换大小写 + 去尾零
展示内容目录、模型 ID、token模型展示名、token、窗口百分比
架构一切在 main()分离为持久化、展示、编排三层
落盘只落 JSONJSON + 环境变量
快照格式原始二进制格式化 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——而是因为当初为"排错"铺的路(类型守卫、快照、分层)恰好覆盖了所有我踩过和可能踩的坑。

这正是我想在这篇文章里记录的东西:不是一段代码,而是一种从使用中学会设计的过程。

Nine and AI — Licensed under the MIT License