Claude Code statuslineの仕組み—JSON入力と自作スクリプトの勘所

Claude Code statuslineの仕組み—JSON入力と自作スクリプトの勘所 | mohablog

先日、自分の~/.claude/statusline.shの中身を見直していて「そもそもClaude Codeはこのスクリプトに何を渡してきているんだっけ?」と気になり、スクリプトの先頭でtee /tmp/sl-debug.jsonして標準入力を覗いてみました。出てきたJSONはmodelcwdの二つくらいだろうと舐めていたのに、実際にはコンテキスト残量・セッションコスト・rate limit・worktree情報まで詰め込まれていて、これだけのデータが流れているのに表示しているのはモデル名だけというのは、はっきり言ってもったいないなと感じました。

そこで公式ドキュメントのCustomize your status lineを読み直して、入力JSONの全フィールドと更新タイミング、自作スクリプトのハマりどころを整理し直したので、設定例と一緒にまとめておきます。検証はClaude Code v2.1.90(2026年5月時点の最新stable)で行いました。

目次

statuslineが受け取るJSONには何が入っているのか

公式ドキュメントのAvailable dataセクションを読むと、stdinに流れてくるJSONはトップレベルだけで20以上のキーがあります。代表的なものを拾ってみます。

基本フィールド: model / cwd / workspace

Claude Codeはセッションごとに、現在のモデル識別子・作業ディレクトリ・プロジェクトルートをstatusLineスクリプトに渡してきます。実際にcat | jq .を仕込んだスクリプトで観測したJSONを抜粋するとこんな具合です。

{
  "cwd": "/Users/moha/workspace/private/ops/wp-auto-poster",
  "session_id": "01HXG4P6Q7K3N8Z5J2W9MT4F6B",
  "transcript_path": "/Users/moha/.claude/projects/-Users-moha-.../transcript.jsonl",
  "model": { "id": "claude-opus-4-7", "display_name": "Opus" },
  "workspace": {
    "current_dir": "/Users/moha/workspace/private/ops/wp-auto-poster",
    "project_dir": "/Users/moha/workspace/private/ops/wp-auto-poster",
    "added_dirs": [],
    "git_worktree": "feature-statusline"
  },
  "version": "2.1.90",
  "output_style": { "name": "default" }
}
// jqで整形した一部抜粋(実際にはこの後にcost / context_window などが続く)

ちょっと意外だったのはcwdworkspace.current_dirが両方入っている点。値は同じですが、公式ドキュメントによればworkspace.project_dirと一貫させたいのでworkspace.current_dir側を使うのが推奨だそうです。workspace.git_worktreegit worktree addで作ったリンク先ディレクトリにいるときだけ現れるので、worktreeを多用する人はここを表示に組み込んでおくと、どのworktreeのセッションかひと目でわかって便利です。

context_window と current_usage の違い

context_windowオブジェクトにはtotal_input_tokensのような累積トークン数と、current_usageのような直近1回のAPIコールでのトークン数が同居しています。残コンテキストを正しく出したいならused_percentageを使うのが正解で、累積トークンから自前計算するとコンテキストウィンドウサイズを超えた値が返ってきて混乱します。

フィールド意味使いどころ
total_input_tokens / total_output_tokensセッション開始からの累積合計コスト計算
current_usage.input_tokens直近1回のAPI呼び出しの入力自前で%を計算するなら入力系の合算
used_percentage事前計算済みのコンテキスト使用率進捗バー表示。これを使うのが推奨
context_window_size200000、または1M拡張時は1000000%を自前計算する時の分母

公式ドキュメントのContext window fieldsセクションには「used_percentageinput_tokens + cache_creation_input_tokens + cache_read_input_tokensから算出されており、output_tokensは含まない」と明記されていて、自前で計算する場合もこの式に揃える必要があります。

条件付きで現れる隠れフィールド

地味に厄介なのが条件付きでしか現れないフィールド群です。jqで// empty?を付けないと、見えない時にスクリプトが落ちます。

  • rate_limits.five_hour / rate_limits.seven_day—Claude.ai Pro/Maxの契約者で、かつセッションで最初のAPIレスポンスが返った後でのみ出現
  • effort.level—現在のモデルがreasoning effortパラメータをサポートしているときだけ。low / medium / high / xhigh / maxの5段階
  • thinking.enabled—extended thinkingがオンのとき
  • worktree.*--worktreeセッション中のみ。workspace.git_worktreeと紛らわしいですが、こちらはClaude Code側のworktree機能用
  • agent.name--agentフラグまたはエージェント設定で起動した時のみ

サジェストでよく見る「rate_limits」を出したい人は、jq -r '.rate_limits.five_hour.used_percentage // empty'のように// emptyでフォールバックするのが安全です。

設定の2ルート—/statusline と settings.json

自然言語で済ませる /statusline コマンド

そもそもJSONの構造を覚えなくても、Claude Codeに/statuslineと打ってやりたいことを口頭で伝えれば、スクリプトを生成して~/.claude/settings.jsonまで書き換えてくれます。

/statusline モデル名とコンテキスト使用率をプログレスバーで表示して
✓ Created ~/.claude/statusline.sh
✓ Updated ~/.claude/settings.json
Status line will appear after the next interaction.

「コスト合計と5時間レート上限を一行で」「git branchと変更ファイル数を色付きで」のように指示を変えればその場で書き直してくれるので、雑な要件出しから始めて少しずつ育てていくスタイルでも問題ありません。

手書きする場合の statusLine 設定キー

自分でスクリプトを書いて配線する場合、~/.claude/settings.jsonに以下のフィールドを追加します。

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 2,
    "refreshInterval": 5,
    "hideVimModeIndicator": false
  }
}

各キーの意味を整理しておきます。

  • type—現状"command"のみ。シェルでcommandを実行する
  • command—スクリプトパス、またはインラインのシェルコマンド。jqを直書きする運用も可能
  • padding—左右の追加パディング(文字数)。デフォルト0。インターフェースのデフォルト余白に対しての追加分
  • refreshInterval—N秒ごとに再実行。最小値1。未指定ならイベントドリブンのみ
  • hideVimModeIndicator—プロンプト下の-- INSERT --表示を抑制。vim.modeを自分のスクリプトで描画する場合にtrueにする

インラインで書く場合はこんな具合に1行で済みます。

{
  "statusLine": {
    "type": "command",
    "command": "jq -r '\"[\\(.model.display_name)] \\(.context_window.used_percentage // 0)% context\"'"
  }
}
[Opus] 23% context

個人的には、最初はインラインで動かしてみて、ロジックが膨らんできたらスクリプトに切り出す流れが扱いやすかったです。

更新タイミング—イベント駆動と300msデバウンス

意外と知られていないのが、statusLineが定期的に更新されるわけではないという事実です。公式ドキュメントのHow status lines workセクションを読むと、再実行のトリガーは次の3つに限定されています。

  • 新しいassistant messageの到着後
  • permission modeの切り替え時
  • vim modeの切り替え時

さらに更新は300msでデバウンスされており、矢継ぎ早の変更はまとめて1回の実行に統合されます。実行中に新しいトリガーが発生した場合、走行中のスクリプトはキャンセルされて新しい方が走り直します。

アイドル中に止まる罠

ここで困るのが、サブエージェントを並列で動かしてメインセッションがアイドル待ちしているときです。アシスタントメッセージが来ないので、statusLineは固まったまま。git stateや時計表示が古くなっていきます。

このケースではrefreshIntervalを入れて時間ベースで再実行させるのが正解です。逆に表示が静的な情報だけなら未指定にしておくと、無駄なプロセスフォークを避けられます。

表示内容refreshInterval推奨値
モデル名・cwd・コンテキスト%のみ未指定(イベントだけで十分)
git branch / 変更ファイル数5〜10秒
時計・経過時間表示1〜2秒
サブエージェント並列実行を多用3〜5秒

重い処理を避けるキャッシュ戦略—session_idを使う

statusLineは頻繁に走るので、git statusgit diffのような遅めのコマンドをそのまま叩いていると、入力中にスクリプトが詰まって表示がカクつきます。公式ドキュメントのCache expensive operations例で示されているのが、tmpファイルキャッシュ+5秒の鮮度判定パターンです。

NG: プロセスIDをキャッシュキーに使う

真っ先にやりがちなのがこれ。

# NG: $$ は呼び出しごとに変わる
CACHE_FILE="/tmp/statusline-git-cache-$$"
# 1回目: /tmp/statusline-git-cache-48213
# 2回目: /tmp/statusline-git-cache-49004
# → 別ファイルなのでキャッシュが効かない

statusLineスクリプトは毎回別プロセスとして起動されるので、$$os.getpid()は呼び出しごとに変わります。これをキーにすると毎回キャッシュミスして、結局git statusが叩かれ続ける羽目になります。

OK: session_idをキーに使う

JSON入力のsession_idはセッションが続く限り不変で、かつ並走する他セッションとは別の値なので、これをキャッシュファイル名に使うのが正解です。

#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
DIR=$(echo "$input" | jq -r '.workspace.current_dir')
SESSION_ID=$(echo "$input" | jq -r '.session_id')

CACHE_FILE="/tmp/statusline-git-cache-$SESSION_ID"
MAX_AGE=5

if [ ! -f "$CACHE_FILE" ] || \
   [ $(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE"))) -gt $MAX_AGE ]; then
  if git rev-parse --git-dir > /dev/null 2>&1; then
    BRANCH=$(git branch --show-current)
    MODIFIED=$(git diff --numstat | wc -l | tr -d ' ')
    echo "$BRANCH|$MODIFIED" > "$CACHE_FILE"
  else
    echo "|" > "$CACHE_FILE"
  fi
fi

IFS='|' read -r BRANCH MODIFIED < "$CACHE_FILE"
echo "[$MODEL] 📁 ${DIR##*/} 🌿 ${BRANCH:-no-git} ~${MODIFIED:-0}"
[Opus] 📁 wp-auto-poster 🌿 main ~3

細かいですがstat -f %mはmacOS、stat -c %YはLinuxという互換差もあるので、両方フォールバックさせる書き方が無難です。複数リポジトリを並走させるとキャッシュファイルが増えていきますが、/tmpなので再起動で勝手に消えます。

statuslineが表示されない時のチェックリスト

サジェストでも「claude code status line not showing」「not working」がよく出てきますが、原因はだいたい以下のどれかに当てはまります。

workspace trustが未承認

statusLineはシェルコマンドを実行するため、そのディレクトリのworkspace trustダイアログを承認していない限り走りません。承認していない場合はstatusline skipped · restart to fixという通知が代わりに出ます。Claude Codeを一度再起動して、起動時に出るtrustプロンプトで承認すれば動き出します。

disableAllHooksが効いている

settings.json"disableAllHooks": trueが入っていると、HooksだけでなくstatusLineも一緒に無効化されます。セキュリティ目的でフックを全停止しているプロジェクトでは、ここが盲点になりがちです。

{
  "disableAllHooks": true,
  "statusLine": { "type": "command", "command": "~/.claude/statusline.sh" }
}

chmod忘れ・stderr出力・空出力

残りはスクリプト側の不備です。よくあるパターンを挙げると次の通り。

  • chmod +x ~/.claude/statusline.shを忘れている
  • 標準出力ではなく標準エラーに書いてしまっている
  • 非ゼロ終了している(statusLineが空白になる)
  • jqの// 0// emptyを入れ忘れてnullでクラッシュ

切り分けたい時はclaude --debugで起動するのが効きます。最初の1回のstatusLine実行のexit codeとstderrがログに出るので、原因が大抵そこに書いてあります。手元のスクリプトを単体テストするなら、こんなモックJSONを流し込めば確認できます。

echo '{"model":{"display_name":"Opus"},"workspace":{"current_dir":"/tmp"},"context_window":{"used_percentage":42},"session_id":"test"}' | ~/.claude/statusline.sh
[Opus] 📁 tmp ▓▓▓▓░░░░░░ 42%

subagentStatusLineでサブエージェント行を書き換える

あまり知られていない機能として、プロンプト下のサブエージェント表示もsubagentStatusLineで上書きできます。デフォルトのname · description · token count形式が物足りない時に有効です。Claude Codeのサブエージェント並列実行でも触れたように、複数のサブエージェントを走らせるとどれが何をしているか追いきれなくなりがちなので、ここをカスタマイズして役割や経過時間を出しておくと運用がだいぶ楽になります。

入力JSONの構造

こちらの入力は普通のstatusLineと少し違って、表示中の全サブエージェント行が1個のJSONオブジェクトでまとめて渡されます。重要なフィールドは次の通り。

  • columns—1行に使える表示幅(文字数)
  • tasks—各サブエージェントの配列。要素はid, name, type, status, description, label, startTime, tokenCount, tokenSamples, cwd

出力はJSONLで「上書きしたい行だけ」返す

出力は標準のstatusLineと違ってJSONLで、上書きしたい行だけを1行ずつ返します。idを返さなければデフォルト描画のまま、contentを空文字にすればその行を非表示にできます。

#!/bin/bash
input=$(cat)
echo "$input" | jq -c '
  .tasks[] |
  {
    id: .id,
    content: "[\(.status)] \(.name) · \(.tokenCount)tk · \((now - (.startTime / 1000)) | floor)s"
  }
'
{"id":"task-01","content":"[running] code-reviewer · 14523tk · 38s"}
{"id":"task-02","content":"[completed] test-runner · 8210tk · 15s"}

細かい注意点ですが、statusLineと同じくworkspace trustとdisableAllHooksのゲートがそのまま効きます。プラグインからデフォルトのsubagentStatusLineを配ることもできるので、チームで揃えたい場合はプラグイン経由が綺麗です。

まとめ

Claude Codeのstatuslineを自作する時に押さえておきたいポイントを振り返ります。

  • stdinに流れるJSONには20以上のフィールドが入っており、used_percentagerate_limitsのような便利なものも含まれている
  • rate_limits / effort / worktree条件付きでしか現れないので、jqの// emptyでフォールバック必須
  • 更新はイベントドリブン+300msデバウンス。アイドル中に更新したいならrefreshIntervalを入れる
  • キャッシュキーには$$ではなくsession_idを使う。プロセスIDだとキャッシュが効かない
  • 表示されない時はまずworkspace trustdisableAllHooksを疑う。claude --debugでstderrが見られる
  • subagentStatusLineはJSONL出力で行ごとに上書きできる。サブエージェントの並列実行管理に効く

個人的には、まず/statuslineで雑に作って、必要に応じてrefreshIntervalとキャッシュ戦略を足していく流れが、トラブルなくスケールさせやすいやり方だと感じています。関連記事として、Claude Code memoryとCLAUDE.mdの違いや、worktreeでの並列開発設計あたりを併せて読むと、Claude Code全体の運用設計がだいぶ見通せるはずです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次