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 の Statusline を自作してコスト・コンテキスト残量を可視化する実装 — stdin JSON パースと描画遅延の3つのハマりどころ【2026】

0
Posted at

はじめに / 対象と前提

Claude Code を毎日ターミナルで使っていて、「今どのモデルを使っているか」「コンテキストウィンドウがどれくらい残っているか」「このセッションでいくら課金されているか」を画面の隅で常に見たい、と思ったことはないでしょうか。

Claude Code には statusLine という設定項目があり、任意の外部スクリプトを登録すると、ターミナルの下部に1行のステータスバーを常時表示できます。tmux のステータスバーや vim の airline のようなものを、Claude Code 用に自作するイメージです。

この記事の対象読者は以下の通りです。

  • Claude Code を日常的に使っていて、コスト管理やコンテキスト残量を意識したい人
  • シェルスクリプトか Python で簡単なパーサーを書ける人
  • ~/.claude/settings.json を編集したことがある(または抵抗がない)人

前提バージョン

  • Claude Code v2.x 系(statusLine 機能が安定して使えるバージョン)
  • Python 3.13 / bash 5.x(どちらでも実装可能、本記事は両方のサンプルを載せる)
  • macOS(iTerm2 / Terminal.app)での検証。Linux でも動作原理は同じ

TL;DR

  • settings.jsonstatusLine.command で外部コマンドを1つ登録するだけで、Claude Code はそのコマンドを毎ターン(正確にはレンダリングが必要になるたびに)起動し、stdin に現在のセッション情報を JSON で渡してくる
  • スクリプト側は stdin を読んで JSON をパースし、標準出力に1行のテキストを返すだけでよい(ANSI カラーコードも使える)
  • 実装でハマったのは主に3つ:①スクリプトが重いと表示が固まる(タイムアウトあり)②git 情報取得を毎回シェルアウトすると体感で分かるレベルの遅延が出る③ANSI エスケープの扱いがターミナルによって微妙に違う

手順 / 動かし方

1. settings.json に statusLine を登録する

~/.claude/settings.json(プロジェクト単位なら .claude/settings.json)に以下を追加します。

{
  "statusLine": {
    "type": "command",
    "command": "python3 ~/.claude/statusline.py",
    "padding": 0
  }
}

type: "command" は「外部コマンドの標準出力をそのままステータスバーとして使う」という意味です。padding はステータスバーの左右余白の調整用で、0 にすると端末幅いっぱいまで使えます。

2. stdin から渡ってくる JSON の中身を確認する

まずはパースする前に、実際にどんな JSON が飛んでくるのか確認するのが手っ取り早いです。デバッグ用に、受け取った JSON をそのままファイルに書き出すだけのスクリプトを一時的に仕込みます。

#!/bin/bash
cat > /tmp/statusline-debug.json
echo "debug"

これを command に指定して Claude Code を数ターン動かすと、/tmp/statusline-debug.json に以下のような構造の JSON が溜まります(フィールド名はバージョンで変わる可能性があるので、必ず自分の手元で確認してください)。

{
  "model": { "display_name": "Claude Sonnet 5" },
  "workspace": { "current_dir": "/Users/you/project" },
  "cost": { "total_cost_usd": 0.42 },
  "context": { "used_tokens": 84213, "max_tokens": 200000 },
  "session_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

ポイントは、モデル名・cwd・課金コスト・コンテキスト使用量が 1回の呼び出しで全部まとめて 渡ってくることです。個別に API を叩く必要はありません。

3. Python でパースして1行にまとめる

#!/usr/bin/env python3
import json
import sys
import subprocess
import os

def git_branch(cwd):
    cache_path = "/tmp/.statusline-git-cache"
    try:
        # git rev-parse は軽量だが、毎回叩くと積み重なるのでmtimeで簡易キャッシュ
        if os.path.exists(cache_path):
            if os.path.getmtime(cache_path) > os.path.getmtime(cwd) - 2:
                with open(cache_path) as f:
                    return f.read().strip()
    except OSError:
        pass

    try:
        out = subprocess.run(
            ["git", "-C", cwd, "branch", "--show-current"],
            capture_output=True, text=True, timeout=0.3
        )
        branch = out.stdout.strip() or "no-branch"
    except (subprocess.TimeoutExpired, FileNotFoundError):
        branch = "?"

    try:
        with open(cache_path, "w") as f:
            f.write(branch)
    except OSError:
        pass
    return branch

def main():
    data = json.load(sys.stdin)

    model = data.get("model", {}).get("display_name", "unknown")
    cwd = data.get("workspace", {}).get("current_dir", ".")
    cost = data.get("cost", {}).get("total_cost_usd", 0)
    used = data.get("context", {}).get("used_tokens", 0)
    max_tokens = data.get("context", {}).get("max_tokens", 1)
    pct = used / max_tokens * 100 if max_tokens else 0

    branch = git_branch(cwd)
    dirname = os.path.basename(cwd)

    color = "\033[32m" if pct < 60 else "\033[33m" if pct < 85 else "\033[31m"
    reset = "\033[0m"

    line = (
        f"\033[36m{model}\033[0m | "
        f"{dirname}({branch}) | "
        f"{color}ctx {pct:.0f}%{reset} | "
        f"${cost:.2f}"
    )
    print(line)

if __name__ == "__main__":
    main()

コンテキスト使用率でステータスの色を緑→黄→赤に変える単純なロジックですが、これだけでも「そろそろ /compact すべきか」の判断材料になります。Python 起動のオーバーヘッドが気になる場合は、同じロジックを jq + awk で書き直すと体感の遅延がさらに減ります。

ハマりどころ

1. スクリプトが重いと入力にもたつきが出る

症状:git loggit status --porcelain のような重めのコマンドを毎回呼んでいたら、リポジトリが大きいプロジェクトでキー入力から描画までの遅延が明らかに増えました。

原因:statusLine コマンドは描画が必要になるたびに高頻度で起動されます。1回あたり数十〜100ms程度でも、積み重なると UI 全体がもたつきます。

回避策:git 情報はファイルの mtime ベースで簡易キャッシュする(上記コード参照)。どうしても重い処理をしたいなら、バックグラウンドで定期的にファイルへ書き出しておき、statusline スクリプト自体は「ファイルを読むだけ」にする。subprocess.run(..., timeout=0.3) のようにタイムアウトも必須で、git がハングしても statusline 全体が固まらないようにします。

2. JSON のフィールドが期待通りに来ないことがある

症状:context.used_tokens が存在しない(0 扱いになる)ケースがあり、KeyError は出ないのに常に ctx 0% と表示されてしまいました。

原因:セッション開始直後など特定のタイミングでは context オブジェクト自体が省略されて渡ってくることがあります。.get() チェーンで例外は防げても、値が欠けていることには気づきにくいのが厄介でした。

回避策:全フィールドを dict.get(key, default) で防御的に取得しつつ、max_tokens が 0 など明らかに異常な値のときは "ctx --" のようなプレースホルダを出し、「取得失敗」と「本当に0%」を見た目で区別できるようにしました。

3. ANSI カラーがターミナルによって崩れる

症状:iTerm2 では綺麗に色が出るのに、tmux 越しに接続した別のターミナルエミュレータでは色コードがそのまま文字として表示されてしまうことがありました。

原因:tmux のバージョンや TERM 環境変数によって、256色 ANSI エスケープの解釈がまちまちでした。tmux 経由で外側のターミナルの色対応がうまく伝播していないケースもありました。

回避策:色は 8色の基本 ANSI(\033[31m 等)に留め、256色指定(\033[38;5;208m のような形式)は避けます。os.environ.get("TERM", "") を見て screentmux を含む場合は色付けを諦めてプレーンテキストにフォールバックする分岐を入れると安定しました。

(任意)背景・補足

statusline は「JSON を受け取って1行の文字列を返すだけ」という単純な設計なので、bash・Python・Node.js など好きな言語で実装できます。ただし高頻度に呼ばれる前提である点は要注意で、外部 API を叩くような重い処理を直接仕込むのはアンチパターンです。重い処理は別プロセスに逃がし、statusline 本体は「キャッシュされた値を読んで整形するだけ」に徹するのが安定運用のコツでした。

まとめ

  • Claude Code の statusLinesettings.json に1コマンド登録するだけで、stdin 経由のセッション JSON を自由に加工してステータスバーに表示できる
  • 実装は Python でも bash + jq でも可能。起動オーバーヘッドが気になるなら jq 版が有利
  • ハマりどころは「重い処理による遅延」「JSON フィールド欠損」「ANSI カラーの互換性」の3点で、いずれもキャッシュ・防御的取得・色のフォールバックで回避できる
  • コンテキスト残量やコストを常時可視化しておくと、/compact のタイミング判断や課金の把握がしやすくなり、地味だが日々の作業効率に効いてくる
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?