Claude Code 用久了你迟早会遇到一个场景主任务里并行派出三四个 Subagent各自读仓库、写测试、跑静态检查终端只显示主对话流根本看不出每个子代理到底在跑还是在挂。为了看清楚这些状态我折腾了一个自定义状态栏工具把 Claude Code 的会话日志和 Hooks 事件串起来在 tmux 底部实时展示 Subagent 状态。这篇文章没有任何玄学成分就是一套可以照着装的工具以及我在实际配置和调试中踩过、爬出来的坑。这套方案适合谁如果你每天要用 Claude Code 做多文件改动、并行跑子任务并且已经能把 tmux 当成日常终端环境那下面的状态栏工具基本能帮你省掉一半“刷新日志猜进度”的时间。如果你是第一次听说 Subagent也能按步骤装完顺便理解 Claude Code 的本地日志是怎么工作的。1. 为什么需要自定义状态栏Subagent 状态到底去哪了1.1 默认界面下的“状态盲区”Claude Code 的终端界面做得并不差主会话的文本流、工具调用结果都会正常打印。但有一个明显的盲区当主 Agent 把任务拆给 Subagent 之后你只能在对话框里看到一句“正在运行子代理”然后就没有然后了。它到底在等上下文在读文件还是在某个函数里来回绕默认界面不会给你一个实时状态面板。我刚开始跑并行任务时是这样工作的先开一个终端跑 Claude Code然后另开一个终端用tail -f盯着~/.claude/projects/下的 JSONL 日志。出问题就 CtrlC切过去翻最后几十行。这样做不是不行但非常累尤其同时有三四个 Subagent 时日志交错在一起光靠肉眼根本分不清哪个子任务已经结束、哪个还卡着。后来我意识到需要一个独立于 Claude Code 输出流的状态展示层把“谁在跑、跑了几个、有没有失败”这些信息压缩成一行固定在终端底边。1.2 设计目标日志驱动、不改内部、一行文本搞定动手之前我先定了三条原则。第一不侵入 Claude Code 本身。我不想去改它的配置来强行塞 UI也不想用屏幕抓取的方式去读输出太脆。最可靠的数据源是它每次会话都会落盘的 JSONL 日志解析日志就能拿到事件流。第二状态栏必须轻量。我不想在这个工具上引入 Node 服务、Python 常驻进程这类重东西最好就是一个 bash 脚本加一行 tmux 配置塞进status-left就能跑。第三允许 1 到 2 秒的延迟。Subagent 状态展示不是监控系统不需要毫秒级精确。只要事件发生之后状态栏能在几秒内反映出来就足够用。最终实现路径定下来是JSONL 日志解析 → 汇总计数 → 生成 tmux 状态栏文本 → 通过 Hooks 或轮询触发刷新。下面所有内容都是围绕这条链路展开的。2. 安装前准备与基础验证2.1 依赖清单与为什么选 tmux jq这个工具对外部依赖的要求很低我实际用的环境是 macOS tmux jqLinux 上同样能跑。先列一下要装的东西tmux 3.2 以上用来承载状态栏。tmux 的status-left支持#(...)命令替换可以在状态栏里执行脚本并显示输出。这是整个方案的地基。jq 1.6 以上用来解析 JSONL。Claude Code 的会话日志是一行一个 JSON 对象用 jq 处理比用 sed、grep 组合要稳得多。bash 4.0 以上脚本里用了关联数组低版本 bash 会直接报错。一个能跑 Claude Code 的环境最好已经跑过一次任务确保本地已经生成过会话日志。为什么不直接改 Claude Code 的 UI因为官方没有暴露稳定的状态面板定制接口与其去适配一个我控制不了的界面不如把状态栏放在 tmux 里它天然独立于任何 CLI 程序。只要 Claude Code 还在终端里跑tmux 状态栏就在终端最底部物理位置上永远不会被内容盖住。2.2 目录规划与安装步骤我把这个工具安装在用户目录下不碰系统目录方便回滚。目录结构大致是~/.local/share/cc-statusline/ ├── bin/ │ └── statusline.sh # 状态栏生成脚本被 tmux 周期调用 │ └── refresh.sh # 事件触发后的刷新脚本 ├── etc/ │ └── config.env # 状态栏颜色、阈值等配置 └── lib/ └── parser.sh # JSONL 解析逻辑安装时我建议先下载脚本检查内容再执行不要直接curl | bash。我自己维护了一个打包脚本但给别人用时一定会提醒先看内容再安装。安装的核心动作就是把上面这些文件放到~/.local/share/cc-statusline/然后给两个脚本加执行权限chmod x ~/.local/share/cc-statusline/bin/*.sh接着配置 tmux。在~/.tmux.conf里加set -g status-left #($HOME/.local/share/cc-statusline/bin/statusline.sh) set -g status-left-length 100 set -g status-interval 2status-interval 2表示每 2 秒调用一次状态栏脚本。调成 1 秒更实时但终端开销会明显上升。如果你后面接了 Hooks 做事件驱动刷新2 秒完全够。2.3 先确认 Claude Code 会话日志能读配置前最重要的一件事确认你自己机器上的日志路径和格式。Claude Code 的会话日志一般在~/.claude/projects/项目目录名/会话ID.jsonl项目目录名和你的实际项目路径有关不一定是纯项目名可能是经过 hash 的目录名。最笨但有效的确认方法ls -lt ~/.claude/projects/*/ | head -5看看最近有没有正在写入的.jsonl文件。找到之后取最后一行的 type 字段看日志到底长什么样tail -n 1 最新日志文件 | jq .type出现assistant、user、system这类字符串就说明日志格式正常。如果这一步读不到任何内容后面所有逻辑都白搭。不要跳过它我在好几个环境里都是卡在“日志目录权限不对”或“还没有生成过会话”这种最基础的问题上。3. 核心配置状态提取、渲染与实时刷新3.1 从 JSONL 中提取 Subagent 运行状态状态栏展示的核心是三个数字正在运行的 Subagent 数、已经完成的 Subagent 数、出错的 Subagent 数。这三个数字全部来自会话日志里的事件。Claude Code 的 JSONL 日志会记录消息类型、时间戳、消息内容和工具调用结果。Subagent 相关的状态通常可以从两类事件里提取主 Agent 发起一个任务型工具调用准备派生 Subagent某个 Subagent 返回结果或者带出错误信息。不同版本的事件字段名有差异所以我写了个parser.sh不直接依赖某个固定字段而是先把所有候选事件抽出来再做聚合。核心逻辑如下#!/usr/bin/env bash # lib/parser.sh parse_log() { local log_file$1 jq -r select(.type assistant or .type user) | .message.content[]? | select(.type tool_use or .type tool_result) | [.type, .name? // , .tool_use_id? // , .is_error? // false] | tsv $log_file | awk -F \t /tool_use/ { if ($2 Task) running_inc; next } /tool_result/ { if ($3 ! ) completed_inc; if ($4 true) failed_inc; next } END { printf %d %d %d, running_inc, completed_inc, failed_inc } }这段脚本的逻辑并不复杂但有一个关键点running_inc不是真正的“当前运行数”而是“启动过的 Subagent 累计数”。真正要显示“正在运行”需要用启动数减去完成数。我实际用的是关联数组按 tool_use_id 去重后面踩坑部分会细说。这里想多说一句Claude Code 的日志结构在不同版本里有变化脚本要做成“能容错”的而不是“字段硬编码”的。比如name字段可能叫Task也可能以后改成别的is_error可能缺失。所以我所有字段都用select(.name? Task)这类可选操作符避免某个字段缺失导致整个 jq 命令崩溃。3.2 生成状态栏文本并接入 tmux status-left拿到三个数字以后下一步就是生成显示在 tmux 底部的一行文本。tmux 的状态栏有自己的颜色语法不是 ANSI 转义序列这里容易踩坑。我用的格式是#!/usr/bin/env bash # bin/statusline.sh source $HOME/.local/share/cc-statusline/etc/config.env source $HOME/.local/share/cc-statusline/lib/parser.sh LATEST_LOG$(ls -t $HOME/.claude/projects/*/*.jsonl 2/dev/null | head -1) if [ -z $LATEST_LOG ]; then echo #[fgcolour244]Subagent: 无会话 exit 0 fi read -r RUNNING_RAW COMPLETED FAILED $(parse_log $LATEST_LOG) if [ -z $RUNNING_RAW ]; then RUNNING0 else RUNNING$(( RUNNING_RAW - COMPLETED )) [ $RUNNING -lt 0 ] RUNNING0 fi if [ $RUNNING -gt 0 ]; then COLOR#[fgcolour208] ICON● LABEL运行中 ${RUNNING} else COLOR#[fgcolour46] ICON● LABEL空闲 fi echo ${COLOR}${ICON} ${LABEL}#[default] #[fgcolour51]完成 ${COMPLETED}#[default] #[fgcolour196]失败 ${FAILED}#[default]这段脚本有几个细节。一是用ls -t找最新日志。如果你只开一个 Claude Code 会话这个方式没问题。多开会话时不够精确需要按当前目录匹配这一点我会在第 4 节展开。二是“正在运行”不是直接从日志里的某个状态字段读出来的而是用“已启动数减去已完成数”推算。因为 JSONL 里很少会直接写“Subagent 已结束”这样一个独立事件更多是看到工具结果返回才知道对应 Subagent 完成了。这算是个土办法但很有效。三是颜色用 tmux 的#[fgcolour208]语法。直接在脚本里写 ANSI\033[38;5;208m在很多终端里也能显示但跟 tmux 状态栏混在一起时会被当成普通字符吃掉或者不生效。统一用 tmux 官方颜色语法最稳。然后在~/.tmux.conf里让状态栏调用这个脚本。注意一定要用绝对路径别写成~tmux 在解析status-left时对~的展开行为有时会出问题。写成set -g status-left #($HOME/.local/share/cc-statusline/bin/statusline.sh)改完 tmux 配置后执行tmux source-file ~/.tmux.conf状态栏应该立刻出现一行● 空闲 完成 0 失败 0。如果没出现先手动在 shell 里执行一次bash ~/.local/share/cc-statusline/bin/statusline.sh直接看输出内容多半是脚本报错或者日志目录路径不对。3.3 用 Claude Code Hooks 精确触发刷新status-interval 2的轮询方案能工作但有两个缺点一是最长有 2 秒延迟二是即使没有事件发生脚本也还是会反复解析日志白白浪费 CPU。为了减少这种情况我给 Claude Code 配置了 Hooks让它在特定事件发生时主动触发状态栏刷新。Claude Code 支持通过 Hooks 在工具调用、用户提示等时机执行外部命令。这里我用的是PostToolUse匹配子代理派发工具。配置位置可以在项目目录下的.claude/settings.json也可以在用户级配置里。如果你只给特定项目加就放项目下如果是全局就放用户级。{ hooks: { PostToolUse: [ { matcher: Task, hooks: [ { type: command, command: $HOME/.local/share/cc-statusline/bin/refresh.sh } ] } ], UserPromptSubmit: [ { hooks: [ { type: command, command: $HOME/.local/share/cc-statusline/bin/refresh.sh } ] } ] } }refresh.sh的内容非常简单核心就是告诉 tmux 立刻重绘状态栏#!/usr/bin/env bash # bin/refresh.sh if [ -n $TMUX ]; then tmux refresh-client -S fitmux refresh-client -S的作用是刷新所有状态栏区域。这样每次用户输入、每个 Task 子代理工具返回时状态栏都会立刻更新一次不用等下一次轮询。这里要提醒一点Hooks 的 matcher 要精确。如果你把它设置成匹配所有工具调用状态栏脚本执行的频率会非常高反而会影响 Claude Code 本身的响应速度。我实际只匹配了Task因为子代理的创建和返回基本都跟这个工具有关。3.4 用事件缓存减少重复解析如果 Hooks 已经很灵敏轮询的现实意义就没那么大了。但我还是没有把轮询完全关掉因为 Hooks 只能覆盖 Claude Code 能感知到的事件有些后台日志写入、子代理内部状态变化不一定都会触发 Hook。所以我保留了 2 秒轮询同时做了一层缓存优化。优化思路很简单每次解析日志前检查日志文件的修改时间。如果mtime没有变化直接返回上一次的计算结果不再跑一遍 jq。这样大部分时间状态栏脚本的成本几乎为零。CACHE_FILE$HOME/.local/share/cc-statusline/etc/cache LAST_MTIME$(stat -f %m $LATEST_LOG 2/dev/null || stat -c %Y $LATEST_LOG 2/dev/null) CACHED_MTIME$(cat $CACHE_FILE 2/dev/null || echo 0) if [ $CACHED_MTIME $LAST_MTIME ]; then cat $CACHE_TEXT_FILE 2/dev/null || true exit 0 fi # 重新解析并写缓存 parse_log $LATEST_LOG $CACHE_TEXT_FILE echo $LAST_MTIME $CACHE_FILE cat $CACHE_TEXT_FILE注意 macOS 和 Linux 的stat参数不一样。macOS 用stat -f %mLinux 用stat -c %Y。我脚本里同时写了两种用||做兜底兼容两个平台。用轮询加 Hook 加缓存这套组合后状态栏延迟基本在 1 秒以内而且 CPU 占用可以忽略。4. 实际踩坑记录与排查速查4.1 状态栏纹丝不动最常遇到的问题是tmux 配置加了但状态栏就是不更新。先别怀疑脚本按下面顺序查。先手动跑一遍脚本看有没有输出。如果脚本输出正常但状态栏还是旧的问题出在status-left的命令替换语法上。检查 tmux 配置里是不是写了~写~很多时候不生效必须用$HOME。另外确认 tmux 版本老版本对动态状态栏支持很差升级到 3.2 以上再试。如果脚本没有输出大概率是source的路径不对或者 jq 没装。在脚本里加一行exec 2/tmp/cc-statusline.err把 stderr 重定向到文件然后运行一次看错误日志。我曾经因为lib/parser.sh里少了一个source路径导致整个脚本静默失败状态栏只显示一个空字符串。还有一个非常隐蔽的问题当你开了多个 tmux 窗口每个窗口都在跑 Claude Code状态栏脚本默认找的是~/.claude/projects/下最新写入的日志而不是当前窗口所在项目的日志。解决方法是把“查找日志”改成基于当前 pane 的工作目录去匹配项目。tmux 的status-left里可以用#{pane_current_path}拿到当前路径set -g status-left #($HOME/.local/share/cc-statusline/bin/statusline.sh #{pane_current_path})脚本里接收参数用当前路径过滤日志目录。具体匹配逻辑可能要看项目目录 hash 规则我的做法是在当前路径下反向找最近修改的.jsonlfind $HOME/.claude/projects -name *.jsonl -newer $HOME/.claude/projects \ -path *$(basename $1)* 2/dev/null | head -1这个方法不完全严谨但实测在多项目场景下命中率很高。如果以后官方改目录结构就再适配一次。4.2 统计数字虚高或归零慢Subagent 状态统计最容易出问题的点是重复计数。同一个 Subagent 在日志里可能有多次tool_use和多次tool_result如果每次都加一数字会越滚越大。我第一次写 parser 时就碰到这个情况明明只跑了两个 Subagent界面上显示“运行中 7 完成 9”。后来我改成按tool_use_id去重才把数字拉回正常。具体做法是用关联数组记录已经见过的 ID不要每次都累加。bash 关联数组在循环里追踪 ID 很顺手declare -A SEEN_RUN declare -A SEEN_DONE parse_log() { while IFS$\t read -r event_type tool_name tool_id is_error; do if [ $event_type tool_use ] [ $tool_name Task ]; then if [ -z ${SEEN_RUN[$tool_id]} ]; then SEEN_RUN[$tool_id]1 RUNNING_RAW$(( RUNNING_RAW 1 )) fi elif [ $event_type tool_result ]; then if [ -z ${SEEN_DONE[$tool_id]} ]; then SEEN_DONE[$tool_id]1 COMPLETED$(( COMPLETED 1 )) if [ $is_error true ]; then FAILED$(( FAILED 1 )) fi fi fi done (parse_log_raw) }注意 bash 关联数组在较老版本里不能直接声明为局部变量要用declare -A在函数内声明否则会报错。这个脚本只能 bash 写sh跑不了。另外“正在运行”的推算逻辑也容易被状态先后顺序搞乱。日志读取顺序如果按文件追加顺序读通常先有tool_use后有tool_result问题不大。但如果日志里混入了旧会话的残留或者一个会话存在多个.jsonl分片计数就会乱。我的建议是只统计当前最新一个.jsonl不要跨文件累加。4.3 多开项目时状态串台如果你只在一个项目下工作用最新日志定位没问题。但像我这种习惯把好几个项目客户端挂在同一个 tmux session 里的人状态栏会出现串台A 项目在跑任务B 项目窗口的底栏却显示了 A 项目的 Subagent 状态。解决办法已经在 4.1 里提到把#{pane_current_path}传给脚本。但这里还有一个细节Claude Code 的项目目录名和实际项目路径不是简单一一对应有些版本会把路径 hash 成一段 ID导致脚本无法靠basename匹配。我后来是拿当前路径的 basename 去grep所有日志路径如果多个匹配取修改时间最新的。find $HOME/.claude/projects -name *.jsonl -newermt -10 minutes 2/dev/null \ | grep $(basename $1) | head -1如果找不到再回退到全局最近修改日志。我在实际使用中加了这样一个降级策略优先基于当前路径找找不到再全局找这样就算目录结构变了也只是显示不准不会整个状态栏消失。4.4 颜色、转义和 tmux 语法混用刚开始我图省事在脚本里直接用 ANSI 颜色写状态栏比如echo \033[38;5;208m● 运行中\033[0m结果终端里出现一堆^[[38;5;208m状态栏整个被弄乱。原因是 tmux 状态栏的着色语法是#[fgcolour208]它有自己的解析规则不是标准 ANSI。后来我把所有输出都改成 tmux 风格echo #[fgcolour208]● 运行中 2#[default]记住这几个常见颜色绿色colour46、橙色colour208、红色colour196、蓝色colour51、灰色colour244。用 tmux 颜色命名可以保持跟状态栏主题一致。还有一个转义坑status-left里的#(...)命令替换如果脚本本身输出#[tmux 会优先解析有时候会导致内容被吞。遇到这类问题把脚本输出先写入文件再cat或者用printf %s\n $STATUS_TEXT而不是echo能减少一部分边界问题。4.5 一个自检清单这里把排查经验整理成一个表每次状态栏有问题就先过一遍现象检查点常见原因状态栏空白手动执行脚本看输出脚本路径、依赖缺失状态栏一直不变status-interval和 Hook 是否配置配置没 source或refresh-client没调数字虚高是否按 tool_use_id 去重重复计数多窗口串台脚本是否接收当前 pane 路径仍用全局最新日志颜色乱码是否用了 tmux#[fg...]语法误用 ANSI 转义状态栏卡顿日志是否过大、是否每 1 秒全量解析没做 mtime 缓存再补一句遇到状态栏不显示但脚本没问题优先怀疑status-left-length设置太短。我默认设了 100够显示三个计数项。如果你还要加额外信息建议至少 120。5. 一些关于后续扩展的思路状态栏跑稳之后我给它加了几个小功能都是基于同一套日志解析逻辑扩展的。比如显示当前活动的任务类型不只是 Task 工具还包括 Read、Edit、Bash 这类的最近调用。也可以在状态栏里显示日志文件大小快速判断会话是不是膨胀得厉害。另一个值得尝试的方向是把状态栏接到类 Unix 的桌面通知上。Claude Code 的 Hooks 能触发命令所以我已经把refresh.sh扩展成在 Subagent 全部完成时弹一个通知这样就不用一直盯着终端底部看了。这个扩展不需要额外依赖macOS 用osascript发通知Linux 用notify-send。不过我个人最推荐的还是先保持最小可用不要一上来就堆功能。状态栏的价值在于一眼能看清状态信息太多反而失去意义。跑一段时间根据你自己的使用习惯慢慢加比一开始就设计得花里胡哨要靠谱得多。