AIエージェント時代のターミナル整備。リミットが見えれば、作業が止まらない

※この記事は「エムティーアイ Blog Summer 2026」の 6/8(月) 分の記事です。

スマートコンテンツ事業部で保守・開発を行っている 兒玉 です。
今回は、Claude Code や Codex CLI のターミナル表示を整えた話を紹介します。
前回の記事(2024 年のターミナル整備編)の続きとして読んでもらえると、少し文脈がつながると思います。

AI 活用について
本記事の構成・初稿の作成には Claude Code を一部利用しています。最終的な文章・公開判断は執筆者が行っています。記事の内容が Claude Code 活用とも関連しているため、こうした形での活用例として紹介します。

はじめに

Claude Code や Codex CLI を長時間使っていると、5時間制限や週次制限に唐突にぶつかることがあります。「今どのくらい使っているんだろう」と思いながら作業を続けていたところで、突然「Rate limited」が来る。「あと5分、ここだけ終わらせたかったのに」と思ったことが何度かありました。

前回の記事でターミナルを ghq・fzf・eza で整えてから、「次は AI エージェントの状態もここに出したい」とぼんやり思っていました。「でも設定がめんどうそう」と後回しにしていたのですが、調べてみたら Claude Code にも Codex CLI にも、すでにステータスライン機能が備わっていました。しかも Codex CLI は config.toml に1行追加するだけだった。

この記事では、実際に設定してみた内容と、そのときに踏んだ小さなバグの話、あわせて気になっていた Codex CLI のサンドボックス設定についても整理します。


Claude Code: ステータスラインで状態を全表示

Claude Code はステータスラインをカスタマイズできます。~/.claude/settings.json に以下を追加することで、任意の Node.js スクリプトをステータスバーとして表示できます。

"statusLine": {
  "type": "command",
  "command": "node ~/.claude/statusline.js",
  "padding": 0
}

スクリプトには Claude Code からセッション情報が JSON 形式で stdin に渡されるので、必要な情報を拾って整形・表示する仕組みです。

表示される情報はだいたいこんな感じです。

項目 内容
Git ブランチ名 今どのブランチで作業しているか
モデル名 現在使用中のモデル(例: claude-sonnet-4-6)
コンテキスト使用量 used / total + プログレスバー
5時間制限使用率 5h:XX% / 50% 超で黄色、80% 超で赤
週次制限使用率 7d:XX%
リセット時刻 @HH:MM(常時表示)
セッションコスト $X.XX

図1: Claude Code のステータスライン。5h と 7d の使用率は 50% 超で黄色、80% 超で赤に変わる。

図1b: 各要素の説明。

5時間制限が 80% を超えると赤く変わるので、「そろそろ区切りをつけよう」というタイミングが自然と意識できます。以前は「突然止まる」だったのが、「手前で気づいてキリのいいところで止める」に変わりました。

スクリプト自体は公式配布ではないため、自作するか GitHub などで共有されている実装を参考にする形になります。最低限 data.rate_limits.five_hour.used_percentagedata.rate_limits.seven_day.used_percentage を受け取れれば目的は達成できます。

@ 表示の謎と、踏んだバグ

設定を終えてしばらく使っていたら、ステータスバーに @1/21 23:48 という表示が出るようになりました。今は6月なのに、なぜ1月21日?

調べてみると、@ はリセット時刻を示すもので、同日リセットなら @17:30 のように時刻だけ、翌日以降なら @6/10 17:30 のように月/日つきで表示される仕組みでした。しかし1月というのはおかしい。

原因はスクリプトのバグでした。Claude Code の API は resets_atUnix タイムスタンプ(秒) で返すのに、new Date() はそれをミリ秒として解釈してしまっていたのです。1 秒を 1 ミリ秒として扱うと、現在時刻(約17億秒)は「1970年1月21日ごろ」に相当してしまいます。

// ❌ 修正前: 秒をミリ秒として解釈してしまう
const r = new Date(iso);

// ✅ 修正後: 数値なら ×1000 してからパース
const ms = (typeof val === 'number' || /^\d+$/.test(String(val)))
  ? Number(val) * 1000
  : val;
const r = new Date(ms);

修正後は @17:30 のように正しい時刻が表示されるようになりました。こういう「見た目は動いているけどデータはおかしい」系のバグは、実際に使い始めるまで気づけないのがつらいところです。


Codex CLI: config.toml に1行追加するだけ

Codex CLI のほうは、~/.codex/config.toml に1行追加するだけです。

# ステータスラインにリミット使用率を表示
tui.status_line = ["project", "git-branch", "five-hour-limit", "weekly-limit"]

Codex CLI を再起動すると、ウィンドウ下部に5時間制限と週次制限の残量がテキストで表示されるようになります。

my-project · feature/add-auth · 5h 62% left · weekly 28% left

(Claude Code が使用率を表示するのに対し、Codex CLI は残量(remaining)を表示します。62% left は「残り 62%」の意味です。)

図2: Codex CLI のステータスライン。5h と weekly がリアルタイムで更新される。

図3: config.toml への追記内容。この数行を追加するだけでステータスラインが変わる。

TUI 内で /statusline コマンドを使うと GUI から同じ設定ができます。どちらでもお好みで。


おまけ: Codex CLI のサンドボックス設定

ついでに気になっていた部分も整理しました。

Codex CLI はデフォルトで workspace-write モードで動いています。これはワークスペース外のファイルへの書き込みや外部ネットワーク通信ができない状態で、パッケージのインストールや外部 API を使うタスクを頼むと途中で詰まることがあります。

danger-full-access にすればすべての操作が可能になりますが、確認なしにバシバシ実行されるのは怖い。そこで approval_policy = "untrusted" と組み合わせると、「全権限あり、でも毎回確認を求める」という状態になります。

# パーミッション設定: 全操作を許可しつつ毎回確認を求める
sandbox_mode    = "danger-full-access"
approval_policy = "untrusted"

図4: sandbox_mode の3種類と approval_policy との組み合わせ。

普段よく使うプロジェクトだけは [projects."..."] セクションで trust_level = "trusted" にしておくと、そのプロジェクトでは自動承認になります。慣れ親しんだコードベースはスムーズに、初見のプロジェクトでは慎重に、という使い分けができます。


ソースコード全文

以下の2つを設定すれば、この記事の Claude Code 表示が再現できます。

▼ここをクリック ① ~/.claude/settings.json(statusLine セクションを追加)

既存の settings.json がある場合は、{} の中に "statusLine": { ... } を追記してください。

{
  "statusLine": {
    "type": "command",
    "command": "node ~/.claude/statusline.js",
    "padding": 0
  }
}

▼ここをクリック ② ~/.claude/statusline.js(ステータスライン本体・新規作成)

#!/usr/bin/env node
const fs = require('fs');
const { execSync } = require('child_process');

const C = {
  RST: '\x1b[0m', BOLD: '\x1b[1m', DIM: '\x1b[2m',
  GREEN: '\x1b[32m', YELLOW: '\x1b[33m', RED: '\x1b[31m',
  LRED: '\x1b[91m', CYAN: '\x1b[36m',
};

let inputBuf = '';
try { inputBuf = fs.readFileSync(0, 'utf-8'); } catch { }
let data = {};
try { data = JSON.parse(inputBuf); } catch {
  process.stdout.write(`${C.DIM}(no data)${C.RST}`);
  process.exit(0);
}

const model        = data.model?.display_name || 'Unknown';
const contextSize  = data.context_window?.context_window_size || 200000;
const usedPct      = data.context_window?.used_percentage || 0;
const fiveHourPct  = data.rate_limits?.five_hour?.used_percentage;
const sevenDayPct  = data.rate_limits?.seven_day?.used_percentage;
const fiveHourReset = data.rate_limits?.five_hour?.resets_at || '';
const sevenDayReset = data.rate_limits?.seven_day?.resets_at || '';
const costUsd      = data.cost?.total_cost_usd;
const cwd          = data.cwd || process.cwd();
const currentUsed  = Math.round((usedPct * contextSize) / 100);

function rateColor(p) { return p >= 80 ? C.RED : p >= 50 ? C.YELLOW : C.GREEN; }
function pctColor(p)  { return p >= 90 ? C.RED : p >= 70 ? C.LRED : p >= 50 ? C.YELLOW : C.GREEN; }
function fmt(n) {
  if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
  if (n >= 1e3) return (n/1e3).toFixed(1)+'k';
  return String(n||0);
}
function fmtReset(val) {
  if (!val) return '';
  try {
    // API は resets_at を Unix 秒で返す
    const ms = (typeof val === 'number' || /^\d+$/.test(String(val)))
      ? Number(val) * 1000 : val;
    const r = new Date(ms), n = new Date();
    const same = r.toDateString() === n.toDateString();
    const t = `${r.getHours()}:${String(r.getMinutes()).padStart(2,'0')}`;
    return same ? t : `${r.getMonth()+1}/${r.getDate()} ${t}`;
  } catch { return ''; }
}
function gitBranch(dir) {
  try {
    return execSync('git branch --show-current',
      {cwd:dir,encoding:'utf-8',stdio:['pipe','pipe','pipe']}).trim()
      || execSync('git rev-parse --short HEAD',
      {cwd:dir,encoding:'utf-8',stdio:['pipe','pipe','pipe']}).trim();
  } catch { return ''; }
}

const pct = Math.round(usedPct);
const bar = '█'.repeat(Math.min(Math.round(pct/10),10))
          + '░'.repeat(10-Math.min(Math.round(pct/10),10));

let sub = '';
if (fiveHourPct != null && sevenDayPct != null) {
  const fiveH = Math.round(fiveHourPct);
  const sevenD = Math.round(sevenDayPct);
  const fc = rateColor(fiveH), sc = rateColor(sevenD);
  sub = `${fc}5h:${fiveH}%${C.RST}`;
  const frs = fmtReset(fiveHourReset);
  if (frs) sub += ` ${fc}@${frs}${C.RST}`;
  sub += ` ${sc}7d:${sevenD}%${C.RST}`;
  const srs = fmtReset(sevenDayReset);
  if (srs) sub += ` ${sc}@${srs}${C.RST}`;
} else { sub = `${C.DIM}5h:--% 7d:--%${C.RST}`; }

let out = '';
const br = gitBranch(cwd);
if (br) out += `${C.CYAN}${br}${C.RST} ${C.DIM}|${C.RST} `;
out += `${C.BOLD}${model}${C.RST}`;
out += ` ${C.DIM}|${C.RST} ${fmt(currentUsed)}/${fmt(contextSize)} ${pctColor(pct)}${bar} ${pct}%${C.RST}`;
out += ` ${C.DIM}|${C.RST} ${sub}`;
if (costUsd > 0) out += ` ${C.DIM}|${C.RST} ${C.DIM}$${C.RST}${costUsd.toFixed(2)}`;
process.stdout.write(out);


おわりに

やってみて思ったのは、「リミットは突然来るもの」という受け身の感覚が、「残量を見ながら使う」という少し能動的な感覚に変わったということです。数字が見えているだけで、区切りのつけ方が変わる。

あと、バグを自分で踏んで直すというのは、設定ファイルをコピペして終わりにするより、仕組みを理解するちょうどいい機会になりました。@1/21 の謎を追いかけるところから、API が秒で返している、new Date() はミリ秒を期待している、という流れが頭に入ったので、次に壊れても怖くない気がしています。

こういう小さい整備がじわじわ効いてくる感じが、ターミナルカスタマイズの面白いところだと思います。設定はどちらも5分以内に終わるので、長時間作業が増えてきた方はぜひ試してみてください。


※出典
Customize your status line — Claude Code 公式ドキュメント
Claude Code Statusline Complete Guide Claude Lab
Claude Code ステータスラインでリセット時刻を表示 Zenn