Claude Codeのステータスライン完全カスタム ― JSON駆動で残量・コスト・健全性を3行表示
「Claude Code環境」シリーズ。前回は Codexのworktree swarm でCodexとClaude Codeを協業させる構成を書いた。今回はもっと地味だが毎ターン目に入るもの ―― ステータスラインのカスタムの話です。
Claude Codeはウィンドウ下端に1行だけステータスを出せる。デフォルトはモデル名程度しか出ない。これをsettings.jsonのstatusLine.commandに任意のシェルスクリプトを指定する機能を使って、5h/7dのプラン残量・コンテキスト消費率・セッションコスト・自動化ヘルスを3行に整形した実装を全コード解説する。うれしいのは、残量数値がstdinから直接流れてくるのでAPIコールも認証も不要なこと。
困りごと:無人ジョブが暴走してもターミナルで気づけない
autopilot.shを夜間に回すようになって、翌朝「5hブロックの残量がゼロ、以降の全ジョブSKIP」という事故が繰り返した。問題は、コストや残量を確認するのに毎回ccusageを叩く必要があったこと。確認が手間なので確認しなくなり、気づくのが遅れる。
理想は「画面を見れば今のプラン消費率がわかる」状態。Claude Codeのステータスラインにそのまま出せればターミナルを開く必要もない。
settings.jsonの設定:JSON駆動の仕組み
~/.claude/settings.jsonの269〜274行目がこれだけ:
"statusLine": {
"type": "command",
"command": "~/.claude/scripts/statusline.sh",
"padding": 0
}
type: "command" にすると、毎ターンClaude Codeがそのコマンドを起動してstdinにJSONを流し込む。標準出力が逐語的にステータスラインに表示される。
このstdinには何が入っているか。実際のフィールドは:
| パス | 内容 |
|---|---|
.rate_limits.five_hour.used_percentage | 5hブロック使用率(0〜100) |
.rate_limits.five_hour.resets_at | リセット時刻(UNIX epoch) |
.rate_limits.seven_day.used_percentage | 7日ブロック使用率 |
.rate_limits.seven_day.resets_at | 7日リセット時刻(UNIX epoch) |
.context_window.used_percentage | コンテキスト消費率(事前計算済み) |
.cost.total_cost_usd | 現セッションコスト(USD) |
.model.display_name | モデル名 |
.effort.level | effortレベル |
rate_limitsはサブスクリプション会員の初回API応答後にのみ出現する。最初のターン前は空なので、スクリプトは--にフォールバックする必要がある。
statusline.sh:stdin読み取りとフォールバック
スクリプトの先頭でstdinを読み、JSONとして有効なら/tmp/cc-statusline-last.jsonにキャッシュする。
RAW_INPUT="$(cat 2>/dev/null || true)"
if printf '%s' "$RAW_INPUT" | jq -e 'type == "object" and length > 0' >/dev/null 2>&1; then
INPUT="$RAW_INPUT"
printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || true
elif [ -s /tmp/cc-statusline-last.json ]; then
INPUT="$(cat /tmp/cc-statusline-last.json 2>/dev/null || echo '{}')"
else
INPUT='{}'
fi
stdinが空(他のツールから参照される場合など)でもキャッシュから読み返せる。claude-limits-segment.shはこのキャッシュパスをCLAUDE_STATUSLINE_JSON環境変数で受け取って使う構成にしている:
STATUS_JSON="${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}"
jqの呼び出しをヘルパー関数に隠す:
j() { printf '%s' "$INPUT" | jq -r "$1" 2>/dev/null; }
プラン残量とコンテキストの取得はこれだけ:
CTX=$(j '.context_window.used_percentage // empty')
H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')
D7=$(j '.rate_limits.seven_day.used_percentage // empty')
D7R=$(j '.rate_limits.seven_day.resets_at // empty')
SESSION_USD=$(j '.cost.total_cost_usd // 0')
色分け:50%・80%の二段しきい値
pcolor()が数値をANSIカラーに変換する:
pcolor() { local n="${1%%.*}"; [ -z "$n" ] && { printf '%s' "$DIM"; return; }
if [ "$n" -ge 80 ] 2>/dev/null; then printf '%s' "$RED"
elif [ "$n" -ge 50 ] 2>/dev/null; then printf '%s' "$YEL"
else printf '%s' "$GRN"; fi; }
- 50%未満 → 緑(余裕)
- 50〜79% → 黄(注意)
- 80%以上 → 赤(危険水位)
これをコンテキスト・5h・7d の3つに適用する:
CTXC=$(pcolor "$CTX")
C5=$(pcolor "$H5")
C7=$(pcolor "$D7")
3行の組み立て
スクリプトコメントの仕様そのままが最終出力になっている:
line1: 📁 dir ⌥ branch
line2: 🤖 model·effort 🧠 ctx% 🕐 5h N% ⏪reset 📅 7d N% ⏪reset
line3: 💴 session ≈¥x 今日 ≈¥y <health>
コードで:
L1=$(printf "${DIM}📁${R} ${CYAN}%s${R}%s%s" "$DIRNAME" "$BR" "$WT")
L2=$(printf "${MAG}🤖 %s%s${R} ${DIM}·${R} ${CTXC}🧠 ctx %s${R} ${DIM}·${R} ${C5}🕐 5h %s${R}%b ${DIM}·${R} ${C7}📅 7d %s${R}%b" \
"$MODEL" "$EFF" "$(pct "$CTX")" "$(pct "$H5")" "$R5" "$(pct "$D7")" "$R7")
L3=$(printf "${DIM}💴 session${R} %s ${DIM}今日${R} %s %s%b" \
"$(yen "$SESSION_USD")" "$(yen "$DAY_USD")" "$HEALTH" "$PROJ_HEALTH_SEG")
printf "%b\n%b\n%b" "$L1" "$L2" "$L3"
リセット時刻はdate -rでepochから変換する。5hはHH:MM、7dはM/D HH:MM(曜日をまたぐため):
clk() { [ -n "$1" ] && date -r "$1" "+%H:%M" 2>/dev/null; }
mdhm() { [ -n "$1" ] && date -r "$1" "+%-m/%-d %H:%M" 2>/dev/null; }
今日のコスト:2分キャッシュ+バックグラウンド更新
5hブロック残量はstdinから直接取れるが、今日の総コストはccusage dailyという外部コマンドが必要。毎ターン呼ぶと体感で遅くなるため、2分キャッシュ+バックグラウンド更新にする:
NEED=true
if [ -f "$DAY_CACHE" ]; then
AGE=$(($(date +%s) - $(stat -f %m "$DAY_CACHE" 2>/dev/null || echo 0)))
[ "$AGE" -lt 120 ] && NEED=false
fi
[ "$NEED" = true ] && ( "$CCUSAGE" daily --json 2>/dev/null > "${DAY_CACHE}.tmp" && mv "${DAY_CACHE}.tmp" "$DAY_CACHE" ) &
&でバックグラウンド起動し、フォアグラウンドはキャッシュから即座に読む。表示は最大2分遅れるが、ステータスラインが固まることはない。tmp→mvのアトミック書き換えで読み途中の破損も防ぐ。
健全性アイコン:サブプロセスゼロ読み出し
automation-health.shはlaunchdジョブの成否を全件チェックするため、起動コストが重い。毎ターン呼ぶと体感遅延が出る。解決策は5分キャッシュ+バックグラウンド更新の同パターン:
HEALTH_CACHE="/tmp/cc-health-cache"; HEALTH=""
if [ -f "$HEALTH_CACHE" ]; then
HAGE=$(($(date +%s) - $(stat -f %m "$HEALTH_CACHE" 2>/dev/null || echo 0)))
[ "$HAGE" -lt 300 ] && HEALTH=$(cat "$HEALTH_CACHE" 2>/dev/null)
fi
if [ -z "$HEALTH" ]; then
( h=$(~/.claude/scripts/automation-health.sh 2>&1 | grep -oE "ALL GREEN|WARN|FAIL" | head -1)
case "$h" in
"ALL GREEN") echo "🟢" > "$HEALTH_CACHE" ;;
"WARN") echo "🟡" > "$HEALTH_CACHE" ;;
"FAIL") echo "🔴" > "$HEALTH_CACHE" ;;
*) echo "⚫" > "$HEALTH_CACHE" ;;
esac ) &
HEALTH="·" # 更新中は中点で表示
fi
キャッシュがある間はファイルをcatするだけ。サブプロセスはゼロ。期限切れなら&でバックグラウンドに投げ、その間は·(中点)を表示する。次ターンにはキャッシュが更新されてアイコンに切り替わる。
project-health-latest.md(プロジェクト健全性レポート)も同じ思想で、ファイルを読むだけで完結させる:
PH_FILE="$HOME/.claude/logs/project-health-latest.md"
PH_WARN=$(grep -oE '^## Warnings \([0-9]+\)' "$PH_FILE" 2>/dev/null | grep -oE '[0-9]+' | head -1)
grepで警告件数を拾い、0なら🟢、あれば🟡/🔴にする。外部プロセスを起動しない。
「ステータスラインのスクリプトは毎ターン呼ばれる」。起動コストは積み上がる。ルールは同期でやることは
jqとANSI整形だけ、それ以外は全部キャッシュかバックグラウンドに逃がす。
claude-limits-segment.sh:他ツールへの部品切り出し
statusline.shとは別に、プラン残量だけを返す小さなスクリプトも持つ。autopilot.shなどが「今プラン余裕あるか」を確認する用途で使う:
STATUS_JSON="${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}"
h5=$(jq_field '.rate_limits.five_hour.used_percentage')
h5r=$(jq_field '.rate_limits.five_hour.resets_at')
d7=$(jq_field '.rate_limits.seven_day.used_percentage')
d7r=$(jq_field '.rate_limits.seven_day.resets_at')
stale=''
[ "$age" -gt 600 ] && stale='*' # 10分以上古いキャッシュに印をつける
printf '🕐 5h %s' "$(pct "$h5")"
[ -n "$r5" ] && printf ' ⏪%s' "$r5"
printf ' · 📅 7d %s' "$(pct "$d7")"
[ -n "$r7" ] && printf ' ⏪%s' "$r7"
printf '%s' "$stale"
statusline.shが書いた/tmp/cc-statusline-last.jsonをそのまま参照する。キャッシュが10分以上古ければ末尾に*を付けてスタールを可視化する。
踏んだ落とし穴
rate_limitsが常に空に見える → 初回ターン前は本当に存在しない。// emptyでnullをフォールバックしないとjqが空文字を返しpcolorが死ぬ- リセット時刻がローカル変換できない → macOSの
date -rはBSD版でLinuxのdate -d @epochと構文が違う。stat -f %mも同様。Linuxで使う場合は書き換えが要る - 毎ターン
ccusageを呼んでターミナルが固まる → 2分キャッシュ+バックグラウンド更新で解決。tmpファイル経由のアトミック書き換えも必須 automation-health.shの起動が重くてラグが出る → 5分キャッシュ+バックグラウンドに逃がす。更新中は·を見せる- ANSI色コードが出力に干渉するターミナルがある → ステータスライン出力に
printf "%b"を使うことでエスケープを正しく展開する。echo -eは移植性がない - worktree内でgit branchが取れない →
git -C "$REAL_CWD"で作業ディレクトリを明示しないとメインのrepoを参照してしまう
まとめ
settings.jsonのstatusLine.commandに指定するだけでJSON stdinが毎ターン届くrate_limits・context_window・cost.total_cost_usdはAPIコールなしにstdinから直接読める- 色分けは
pcolor()で50%・80%の二段しきい値 - 重い処理(ccusage・health check)はキャッシュ+バックグラウンドに逃がす。同期パスは
jqと文字列整形だけ tmp→mvのアトミック書き換えでキャッシュの読み壊しを防ぐ- 部品スクリプト(
claude-limits-segment.sh)を別に切り出すと他の自動化から流用しやすい
次回は、このステータスラインに表示している自動化健全性チェック(automation-health.sh)の中身 ―― launchdジョブの死活・スクリプトのexit code・最終成功時刻を一覧する構成を書く予定です。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!