📊 Claude Codeのステータスライン完全カスタム ― JSON駆動で残量・コスト・健全性を3行表示 — リーダー×
📊

Claude Codeのステータスライン完全カスタム ― JSON駆動で残量・コスト・健全性を3行表示

#claudecode#automation#shell2026-07-04 · 約9

「Claude Code環境」シリーズ。前回は Codexのworktree swarm でCodexとClaude Codeを協業させる構成を書いた。今回はもっと地味だが毎ターン目に入るもの ―― ステータスラインのカスタムの話です。

Claude Codeはウィンドウ下端に1行だけステータスを出せる。デフォルトはモデル名程度しか出ない。これをsettings.jsonstatusLine.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_percentage5hブロック使用率(0〜100)
.rate_limits.five_hour.resets_atリセット時刻(UNIX epoch)
.rate_limits.seven_day.used_percentage7日ブロック使用率
.rate_limits.seven_day.resets_at7日リセット時刻(UNIX epoch)
.context_window.used_percentageコンテキスト消費率(事前計算済み)
.cost.total_cost_usd現セッションコスト(USD)
.model.display_nameモデル名
.effort.leveleffortレベル

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分遅れるが、ステータスラインが固まることはない。tmpmvのアトミック書き換えで読み途中の破損も防ぐ。

健全性アイコン:サブプロセスゼロ読み出し

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.jsonstatusLine.commandに指定するだけでJSON stdinが毎ターン届く
  • rate_limitscontext_windowcost.total_cost_usdはAPIコールなしにstdinから直接読める
  • 色分けはpcolor()で50%・80%の二段しきい値
  • 重い処理(ccusage・health check)はキャッシュ+バックグラウンドに逃がす。同期パスはjqと文字列整形だけ
  • tmpmvのアトミック書き換えでキャッシュの読み壊しを防ぐ
  • 部品スクリプト(claude-limits-segment.sh)を別に切り出すと他の自動化から流用しやすい

次回は、このステータスラインに表示している自動化健全性チェック(automation-health.sh)の中身 ―― launchdジョブの死活・スクリプトのexit code・最終成功時刻を一覧する構成を書く予定です。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!