0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

0
Posted at

「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_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分遅れるが、ステータスラインが固まることはない。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サービスを量産しています

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?