Claude Codeのステータスラインをカスタマイズして使用量クォータを常に表示する

Claude Code のステータスラインを robbyrussell テーマ風にカスタマイズし、5時間と7日間の使用量クォータを残り時間つきで常に表示するようにした。プロジェクト名や Git ブランチ、コンテキスト使用率、コストも並べる。スクリプトの入手と settings.json への登録手順、仕組みを解説する。

Claude Codeの入力欄の下にはステータスラインが表示されるが、デフォルトではモデル名とコンテキスト使用量くらいしか表示されない。

Maxプランで使っていると「今どれくらいクォータを消費しているか」が気になるが、毎回Webで確認するのは面倒だ。そこで、ステータスラインをカスタマイズして使用量クォータを常に表示するようにした。

完成形

最終的にこのような表示になる。

カスタマイズ後のステータスライン

左から順に:

  • プロジェクト名(シアン)— ワークツリー使用時も元のリポジトリ名を表示
  • Gitブランチ(赤)+ dirty flag — 長いブランチ名は自動省略
  • モデル名(薄字)
  • コンテキスト使用率 — 色分け表示
  • セッションコスト
  • 5時間/7日間クォータ — 使用率と残り時間のカウントダウン

クォータの色はしきい値で変化する。50%未満は緑、50〜79%は黄色、80%以上は赤。

セットアップ

前提条件

  • jq(JSONパーサー)
  • curl(API呼び出し用)
  • git

1. スクリプトの配置

以下のスクリプトを ~/.claude/statusline-command.sh に保存する。curlで直接ダウンロードできる。

1
2
3
curl -o ~/.claude/statusline-command.sh \
  https://gist.githubusercontent.com/sudolifeagain/605b2fdf22b12b87cf0dc485e2992de2/raw/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
#!/usr/bin/env bash
# Claude Code status line - robbyrussell theme inspired
# with 5-hour / weekly usage quota display

umask 077

input=$(cat)

{
  read -r cwd
  read -r model
  read -r used_pct
  read -r cost
} <<< "$(echo "$input" | jq -r '
  (.workspace.current_dir // .cwd // ""),
  (.model.display_name // ""),
  (.context_window.used_percentage // "" | tostring),
  (.cost.total_cost_usd // 0 | tostring)
' | tr -d '\r')"

[[ "$cost" =~ ^[0-9]*\.?[0-9]+$ ]] || cost=0

# Git branch, dirty status, and project directory (single pass)
branch=""
dirty=""
dir_name="?"
if [ -n "$cwd" ] && git -C "$cwd" rev-parse --git-dir > /dev/null 2>&1; then
  branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null || git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
  if [ -n "$branch" ] && git -C "$cwd" status --porcelain --no-renames 2>/dev/null | read -r _; then
    dirty="✗"
  fi
  # Use main worktree path as project name (resolves worktree → original repo)
  main_wt=$(git -C "$cwd" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
  if [ -n "$main_wt" ]; then
    dir_name=$(echo "$main_wt" | sed 's|\\|/|g' | sed 's|/$||' | awk -F/ '{print $NF}')
  fi
fi

# Fallback if not a git repo or detection failed
if [ "$dir_name" = "?" ] && [ -n "$cwd" ]; then
  dir_name=$(echo "$cwd" | sed 's|\\|/|g' | sed 's|/$||' | awk -F/ '{print $NF}')
  [ -z "$dir_name" ] && dir_name="?"
fi

# Abbreviate long branch names (especially for worktree)
abbreviate_branch() {
  local b="$1"
  local max_len=25

  # Strip common worktree/feature prefixes for display
  local display="$b"
  display="${display#worktree-}"
  display="${display#feature/}"
  display="${display#feat/}"
  display="${display#fix/}"
  display="${display#bugfix/}"
  display="${display#hotfix/}"

  # If it had a prefix, show a short indicator
  local prefix_indicator=""
  if [ "$display" != "$b" ]; then
    case "$b" in
      worktree-*) prefix_indicator="wt:" ;;
      feature/*|feat/*) prefix_indicator="f/" ;;
      fix/*|bugfix/*) prefix_indicator="x/" ;;
      hotfix/*) prefix_indicator="h/" ;;
    esac
  fi

  local full="${prefix_indicator}${display}"
  if [ ${#full} -gt "$max_len" ]; then
    echo "${full:0:$(( max_len - 1 ))}…"
  else
    echo "$full"
  fi
}

# Context usage
ctx_pct=""
if [ -n "$used_pct" ]; then
  ctx_pct=${used_pct%.*}
fi

# --- Fetch 5-hour / weekly usage quota ---

CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/claude-statusline"
mkdir -p "$CACHE_DIR" 2>/dev/null
CACHE_FILE="${CACHE_DIR}/usage-cache"
CACHE_TTL=300
CACHE_TTL_FAIL=600
FAIL_MARKER="${CACHE_DIR}/usage-cache.fail"
LOCK_DIR="${CACHE_DIR}/fetch.lock"
five_hour=""
seven_day=""

fetch_usage() {
  CRED_FILE="$HOME/.claude/.credentials.json"
  if [ ! -f "$CRED_FILE" ]; then
    touch "$FAIL_MARKER" "$CACHE_FILE" 2>/dev/null
    return 1
  fi

  token=$(jq -r '.claudeAiOauth.accessToken // empty' "$CRED_FILE" 2>/dev/null)
  if [ -z "$token" ]; then
    touch "$FAIL_MARKER" "$CACHE_FILE" 2>/dev/null
    return 1
  fi

  local _auth="${CACHE_DIR}/.auth-header"
  printf 'Authorization: Bearer %s' "$token" > "$_auth"
  response=$(curl -s --max-time 3 \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -H @"$_auth" \
    -H "anthropic-beta: oauth-2025-04-20" \
    -H "User-Agent: claude-code-statusline/1.0" \
    "https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
  rm -f "$_auth" 2>/dev/null

  if echo "$response" | jq -e '.five_hour' > /dev/null 2>&1; then
    echo "$response" > "$CACHE_FILE"
    rm -f "$FAIL_MARKER" 2>/dev/null
    return 0
  fi
  touch "$FAIL_MARKER" 2>/dev/null
  if [ -f "$CACHE_FILE" ] && [ -s "$CACHE_FILE" ]; then
    touch "$CACHE_FILE" 2>/dev/null
  else
    touch "$CACHE_FILE" 2>/dev/null
  fi
  return 1
}

fetch_with_lock() {
  if [ -d "$LOCK_DIR" ]; then
    lock_age=$(( $(date +%s) - $(_file_mtime "$LOCK_DIR") ))
    [ "$lock_age" -gt 30 ] && rmdir "$LOCK_DIR" 2>/dev/null
  fi
  if mkdir "$LOCK_DIR" 2>/dev/null; then
    trap 'rmdir "$LOCK_DIR" 2>/dev/null' RETURN
    fetch_usage
  fi
}

active_ttl="$CACHE_TTL"
[ -f "$FAIL_MARKER" ] && active_ttl="$CACHE_TTL_FAIL"

_file_mtime() {
  stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
}

if [ -f "$CACHE_FILE" ]; then
  cache_age=$(( $(date +%s) - $(_file_mtime "$CACHE_FILE") ))
  [ "$cache_age" -gt "$active_ttl" ] && fetch_with_lock
else
  fetch_with_lock
fi

five_hour=""
seven_day=""
five_hour_reset=""
seven_day_reset=""

if [ -f "$CACHE_FILE" ] && [ -s "$CACHE_FILE" ]; then
  {
    read -r five_hour
    read -r seven_day
    read -r five_hour_reset
    read -r seven_day_reset
  } <<< "$(jq -r '
    (.five_hour.utilization // "" | tostring),
    (.seven_day.utilization // "" | tostring),
    (.five_hour.resets_at // ""),
    (.seven_day.resets_at // "")
  ' "$CACHE_FILE" 2>/dev/null | tr -d '\r')"
fi

remaining_time() {
  local reset_at="$1"
  if [ -z "$reset_at" ]; then return; fi
  local clean=$(echo "$reset_at" | sed 's/\.[0-9]*//' | sed 's/+00:00$/+0000/' | sed 's/Z$/+0000/')
  # GNU date (Linux/Windows Git Bash) or BSD date (macOS)
  local reset_epoch=$(date -d "$clean" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S%z" "$clean" +%s 2>/dev/null)
  if [ -z "$reset_epoch" ]; then return; fi
  local now=$(date +%s)
  local diff=$(( reset_epoch - now ))
  if [ "$diff" -le 0 ]; then echo "now"; return; fi
  local days=$(( diff / 86400 ))
  local hours=$(( (diff % 86400) / 3600 ))
  local mins=$(( (diff % 3600) / 60 ))
  if [ "$days" -gt 0 ]; then
    echo "${days}d${hours}h"
  elif [ "$hours" -gt 0 ]; then
    echo "${hours}h${mins}m"
  else
    echo "${mins}m"
  fi
}

# ANSI colors
CYAN="\033[0;36m"
BLUE="\033[1;34m"
RED="\033[0;31m"
YELLOW="\033[0;33m"
GREEN="\033[0;32m"
DIM="\033[2m"
RESET="\033[0m"

_color_for_val() {
  local val=${1%.*}
  if [ "$val" -ge 80 ] 2>/dev/null; then _uc="${RED}"
  elif [ "$val" -ge 50 ] 2>/dev/null; then _uc="${YELLOW}"
  else _uc="${GREEN}"; fi
}

# --- Output ---
# Format: dir git:(branch) model ctx:N% $0.12 | 5h:N%(Xh) 7d:N%(Xd)

printf "${CYAN}%s${RESET}" "$dir_name"

if [ -n "$branch" ]; then
  br_display=$(abbreviate_branch "$branch")
  if [ -n "$dirty" ]; then
    printf " ${BLUE}git:(${RED}%s${BLUE})${RESET} ${YELLOW}%s${RESET}" "$br_display" "$dirty"
  else
    printf " ${BLUE}git:(${RED}%s${BLUE})${RESET}" "$br_display"
  fi
fi

if [ -n "$model" ]; then
  printf " ${DIM}%s${RESET}" "$model"
fi

if [ -n "$ctx_pct" ]; then
  _color_for_val "$ctx_pct"; printf " ${_uc}ctx:%s%%${RESET}" "$ctx_pct"
fi

printf " ${DIM}\$%.2f${RESET}" "$cost"

# 5-hour / weekly usage quota with reset countdown
if [ -n "$five_hour" ]; then
  five_int=${five_hour%.*}
  printf " ${DIM}|${RESET} "
  _color_for_val "$five_hour"; printf "${_uc}5h:${five_int}%%${RESET}"
  five_remain=$(remaining_time "$five_hour_reset")
  if [ -n "$five_remain" ]; then
    printf "${DIM}(%s)${RESET}" "$five_remain"
  fi
fi

if [ -n "$seven_day" ]; then
  seven_int=${seven_day%.*}
  _color_for_val "$seven_day"; printf " ${_uc}7d:${seven_int}%%${RESET}"
  seven_remain=$(remaining_time "$seven_day_reset")
  if [ -n "$seven_remain" ]; then
    printf "${DIM}(%s)${RESET}" "$seven_remain"
  fi
fi
statusline-command.sh(GitHub Gist)

2. settings.jsonに登録

~/.claude/settings.json に以下を追加する。

1
2
3
4
5
6
{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline-command.sh"
  }
}

これだけで次回のClaude Code起動時からステータスラインが変わる。

仕組み

ステータスラインの入力

Claude Codeはステータスラインスクリプトを実行する際、stdinにJSON形式でセッション情報を渡してくる。

1
2
3
4
5
6
{
  "model": { "display_name": "Claude Opus 4.6" },
  "workspace": { "current_dir": "/home/user/project" },
  "context_window": { "used_percentage": 12.5 },
  "cost": { "total_cost_usd": 0.45, "total_duration_ms": 120000 }
}

スクリプトは jq でこのJSONをパースし、必要な情報を取り出している。

クォータの取得

5時間/7日間の使用量クォータはClaude Codeの入力JSONには含まれていないため、Anthropic APIから直接取得している。

1
2
curl -s -H @"$auth_file" \
  "https://api.anthropic.com/api/oauth/usage"

OAuthトークンは ~/.claude/.credentials.json から読み取る。トークンは一時ファイル経由で渡すことで ps aux への露出を防いでいる。APIレスポンスは ~/.cache/claude-statusline/ に5分間キャッシュし、APIコール失敗時は10分間のバックオフを入れている。複数のClaude Codeインスタンスが同時にキャッシュを更新しないよう、mkdir によるアトミックロックで排他制御もしている。

ワークツリー対応

claude -w でワークツリーモードを使うと、カレントディレクトリがワークツリーのパスになる。そのままだとプロジェクト名の代わりにワークツリー名(ランダム文字列)が表示されてしまう。

1
2
# ワークツリーのメインリポジトリパスを取得
main_wt=$(git -C "$cwd" worktree list --porcelain | head -1 | sed 's/^worktree //')

git worktree list --porcelain の1行目がメインワークツリー(元のリポジトリ)のパスを返すので、そこからプロジェクト名を取得している。

ブランチ名の省略

ワークツリーのブランチ名はランダムに生成されて長くなりがちなので、自動省略する機能を入れた。

元のブランチ名 省略後
worktree-delightful-brewing-wreath wt:delightful-brewing-w…
feature/add-user-auth f/add-user-auth
fix/null-pointer-crash x/null-pointer-crash

25文字を超える場合は末尾を … で切り詰める。

クロスプラットフォーム対応

Linux、macOS、Windows(Git Bash)で動作するよう、プラットフォーム依存の箇所にフォールバックを入れている。

1
2
3
4
5
6
7
8
# ファイルのmtime取得: GNU stat → BSD stat
_file_mtime() {
  stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
}

# ISO 8601パース: GNU date → BSD date
local reset_epoch=$(date -d "$clean" +%s 2>/dev/null || \
  date -j -f "%Y-%m-%dT%H:%M:%S%z" "$clean" +%s 2>/dev/null)

リポジトリ

スクリプト本体に加えて、カスタムスキル(/review-pr、/copilot-review)やインストールスクリプトも含めたリポジトリを公開している。

発言は個人の見解であり、所属組織とは関係ありません。
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。