1
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?

週の途中で「利用枠75%」に焦らないために、Claude Code の mod でペースの予報を出した

1
Last updated at Posted at 2026-11-08

1. はじめに

仕事で Claude Code を使っていたある水曜日、「今週の利用枠が75%に達しました」という警告が出ました。

えーーーー、今週の仕事どうするのよーー、と思いました。幸い週の枠は木曜日にリセットされたので、仕事は止まらずに済みました。でも、心臓に悪いです。

利用枠は、Web の設定画面(使用量)や /usage コマンドで見られます。ただ、そこにあるのは「今何%か」と「いつリセットされるか」だけです。「このペースで使っても大丈夫か」を自分で計算するのが面倒で、結局「警告が出てから気づく」ことになっていました。

そこで、Claude Code の新しい拡張の仕組み「mod」を使って、利用枠を VS Code の横にずっと表示しておくようにしました。さらに、「今何%か」だけでなく「このペースで足りるか」の予報も出すようにしました。

この記事では、次の3つの問いに答えます。

  1. 利用枠(5時間・週)を、作業しながら見られないか
  2. 「今何%」だけでなく、「このペースで足りるか」を先に知れないか
  3. そのしくみ自体が、トークン(利用枠)を食わないか

mod は Claude Code 2.1.287 から標準で使えるようになった新しい仕組みで、私も今回初めて触りました。AI(Claude Code)に相談しながら作っています。間違いなどがあれば、コメントで教えていただけるとうれしいです。

2. 先に結論

VS Code の横に、利用枠とペースの予報をずっと出しておけるようになりました。

01_panel.png

  • 利用枠が見える: 「現在のセッション(5時間)」と「今週」の使用率とリセットの時刻。Web の設定画面(使用量)と同じ数字です
  • ペースの予報が出る: 「このペースなら、リセット時に約47%」、足りなくなりそうなら「あと約2.9時間で上限に届きます(リセットより30分早い)」
  • トークンは使わない: 表示しているのは Claude Code(プログラム)で、AI ではありません

画面の例では、5時間の枠は36%しか使っていないのに、時間の経過(32%)より速いペースなので「使いすぎのペース」と出ています。週の枠は12%で、このペースならリセット時に約47%なので「余裕あり」です。

3. 環境

項目 内容
OS Windows 11 Pro
エディタ VS Code + Claude Code 拡張機能 2.1.319
契約 Claude Pro(サブスクリプション)
確認した日 2026-11-08
  • mod は、ターミナル版なら Claude Code 2.1.287 以降、VS Code 拡張なら 2.1.299 以降で画面に描けます(公式ドキュメント)
  • 利用枠の数字が取れるのは、Pro や Max などのサブスクリプションで使っているときだけです。API キーで使っている場合は空になります
  • Node.js などの開発環境は要りません。mod のコード(TypeScript)は Claude Code がそのまま読み込みます

「VS Code では mod のパネルは表示されない」という解説もありますが、公式ドキュメントでは VS Code 拡張 2.1.299 以降で表示されることになっています。私の環境(2.1.319)でも、実際に表示されました。

4. 利用枠の数字はどこにあるのか

利用枠の数字は、Claude の API から、返事と一緒に Claude Code へ届いています。mod は Claude Code の中で動くので、その数字を直接読めます。

02_flow.png

mod は、Claude Code がすでに受け取った数字を読むだけです。

利用枠を見る方法は、ほかにもあります。

方法 どこに出るか いつ新しくなるか 向いている人
Web の設定画面(使用量) ブラウザ 開いたとき たまに確かめればよい人
/usage コマンド Claude Code の会話 打ったとき その場で確かめたい人。VS Code では「Account & Usage」の画面が開き、使用率とリセットの時刻に加えて、何が枠を使っているかの内訳も出る。ペースの予報はない
ステータスライン 入力欄の下の1行 返事のたび ターミナルで使う人。公開されたツールも多い。VS Code 拡張(2.1.319)では表示されなかった
mod(今回) VS Code の横のパネル 返事のたび 予報や通知まで自分で決めたい人

ステータスラインで利用枠を出す方法は、すでに解説やツールがたくさんあります(例: ccstatusline の解説)。この記事では、VS Code のパネルに出すことと、ペースの予報を出すことに絞ります。

ほかにも、利用枠を入力欄の上に出す mod(limits-meter)や、ペースを予測するツール(claumon など)があります。この記事の mod は、VS Code のパネルに出せることと、ほかに何も入れず mod だけでペースの予報まで出すことが違います。

トークン(利用枠)は使わない

使いません。mod は Claude Code というプログラムの中で動く部品で、AI には何も送りません。パネルの中身も AI には送られないので、利用枠を見張るために利用枠を使う、ということは起きません。

ただし、数字が新しくなるのは AI が返事をしたときだけです。何もしていない間は止まって見えます。ブラウザのチャットや別の PC で使った分も、次の返事のときに反映されます(数字はアカウント全体のものです)。

5. 作り方

作るものは、mod のファイル4つと、個人設定に足す2行だけです。

(1) ファイルを4つ作る

~/.claude/mods/context-meter/
├── .claude-plugin/
│   └── plugin.json      ← mod の名前と説明
├── hooks/
│   ├── hooks.json       ← 「コードは register.tsx」という指定
│   └── register.tsx     ← 本体
└── types/
    └── index.d.ts       ← 保存する値の型

中心は register.tsx のこの部分です。AI の返事が終わるたび(turn.complete)に、Claude Code が持っている数字($.session.usage())を読んで保存します。保存すると、パネルが自動で描き直されます。

// AI の返事が1回終わるたびに呼ばれる
on('turn.complete', async ($, e, next) => {
  const result = await next(e)
  const usage = await $.session.usage()
  // usage.rateLimits = [{ kind: 'five_hour', percentUsed: 36, resetsAt: '...' }, { kind: 'seven_day', ... }]
  await update($, limits, () => ({ list: usage.rateLimits.map(r => ({ kind: r.kind, percent: r.percentUsed, resetsAt: r.resetsAt })), at: now() }))
  return result
})

// 横のパネルを描く(保存した値が変わると描き直される)
on('ui.render', { component: 'Pane', requestId: 'context-meter' }, async ($, e) => {
  const { Box, Text } = $.ui.resolve(e)
  const lim = await read($, limits)
  // ... 使用率・リセット時刻・ペースの予報を並べる
})

4つのファイルの全文は、次の折りたたみに載せます。

.claude-plugin/plugin.json
.claude-plugin/plugin.json
{
  "name": "context-meter",
  "version": "0.1.0",
  "description": "会話の入れ物(コンテキスト)の埋まり具合を、返事のたびにパネルに棒グラフで出す",
  "types": "./types/index.d.ts"
}
hooks/hooks.json
hooks/hooks.json
{ "modules": ["./register.tsx"] }
types/index.d.ts
types/index.d.ts
// 1回の返事が終わったときの読み取り値
export type Reading = {
  percent: number // 0〜100
  tokens: number
  window: number
  at: string // 表示用の時刻
}

// アカウントの利用枠(設定画面の「使用量」と同じもの)
export type Limit = {
  kind: string // five_hour(現在のセッション) / seven_day(今週)
  percent: number
  resetsAt?: string // ISO 8601
}

export type Limits = { list: Limit[]; at: string }

declare module 'claude-code' {
  interface PluginState {
    'context-meter': {
      readings: Shaped<Reading[]>
      limits: Shaped<Limits | null>
    }
  }
}
hooks/register.tsx
hooks/register.tsx
// context-meter: 会話の入れ物(コンテキスト)の埋まり具合を、返事のたびにパネルに出す
//
// しくみ:
//   先生の返事が1回終わるたび(turn.complete)に、Claude Code が持っている「今の会話の大きさ」
//   ($.session.usage() の context)を読んで、パネルに棒グラフで描く
//   値を読むだけなので、トークンは使わない
//
// なぜ大事か:
//   会話が長くなると、入れ物が埋まって、古いやり取りが自動で要約(compact)される。
//   その前にメモリやメモに残し、/clear で区切ると、話がぶれにくい
//
// VS Code では入力欄の上の帯(AbovePrompt)が描かれないので、横のパネル(Pane)に出す
import { atom, read, update } from 'claude-code'
import type { EngineInterface, Register } from 'claude-code'

import type { Limit, Limits, Reading } from '../types'

const PANE = 'context-meter'
const TITLE = '会話メーター'
const THRESHOLDS = [50, 75, 90]
const KEEP = 30 // 推移として覚えておく回数

const readings = atom({ plugin: 'context-meter', key: 'readings' } as const, [] as Reading[], {
  shape: 'readings-1',
})
const limits = atom({ plugin: 'context-meter', key: 'limits' } as const, null as Limits | null, {
  shape: 'limits-1',
})

// 利用枠: 設定画面と同じ名前で出す。この割合を超えた瞬間に通知する
const LIMIT_NAMES: Record<string, string> = { five_hour: '現在のセッション(5時間)', seven_day: '今週' }
const LIMIT_ALERTS = [50, 80, 95]
const WEEK = ['日', '月', '火', '水', '木', '金', '土']

function resetText(iso?: string): string {
  if (!iso) return ''
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return ''
  const p = (n: number) => String(n).padStart(2, '0')
  const today = new Date()
  const sameDay = d.toDateString() === today.toDateString()
  return `${sameDay ? '' : `${d.getMonth() + 1}/${d.getDate()}(${WEEK[d.getDay()]}) `}${p(d.getHours())}:${p(d.getMinutes())}にリセット`
}

function limitColor(percent: number): string {
  return percent < 50 ? 'green' : percent < 80 ? 'yellow' : 'red'
}

// ---- ペースの予報 ----
// 枠の長さとリセットの時刻から「枠の始まり」を逆算し、経過の割合と使った割合を比べる
//   例: 週の枠で 4日/7日(57%)経過して 45% 使用 → このペースならリセット時に 45/0.57 ≒ 79%
const WINDOW_MS: Record<string, number> = { five_hour: 5 * 3600_000, seven_day: 7 * 86400_000 }

type Pace = {
  elapsedText: string // 経過 4.2日/7日(60%)
  status: 'early' | 'ok' | 'tight' | 'over' // 予報はまだ / 余裕あり / ぎりぎり / 使いすぎ
  forecast: string // このペースなら、リセット時に約79% など
}

function span(ms: number, kind: string): string {
  if (kind === 'seven_day') {
    const days = ms / 86400_000
    return days >= 1 ? `${days.toFixed(1)}日` : `${Math.round(ms / 3600_000)}時間`
  }
  const hours = ms / 3600_000
  return hours >= 1 ? `${hours.toFixed(1)}時間` : `${Math.round(ms / 60_000)}分`
}

function pace(l: Limit, nowMs: number): Pace | null {
  const win = WINDOW_MS[l.kind]
  if (!win || !l.resetsAt) return null
  const resetMs = new Date(l.resetsAt).getTime()
  if (Number.isNaN(resetMs)) return null
  const elapsedMs = Math.min(win, Math.max(0, nowMs - (resetMs - win)))
  const frac = elapsedMs / win
  const elapsedText = `経過 ${span(elapsedMs, l.kind)}/${span(win, l.kind)}(${Math.round(frac * 100)}%)`

  if (frac < 0.05) return { elapsedText, status: 'early', forecast: '始まったばかりなので、予報はまだ出せません' }

  const projected = l.percent / frac // このペースのまま、リセット時に何%か
  if (projected < 100) {
    return {
      elapsedText,
      status: projected < 90 ? 'ok' : 'tight',
      forecast: `このペースなら、リセット時に約${Math.round(projected)}%`,
    }
  }
  // 上限に届くまでの残り時間 = 残りの% ÷ 1ミリ秒あたりの使用量
  const rate = l.percent / Math.max(1, elapsedMs)
  const leftMs = Math.max(0, (100 - l.percent) / rate)
  return {
    elapsedText,
    status: projected < 110 ? 'tight' : 'over',
    forecast: `このペースだと、あと約${span(leftMs, l.kind)}で上限に届きます(リセットより${span(resetMs - nowMs - leftMs, l.kind)}早い)`,
  }
}

const PACE_LABEL: Record<Pace['status'], { text: string; color: string }> = {
  early: { text: '予報はまだ', color: 'gray' },
  ok: { text: 'ペースは余裕あり', color: 'green' },
  tight: { text: 'ペースはぎりぎり', color: 'yellow' },
  over: { text: '使いすぎのペース', color: 'red' },
}

function now(): string {
  const d = new Date()
  const p = (n: number) => String(n).padStart(2, '0')
  return `${p(d.getHours())}:${p(d.getMinutes())}`
}

function k(n: number): string {
  return n >= 1000 ? `${Math.round(n / 1000)}k` : String(n)
}

function bar(percent: number, width = 20): string {
  const filled = Math.round((percent / 100) * width)
  return '█'.repeat(filled) + '░'.repeat(Math.max(0, width - filled))
}

// 推移を1文字ずつの高さで表す(▁▂▃▄▅▆▇█)
function spark(list: Reading[]): string {
  const marks = '▁▂▃▄▅▆▇█'
  return list.map(r => marks[Math.min(7, Math.floor((r.percent / 100) * 8))]).join('')
}

function advice(percent: number): { text: string; color: string } {
  if (percent < 50) return { text: '余裕があります', color: 'green' }
  if (percent < 75) return { text: '半分を超えました。区切りのよいところでメモリに残すことを意識', color: 'yellow' }
  if (percent < 90) return { text: 'メモに残してから /clear を考える頃です', color: 'red' }
  return { text: '自動の要約(compact)が近いかもしれません。先にメモに残してください', color: 'red' }
}

async function record($: EngineInterface): Promise<void> {
  const usage = await $.session.usage()

  // アカウントの利用枠(先生の返事のたびに、Claude の API が最新の値を返してくる)
  if (usage.rateLimits.length > 0) {
    const old = await read($, limits)
    const list: Limit[] = usage.rateLimits.map(r => ({ kind: r.kind, percent: r.percentUsed, resetsAt: r.resetsAt }))
    await update($, limits, () => ({ list, at: now() }))
    const nowMs = Date.now()
    for (const l of list) {
      const oldOne = old?.list.find(o => o.kind === l.kind)
      const before = oldOne?.percent ?? 0
      for (const t of LIMIT_ALERTS) {
        if (before < t && l.percent >= t) {
          $.ui.toast(`利用枠: ${LIMIT_NAMES[l.kind] ?? l.kind}が${t}%を超えました(${resetText(l.resetsAt)})`)
        }
      }
      // 「使いすぎのペース」に変わった瞬間だけ知らせる(75%の警告より前に気づけるように)
      const p = pace(l, nowMs)
      const wasOver = oldOne ? pace(oldOne, nowMs)?.status === 'over' : false
      if (p?.status === 'over' && !wasOver) {
        $.ui.toast(`利用枠: ${LIMIT_NAMES[l.kind] ?? l.kind}が使いすぎのペースです。${p.forecast}`)
      }
    }
  }

  const c = usage.context
  if (c.percent === undefined || c.tokens === undefined) return
  const reading: Reading = { percent: c.percent, tokens: c.tokens, window: c.window, at: now() }

  const before = await read($, readings)
  const prev = before.length > 0 ? before[before.length - 1].percent : 0
  await update($, readings, list => [...list, reading].slice(-KEEP))

  // しきい値を超えた瞬間だけ通知する
  for (const t of THRESHOLDS) {
    if (prev < t && reading.percent >= t) {
      $.ui.toast(`会話メーター: ${t}%を超えました。${advice(reading.percent).text}`)
    }
  }
}

export const register: Register = on => {
  on('session.start', async ($, e, next) => {
    const result = await next(e)
    await $.command.register({
      name: 'context-meter',
      description: '会話の埋まり具合をパネルに出す(トークンを使わない)',
    })
    // 体感してもらうため、セッションの開始時にパネルを開く
    void $.ui.open({ id: PANE, title: TITLE })
    return result
  })

  on('turn.complete', async ($, e, next) => {
    const result = await next(e)
    await record($).catch(() => {})
    return result
  })

  on('command.run', { command: 'context-meter' }, async $ => {
    await record($).catch(() => {})
    await $.ui.open({ id: PANE, title: TITLE })
    const list = await read($, readings)
    const last = list[list.length - 1]
    return { text: last ? `会話メーター: ${last.percent}%(${k(last.tokens)} / ${k(last.window)})` : '会話メーター: まだ値がありません' }
  })

  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
    const { Box, Text } = $.ui.resolve(e)
    const list = await read($, readings)
    const last = list[list.length - 1]

    if (!last) {
      return (
        <Box flexDirection="column">
          <Text dimColor>先生の次の返事が終わると、ここに会話の埋まり具合が出ます。</Text>
        </Box>
      )
    }

    const prev = list.length >= 2 ? list[list.length - 2] : undefined
    const diff = prev ? last.percent - prev.percent : 0
    const a = advice(last.percent)
    const lim = await read($, limits)

    return (
      <Box flexDirection="column">
        <Text bold>アカウントの利用枠</Text>
        {!lim && <Text dimColor>(まだ値がありません)</Text>}
        {lim &&
          lim.list.map(l => {
            const p = pace(l, Date.now())
            return (
              <Box flexDirection="column">
                <Text>
                  {LIMIT_NAMES[l.kind] ?? l.kind}: <Text bold color={limitColor(l.percent)}>{l.percent}%</Text>{' '}
                  <Text dimColor>{resetText(l.resetsAt)}</Text>
                </Text>
                <Text color={limitColor(l.percent)}>{bar(Math.min(100, l.percent))}</Text>
                {p && (
                  <Text>
                    <Text dimColor>{p.elapsedText} → </Text>
                    <Text bold color={PACE_LABEL[p.status].color}>{PACE_LABEL[p.status].text}</Text>
                  </Text>
                )}
                {p && <Text color={PACE_LABEL[p.status].color}>  {p.forecast}</Text>}
              </Box>
            )
          })}
        {lim && <Text dimColor>最後の更新: {lim.at}(先生の返事のたびに更新。ほかの会話やチャットの分も含む)</Text>}

        <Box marginTop={1}>
          <Text bold>この会話の入れ物</Text>
        </Box>
        <Text bold>
          {last.percent}% <Text dimColor>({k(last.tokens)} / {k(last.window)} トークン)</Text>
        </Text>
        <Text color={a.color}>{bar(last.percent)}</Text>
        <Text color={a.color}>{a.text}</Text>
        <Box marginTop={1} flexDirection="column">
          <Text dimColor>
            前の返事から: {diff >= 0 ? `+${diff}` : diff}%  ・  最後の更新: {last.at}
          </Text>
          <Text dimColor>直近{list.length}回の推移(左が古い):</Text>
          <Text>{spark(list)}</Text>
        </Box>
        <Box marginTop={1} flexDirection="column">
          <Text dimColor>目安: 50%で区切りを意識 / 75%でメモに残して /clear / いっぱいに近づくと自動で要約される</Text>
        </Box>
      </Box>
    )
  })
}

(2) 個人設定に2行足す

~/.claude/settings.json の env に足します。

"env": {
  "CLAUDE_CODE_PLUGIN_DIRS": "C:\\Users\\<ユーザー名>\\.claude\\mods\\context-meter",
  "CLAUDE_CODE_PLUGIN_DIR_WATCH": "1"
}
  • CLAUDE_CODE_PLUGIN_DIRS: mod のフォルダを読み込む設定です。プロジェクトの設定には書けず、個人の設定だけです
  • CLAUDE_CODE_PLUGIN_DIR_WATCH: mod を直したときに自動で読み込み直す設定です(7章)

(3) VS Code を読み直す

Ctrl+Shift+P →「Developer: Reload Window」で読み直して、何か一言送ります。横に「会話メーター」のパネルが開き、AI の返事が終わると数字が出ます。

mod は、自分の権限そのままで動きます(サンドボックスはありません)。人が作った mod は、中身を確かめてから入れましょう。今回作った会話メーターは、利用枠の数字を読んで、パネルを描き、通知を出すだけです。ファイルの読み書きや通信はしません。

6. ペースの予報のしくみ

「時間が何%過ぎたか」と「枠を何%使ったか」を比べるだけです。使った割合の方が大きければ、このままだとリセット前に足りなくなります。

API から届くのは「使用率」と「リセットの時刻」だけなので、枠の始まりはリセットの時刻から逆算します。

\text{経過の割合} = \frac{\text{今} - (\text{リセットの時刻} - \text{枠の長さ})}{\text{枠の長さ}} \qquad \text{リセット時の見込み} = \frac{\text{使用率}}{\text{経過の割合}}

枠の長さは、5時間の枠が5時間、週の枠が7日です。2章の画面の数字で計算すると、次のようになります。

枠 経過 使用率 リセット時の見込み 表示
今週 1.8日 / 7日(26%) 12% 12 ÷ 0.26 ≒ 46% ペースは余裕あり
5時間 1.6時間 / 5時間(32%) 36% 36 ÷ 0.32 ≒ 112% 使いすぎのペース

見込みが100%を超えるときは、上限に届くまでの時間も出します。5時間の枠は1.6時間で36%、つまり1時間あたり約22.5%のペースです。残りの64%を使い切るまで約2.8時間で、リセットまでの残り約3.4時間より早く届きます。表とこの計算は経過を丸めているので、分単位で計算している画面の値(約47%、約2.9時間)とは少しずれます。

色は、リセット時の見込みで分けています。

見込み 表示 色
90%未満 ペースは余裕あり 緑
90%以上110%未満 ペースはぎりぎり 黄
110%以上 使いすぎのペース 赤
  • 「使いすぎのペース」へ変わった瞬間、通知も出します。75%の警告より前に気づくためです
  • 枠が始まったばかり(経過5%未満)のうちは、予報を出しません。最初の数分の使い方で、見込みが大きくぶれるからです
  • 予報は「今までと同じペースで使い続けたら」という前提です。休憩したり、重い作業をしたりすると変わります

7. つまずいたところ

どれも、VS Code 拡張で使うときだけの話です。

起きたこと 原因 どうしたか
入力欄の上に帯で出そうとしたが、表示されない 入力欄の上の帯(AbovePrompt)は、ターミナル版とデスクトップアプリでしか描かれない 横のパネル(Pane)に出すようにした
mod のファイルを直しても、表示が変わらない VS Code 拡張は、裏で SDK と同じ方式で動いている。この方式では、CLAUDE_CODE_PLUGIN_DIR_WATCH=1 がないとフォルダの変更を見張らない 個人設定に足した。それまでは、直すたびに VS Code を開き直していた
設定画面と数字が1%ずれる(今週 12% と 11%) 端数の丸め方の違いと思われる。更新されるタイミングも違う そのまま。リセットの時刻は一致していた

なお、会話メーターは Claude Code の画面の中(会話の右側)に表示されます。VS Code の好きな場所に単独で動かせるかは、確認できませんでした。

また、この記事の手順では出ませんが、Claude Code に mod を作ってもらうと、「Turn on the mods Claude writes in this session?(このセッションで Claude が書いた mod を読み込んで動かしますか?)」という英語の確認が出る場合もあります。mod は自分の権限そのままで動くプログラムなので、AI が書いたものを動かす前に、Claude Code が人に確かめる仕組みです。選択肢は3つです。

  • How does this work?: しくみの説明を見る(英語)
  • Turn them on: 今すぐ読み込んで動かす。そのあと Claude が mod を直しても、自動で読み込み直される。作ってもらった mod をすぐ試したいときはこちら
  • Not now: 今は動かさない。ファイルは残っていて、次にそのセッションを開いたときに読み込まれる。中身を先に確かめたいときはこちら

8. まとめ

はじめの3つの問いへの答えです。

  1. 利用枠を作業しながら見られるか: 見られます。mod で VS Code の横のパネルに出せば、Web の設定画面を開かなくてもよくなります
  2. 「このペースで足りるか」を先に知れるか: 知れます。経過の割合と使用率を比べれば、リセット時の見込みと、上限に届くまでの時間が出せます
  3. トークンを食わないか: 食いません。mod は Claude Code の中で動き、AI には何も送りません

Web の設定画面や /usage コマンドでも、今の利用状況は確認できました。ただ、「このペースで使っても大丈夫かな」と計算するのは面倒でした。

今回作った mod のおかげで、利用状況を毎回確かめに行くことも、ペース配分を自分で計算することもなくなりました。便利になったと感じています。

1
0
2

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
1
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?