- Claude Codeのステータスラインにrate limit・コンテキスト使用率を常時表示できる
- usage APIは存在するがanthropic-beta ヘッダーなしだと認証エラーになる
- OAuthトークン取得にはanthropic-beta: oauth-2025-04-20ヘッダーが必須
そう思って始めたら、認証エラーとレートリミットの無限ループで3時間溶かすという地獄を見ました。
最終的にClaude Code自身がバイナリ解析を始め、「なぜ動かないのか」の根本原因を特定するという展開に。この記事では完成したスクリプトの設置方法と、そこに至るまでの試行錯誤の全記録をお届けします。
- 完成形と設定方法
- きっかけ — Zennの記事を見て「これやりたい」
- 最初の成果物 — 動いたけどバグだらけ
- 認証エラーとレートリミットで3時間溶かした話
- Claude Codeがバイナリ解析を始めた
- 最終的に分かったこと
- おまけ — 見た目のカスタマイズ
完成形と設定方法
まず結果から。こんな感じで表示されます。
ステータスラインに以下の情報が常時表示されます。
| 表示項目 | 説明 |
|---|---|
| モデル名(プラン) | Opus 4.6 (Max) のように表示 |
| コンテキスト使用率 | 使用率に応じて緑→黄→赤に変化 |
| セッションコスト | そのセッションでの累計コスト |
| 行変更数 | +追加行数 / -削除行数 |
| ディレクトリ・ブランチ | 現在のディレクトリ名とGitブランチ |
| 5時間rate limit | プログレスバー + % + リセット時刻 |
| 7日間rate limit | プログレスバー + % + リセット日時 |
スクリプトの設置
~/.claude/statusline-command.sh として以下のスクリプトを保存します。
#!/bin/bash
# Claude Code Status Line
# Usage data from Messages API response headers (anthropic-ratelimit-unified-*)
SESSION_DATA=$(cat)
# --- Extract session info ---
MODEL=$(echo "$SESSION_DATA" | jq -r 'if .model | type == "object" then .model.display_name // .model.id // "?" else .model // "?" end')
CTX_PCT=$(echo "$SESSION_DATA" | jq -r '.context_window.used_percentage // 0')
LINES_ADDED=$(echo "$SESSION_DATA" | jq -r '.cost.total_lines_added // 0')
LINES_REMOVED=$(echo "$SESSION_DATA" | jq -r '.cost.total_lines_removed // 0')
COST_USD=$(echo "$SESSION_DATA" | jq -r '.cost.total_cost_usd // 0')
CWD=$(echo "$SESSION_DATA" | jq -r '.cwd // ""')
GIT_BRANCH=""
if [ -n "$CWD" ]; then
GIT_BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
fi
# --- Plan info from keychain ---
CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "")
SUB_TYPE=$(echo "$CREDS" | jq -r '.claudeAiOauth.subscriptionType // ""' 2>/dev/null || echo "")
case "$SUB_TYPE" in
max) PLAN="Max" ;;
pro) PLAN="Pro" ;;
free) PLAN="Free" ;;
*) PLAN="$SUB_TYPE" ;;
esac
# --- Color helpers ---
c() { echo "\033[38;2;$1m"; }
RESET="\033[0m"
BOLD="\033[1m"
DIM=$(c "100;110;114")
SEP=" $(c "60;70;74")│${RESET} "
color_by_pct() {
local pct=$1
if [ "$pct" -ge 80 ] 2>/dev/null; then echo "231;111;81"
elif [ "$pct" -ge 50 ] 2>/dev/null; then echo "241;196;15"
else echo "46;204;113"; fi
}
bar() {
local pct=$1 color=$2
local filled=$(( pct / 5 ))
local empty=$(( 20 - filled ))
local out=""
out+="$(c "80;85;90")["
out+="$(c "$color")"
for ((i=0; i<filled; i++)); do out+="█"; done
out+="$(c "50;55;60")"
for ((i=0; i<empty; i++)); do out+="░"; done
out+="$(c "80;85;90")]"
out+="${RESET}"
echo "$out"
}
# --- Fetch usage via Messages API headers ---
CACHE_FILE="/tmp/claude-usage-cache.json"
LOCK_FILE="/tmp/claude-usage-cache.lock"
CACHE_TTL=900
fetch_usage() {
if [ -f "$CACHE_FILE" ]; then
local cache_age
cache_age=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) ))
if [ "$cache_age" -lt "$CACHE_TTL" ]; then
cat "$CACHE_FILE"; return
fi
fi
if [ -f "$LOCK_FILE" ]; then
local lock_age
lock_age=$(( $(date +%s) - $(stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0) ))
if [ "$lock_age" -lt 30 ]; then
[ -f "$CACHE_FILE" ] && cat "$CACHE_FILE" || echo '{}'; return
fi
fi
echo $$ > "$LOCK_FILE"
local token
token=$(echo "$CREDS" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null || echo "")
if [ -z "$token" ]; then
rm -f "$LOCK_FILE"
[ -f "$CACHE_FILE" ] && cat "$CACHE_FILE" || echo '{"error":"no_token"}'
return
fi
local hf="/tmp/claude-usage-headers.txt"
curl -s -D "$hf" --max-time 10 \
-H "Authorization: Bearer $token" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "content-type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \
"https://api.anthropic.com/v1/messages" > /dev/null 2>/dev/null
rm -f "$LOCK_FILE"
if [ -f "$hf" ]; then
local fu fr su sr
fu=$(grep -i "ratelimit-unified-5h-utilization" "$hf" | awk '{print $2}' | tr -d '\r')
fr=$(grep -i "ratelimit-unified-5h-reset" "$hf" | awk '{print $2}' | tr -d '\r')
su=$(grep -i "ratelimit-unified-7d-utilization" "$hf" | awk '{print $2}' | tr -d '\r')
sr=$(grep -i "ratelimit-unified-7d-reset" "$hf" | awk '{print $2}' | tr -d '\r')
if [ -n "$fu" ]; then
jq -n --arg fu "$fu" --arg fr "$fr" --arg su "$su" --arg sr "$sr" \
'{five_hour:{utilization:($fu|tonumber),reset:($fr|tonumber)},seven_day:{utilization:($su|tonumber),reset:($sr|tonumber)}}' \
> "$CACHE_FILE"
cat "$CACHE_FILE"; rm -f "$hf"; return
fi
rm -f "$hf"
fi
echo '{"error":"fetch_failed"}' > "$CACHE_FILE"
cat "$CACHE_FILE"
}
続いて後半のパース・表示部分です。上記の fetch_usage 関数の続きに追加してください。
USAGE=$(fetch_usage)
# Parse usage
F5_PCT=0; F7_PCT=0; F5_RST=""; F7_RST=""; HAS=false
if echo "$USAGE" | jq -e '.five_hour' >/dev/null 2>&1; then
HAS=true
F5_PCT=$(echo "$USAGE" | jq -r '.five_hour.utilization // 0' | awk '{printf "%d",$1*100}')
F5_E=$(echo "$USAGE" | jq -r '.five_hour.reset // 0')
[ "$F5_E" -gt 0 ] 2>/dev/null && F5_RST=$(TZ=Asia/Tokyo date -r "$F5_E" "+%H:%M" 2>/dev/null)
F7_PCT=$(echo "$USAGE" | jq -r '.seven_day.utilization // 0' | awk '{printf "%d",$1*100}')
F7_E=$(echo "$USAGE" | jq -r '.seven_day.reset // 0')
[ "$F7_E" -gt 0 ] 2>/dev/null && F7_RST=$(TZ=Asia/Tokyo date -r "$F7_E" "+%m/%d %H:%M" 2>/dev/null)
fi
# --- Build output ---
# Line 1: Model (Plan) | Ctx | Cost | Lines | Dir on Branch
CTX_C=$(color_by_pct "$CTX_PCT")
L1="${BOLD}${MODEL}${RESET}"
[ -n "$PLAN" ] && L1+=" ${DIM}(${PLAN})${RESET}"
L1+="${SEP}$(c "$CTX_C")${CTX_PCT}%${RESET}"
L1+="${SEP}$(c "180;180;180")\$$(printf '%.2f' "$COST_USD")${RESET}"
L1+="${SEP}$(c "46;204;113")+${LINES_ADDED}${RESET}$(c "100;110;114")/${RESET}$(c "231;111;81")-${LINES_REMOVED}${RESET}"
DIR_NAME=""
if [ -n "$CWD" ]; then
DIR_NAME=$(basename "$CWD")
fi
if [ -n "$DIR_NAME" ]; then
L1+="${SEP}$(c "180;190;200")${DIR_NAME}${RESET}"
[ -n "$GIT_BRANCH" ] && L1+=" $(c "60;70;74")on${RESET} $(c "129;178;210")${GIT_BRANCH}${RESET}"
fi
echo -e "$L1"
# Line 2-3: Rate limit bars
if [ "$HAS" = true ]; then
F5_BAR=$(bar "$F5_PCT" "$(color_by_pct "$F5_PCT")")
F5_C=$(color_by_pct "$F5_PCT")
L2="5h ${F5_BAR} $(c "$F5_C")${F5_PCT}%${RESET}"
[ -n "$F5_RST" ] && L2+=" ${DIM}reset ${F5_RST}${RESET}"
echo -e "$L2"
F7_BAR=$(bar "$F7_PCT" "$(color_by_pct "$F7_PCT")")
F7_C=$(color_by_pct "$F7_PCT")
L3="7d ${F7_BAR} $(c "$F7_C")${F7_PCT}%${RESET}"
[ -n "$F7_RST" ] && L3+=" ${DIM}reset ${F7_RST}${RESET}"
echo -e "$L3"
else
echo -e "${DIM}Usage loading...${RESET}"
fi
前半・後半を1つのファイルとして ~/.claude/statusline-command.sh に保存すれば完成です。
settings.jsonへの追加
~/.claude/settings.json に以下を追加します。
{ "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" } }
実行権限の付与と、jq(JSONパーサー)のインストールを忘れずに。
chmod +x ~/.claude/statusline-command.sh # jqが入っていなければインストール jq --version || brew install jq
設定自体はシンプルです。ただ、ここに至るまでの道のりが壮絶だったのでその話をします。
きっかけ — Zennの記事を見て「これやりたい」
こちらの記事を見て「これは便利だ」と思ったのがすべての始まりです。
記事の内容は明快で、statusLine 設定にスクリプトを指定するだけ。Claude CodeのAPIからusage情報を取得して表示する仕組みです。
そう思って「これ、できる?」とClaude Codeに投げたところ、ものの1分で最初のスクリプトが完成しました。
最初の成果物 — 動いたけどバグだらけ
Claude Codeが生成したスクリプトを設定し、再起動すると確かにステータスラインに何かが表示されました。
ただ、表示がおかしい。
モデル名がJSON丸ごと表示される
{"id": "claude-opus-4-6", "display_name": "Opus 4.6"} │ Ctx: 0%
sessionデータの .model フィールドが文字列ではなくオブジェクトで返ってきていました。.model.display_name を取る必要があったのに、.model をそのまま出力していた。
パーセント記号が二重になる
Ctx: 0%% │ 5h ▱▱▱▱▱▱▱▱▱▱ 0%%
echo -e で出力しているのに %% と書いていたため、printf のエスケープと混同していたようです。echo -e なら % 単体でOK。
Usage が全部0%
5h ▱▱▱▱▱▱▱▱▱▱ 0% 7d ▱▱▱▱▱▱▱▱▱▱ 0%
ここからが本番でした。
認証エラーとレートリミットで3時間溶かした話
OAuthトークンがJSONの中にJSON
Claude Codeはキーチェーンの Claude Code-credentials にOAuthトークンを保存しています。ところがこの値、JSONオブジェクトの中にアクセストークンが入っている構造でした。
# 取得した値の構造
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"expiresAt": 1772677734017,
"subscriptionType": "max",
...
}
}
最初のスクリプトでは -a "oauth_token" -w のように直接トークンを取ろうとしていましたが、-w で取れるのはJSONオブジェクト全体。そこから .claudeAiOauth.accessToken をjqで抽出する必要があったのです。
レートリミット自爆
トークンは取れるようになりましたが、APIエンドポイント /api/oauth/usage にリクエストすると OAuth authentication is currently not supported が返ってくる。
後から分かったことですが、この時点でanthropic-beta: oauth-2025-04-20 ヘッダーを付けていなかったのが原因です。OAuthトークンでAnthropicのAPIを叩くにはこのヘッダーが必須で、なしだと認証方式として認識されません。
ただ、この時はその事実を知る由もなく、Claude Codeはデバッグのためにキャッシュファイルを削除しました。
rm -f /tmp/claude-usage-cache.json
これが致命的だった。 ステータスラインのスクリプトはClaude Codeが数秒おきに実行します。キャッシュファイルがない状態でスクリプトが動くと、毎回APIを直接叩いてしまう。
結果、APIに大量のリクエストが飛び、429 Rate Limited の嵐に。
{ "error": { "message": "Rate limited. Please try again later.", "type": "rate_limit_error" } }
ロックファイルとエラーキャッシュを入れて連打を止めたものの、すでにレートリミットは発動済み。3時間経っても429が返り続ける状態になりました。
そもそもこのAPI、存在するの?
3時間以上レートリミットが解除されない。流石におかしい。
2026年 3月 5日 09時42分: Rate limited. Please try again later. 2026年 3月 5日 12時22分: Rate limited. Please try again later.
別のエンドポイントを片っ端から試しました。
/api/oauth/usage → Rate limited(もう叩きすぎ) /api/usage → 404 Not Found /v1/usage → 404 Not Found /api/auth/usage → 404 Not Found
全滅です。
ここで発想を転換します。「Claude Code自体は /usage の情報を正しく取得できている。なら、Claude Codeがどうやってデータを取っているのか調べればいい」
Claude Codeがバイナリ解析を始めた
はい、Claude Codeが strings と grep を使って自分自身のバイナリファイルを解剖し始めました。
stringsとgrepでClaude Code本体を解剖
まずClaude Codeが内部で使っているURLパターンを抽出。
grep -ao 'https://[a-zA-Z0-9._/-]*usage[a-zA-Z0-9._/-]*' \ ~/.local/share/claude/versions/2.1.69
https://claude.ai/admin-settings/usage https://claude.ai/settings/usage https://code.claude.com/docs/en/data-usage https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
完全URLでの検索では見つからない。(実際には api/oauth/usage は相対パスとしてバイナリに含まれていたのですが、この時点ではフルURL検索だったため引っかからなかった。これは後から判明します。)
anthropic-ratelimit-unified-* ヘッダーの発見
別の角度から、レートリミット関連の文字列を検索。
grep -ao '[a-zA-Z_-]*rate[_-]*limit[a-zA-Z_-]*\|[a-zA-Z_-]*utilization[a-zA-Z_-]*' \ ~/.local/share/claude/versions/2.1.69 | sort -u
anthropic-ratelimit-unified- anthropic-ratelimit-unified-5h-reset anthropic-ratelimit-unified-5h-utilization anthropic-ratelimit-unified-7d-reset anthropic-ratelimit-unified-7d-utilization anthropic-ratelimit-unified-status
見つけました。 Claude Codeは専用のusage APIなど使っていない。通常のMessages APIのレスポンスヘッダーにrate limit情報が含まれているのです。
隠し鍵 anthropic-beta: oauth-2025-04-20
ヘッダーの存在は分かりましたが、OAuthトークンでMessages APIを叩くと 401 Unauthorized が返る。通常のAPIキーとは認証方式が違うはず。
バイナリからリクエストヘッダーの設定部分を抽出しました。
grep -aob 'anthropic-beta' ~/.local/share/claude/versions/2.1.69
コードを読み解くと、Claude Codeは以下のヘッダーを付けてAPIリクエストを送っていました。
Authorization: Bearer {OAuthトークン}
anthropic-beta: oauth-2025-04-20
この anthropic-beta ヘッダーがOAuth認証に必要な鍵だったのです。公式ドキュメントには載っていません。
早速テスト。
TOKEN=$(security find-generic-password -s "Claude Code-credentials" -w | jq -r '.claudeAiOauth.accessToken')
curl -s -D /tmp/headers.txt \
-H "Authorization: Bearer $TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "content-type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \
"https://api.anthropic.com/v1/messages" > /dev/null
レスポンスヘッダー:
anthropic-ratelimit-unified-status: allowed anthropic-ratelimit-unified-5h-utilization: 0.02 anthropic-ratelimit-unified-5h-reset: 1772697600 anthropic-ratelimit-unified-7d-utilization: 0.44 anthropic-ratelimit-unified-7d-reset: 1772935200
完全に動作しました。 5h utilization: 2%、7d utilization: 44%。/usage で表示される値と一致。
最終的に分かったこと
ここまでの調査で判明した事実をまとめます。
/api/oauth/usage は存在する — ただし anthropic-beta ヘッダーが必須
当初「/api/oauth/usage エンドポイントは存在しない」と考えていましたが、追加調査でこれは誤りだと判明しました。
バイナリを改めて精査すると、Claude Code内部にこのエンドポイントを呼び出すコードがしっかり存在していました。
// Claude Code v2.1.69 内部のコード(バイナリから抽出・整形) let R = { "Content-Type": "application/json", "User-Agent": Bh(), ..._.headers // ← ここにBf()の結果が展開される }; let q = `${t8().BASE_API_URL}/api/oauth/usage`; return (await UR.get(q, {headers: R, timeout: 5000})).data
そして _.headers に展開される Bf() 関数の中身はこう。
function Bf() { return { headers: { Authorization: `Bearer ${_.accessToken}`, "anthropic-beta": "oauth-2025-04-20" } } }
つまり /api/oauth/usage は anthropic-beta: oauth-2025-04-20 ヘッダーを付ければ正常に動く。 前のセッションでうまくいかなかったのは、Claude Codeが生成したスクリプトにこのヘッダーが含まれていなかっただけでした。
なぜ参考記事を見ても動かなかったのか
ここで「参考にしたZennの記事はなぜこの問題に触れていないのか」という疑問が湧きます。
記事の内容を改めて確認すると、具体的なスクリプトのコードは掲載されていませんでした。 「Claude Codeにこういう仕様でスクリプトを作らせた」という説明と、完成後のスクリーンショットだけ。
実際、私がClaude Codeに同じ指示を出した時も、最初に生成されたスクリプトには anthropic-beta ヘッダーが含まれていませんでした。Claude Code自身が、自分のOAuth認証に必要なヘッダーを知らないのです。
これは考えてみれば当然で、anthropic-beta: oauth-2025-04-20 は公式ドキュメントに記載されておらず、Claude Codeのバイナリ内部にしか情報がありません。Claude(LLM)がこのヘッダーの存在を学習データから知る手段がない。
参考記事の著者も同じ状況に遭遇した可能性が高いですが、スクリプトのコードが非公開なので、最終的に動いているのか、あるいは記事公開時点では動作未確認だったのかは分かりません。
これはClaude Codeに限った話ではなく、ZennやQiitaのAI関連記事全般に言えることです。特にAIが生成したコードをそのまま掲載している記事は、細かい認証やヘッダーの部分で実は動かないケースが少なくありません。
usage情報のもう一つの取得方法 — Messages APIのレスポンスヘッダー
/api/oauth/usage エンドポイント以外に、Claude CodeはMessages APIのレスポンスヘッダーからもrate limit情報を取得しています。
| ヘッダー名 | 内容 |
|---|---|
anthropic-ratelimit-unified-status |
現在のステータス(allowed等) |
anthropic-ratelimit-unified-5h-utilization |
5時間使用率(0.0〜1.0) |
anthropic-ratelimit-unified-5h-reset |
5時間リセットのUnixタイムスタンプ |
anthropic-ratelimit-unified-7d-utilization |
7日間使用率(0.0〜1.0) |
anthropic-ratelimit-unified-7d-reset |
7日間リセットのUnixタイムスタンプ |
OAuth認証に必要な3つの要素
OAuthトークンでMessages APIを叩くには以下の3つが必要です。
| 要素 | 値 |
|---|---|
| トークン取得先 | macOSキーチェーン Claude Code-credentials → .claudeAiOauth.accessToken |
| 認証ヘッダー | Authorization: Bearer {token} |
| betaヘッダー | anthropic-beta: oauth-2025-04-20(これがないと401) |
キャッシュ設計は必須
ステータスラインのスクリプトはClaude Codeが数秒おきに実行します。毎回Messages APIを叩くと:
- レートリミットに引っかかる(実体験済み)
- Haikuのトークンを無駄に消費する
今回のスクリプトでは以下の対策を入れています。
- キャッシュTTL: 15分 — APIは15分に1回だけ叩く
- ロックファイル — 複数のstatusline呼び出しが同時にAPIを叩かない
- エラーキャッシュ — API失敗時もキャッシュファイルを作成し、TTL期間は再リクエストしない
おまけ — 見た目のカスタマイズ
基本的な表示ができたら、見た目も凝りたくなるのが人の性です。
プラン名の表示
キーチェーンの認証情報には subscriptionType フィールドが含まれています。これを使ってモデル名の横にプラン名を表示。
Opus 4.6 (Max)
カラフルなプログレスバー
使用率に応じて色が変わるプログレスバー。20セグメントのブロックスタイルです。
[████████████░░░░░░░░] 60% ← 金色(50%超え) [████████████████████] 95% ← 赤色(80%超え) [████░░░░░░░░░░░░░░░░] 20% ← 緑色(50%未満)
ディレクトリとブランチ
作業ディレクトリ名とGitブランチを表示。地味に便利。
📂 hatena_blog on 🔀 main
5hバーと7dバーの色分け・セパレーター
5時間と7日間のバーが同じ色だと一瞬どちらか分かりにくいので、色系統を変えるのもおすすめです。
- 5hバー: シアン系(青→水色→赤)
- 7dバー: パープル系(紫→ラベンダー→赤)
バーの間に薄いセパレーターラインを入れると、さらに見やすくなります。
rate limitティアの表示
Maxプランの場合、rateLimitTier フィールドに default_claude_max_20x のような値が入っています。ここから倍率部分を抽出すれば Opus 4.6 (Max 20x) のように表示可能です。
最終的な表示例
🤖 Opus 4.6 (Max 20x) │ 📊 21% │ 💰 $1.98 │ ✏️ +48/-12 │ 📂 hatena_blog on 🔀 main ⏱ [████░░░░░░░░░░░░░░░░] 10% ↻ 17:00 ───────────────────────────────── 📅 [████████░░░░░░░░░░░░] 42% ↻ 03/08 11:00
これがClaude Codeの画面下部に常時表示されます。 rate limitが80%を超えると赤くなるので、「そろそろヤバいな」と一目で分かる。
でもおかげで、Claude Codeが内部でどうやってrate limit情報を管理しているのか完全に理解できました。遠回りしたからこそ得られた知見です。
同じことをやろうとしてハマっている人がいたら、この記事が助けになれば幸いです。