Claude Codeの「待ち時間」を解消する!ステータスラインに残り時間・サブエージェント状況を表示するmod自作ガイド

Claude Codeの「待ち時間」を解消する!ステータスラインに残り時間・サブエージェント状況を表示するmod自作ガイド AI

Claude Codeに作業を依頼したあと、「あと何分くらいで終わるんだろう」と画面を見つめた経験はないでしょうか。

CIツールやダウンローダーなら進捗バーがあり、残り時間の目安が表示されます。ところがClaude Codeのようなコーディングエージェントは、依頼内容によって作業時間が大きく変わるため、「完了未知」の待ち時間になりやすいのが実情です。長いタスクほど「止まっているのか、動いているのか」が分からず落ち着かない、という声は少なくありません。

結論を先に言うと、この不安はmod(プラグイン)機能を使ってCLI自体の挙動を拡張することで解消できます。この記事では、筆者(CEOの一次体験)から出発し、

  1. ステータスラインに経過時間・残り時間の目安を表示するmodを自作し
  2. セッションを超えて使えるよう恒久化し
  3. 使ってみて気づいた不足(バックグラウンドのサブエージェント実行中は無表示になる問題)をさらに直した

という、実際に手を動かした開発の流れをそのまま紹介します。単発のTipsではなく、「標準機能に無いものを自分で作り、使いながら育てていく」という一連の過程を追体験できる記事です。

Claude Code標準の制約を正しく理解する

まず前提を整理しておきます。

  • 経過時間の表示は作れます。Claude Codeには応答のやり取りを計測する仕組みがあるため、「今何分経過しているか」は比較的簡単に表示できます。
  • 一方、正確な「残り時間」の予測機能は標準には存在しません。これはClaude Codeの欠陥ではなく、そもそもタスクの終了時点を事前に知る仕組み自体が無いためです。

つまり、これから紹介する仕組みで作れるのは、あくまで過去の経験に基づく「ヒューリスティックな目安」です。正確な予測ではない、という前提を最初に明示しておきます。過度な期待を煽らず、実際にできることだけを誠実に紹介するのが本記事の方針です。

mod(プラグイン)機能とは何か

Claude Codeには、ステータスライン表示・フック・スラッシュコマンドなど、CLI自体の挙動をカスタマイズできる「mod」という拡張機能の仕組みがあります。

ユーザーの入力や応答の開始・終了といった「イベント」にフックするコードを書くことで、標準機能には無い表示や振る舞いを自分で追加できます。「Claude Code自体を自分好みに改造できる」という事実は、多くのユーザーにとって初耳情報かもしれません。この拡張性こそが、今回紹介する「残り時間表示」を実現する土台です。

実際に作った仕組み(turn-eta mod・第一段階)

ステータスラインに「経過 X:XX / 残り目安 約X分」を表示する

Claude Codeには、ユーザーの依頼に応答するたびに発火するturn.start(応答開始)・turn.complete(応答終了)というイベントがあります。modはこれをフックして経過時間を測定できます。測定結果をステータスラインAPI($.ui.status())に渡すだけで、画面下部に自由な文字列を表示できます。

on('turn.start', async ($, e, next) => {
const startedAt = await $.clock.now()
const bucket = bucketFor(e.text) // 文字数でshort/medium/longに分類
const history = (await $.store.get('history')) ?? []
const estimateMs = estimateFrom(history, bucket) // 過去の同じ箱の平均値
// ...ステータスライン表示用のタイマーを起動...
return next(e)
})

on('turn.complete', async ($, e, next) => {
const result = await next(e)
if (!e.agentId && !e.isAborted) { // メインループの正常完了のみ記録
const history = (await $.store.get('history')) ?? []
history.push({ bucket, ms: e.durationMs })
await $.store.set('history', history)
}
return result
})

過去のターン時間を「プロンプトの文字数」で分類し、目安を算出する

プロンプトの文字数を「短い(80字未満)」「普通(300字未満)」「長い(300字以上)」の3つの箱に分け、同じ箱に入った過去のやり取りの所要時間の平均を「残り時間の目安」として使っています。文字数が多ければ必ず複雑な指示とは限りませんが、経験的には「ざっくりした傾向」をつかむには十分、というのが今回のアプローチです。

// 簡略化版
function bucketFor(text: string): 'short' | 'medium' | 'long' {
if (text.length < 80) return 'short'
if (text.length < 300) return 'medium'
return 'long'
}

$.store に蓄積して使い続けるほど精度が上がる設計

$.storeというmod専用の保存領域に、ターンが完了するたびに「どの文字数の箱だったか」「実際に何ミリ秒かかったか」を記録していきます。これはセッションをまたいで保存されるため、使い続けるほど過去データが増え、目安の精度が上がっていく設計です(直近100件のみ保持し、古いものは自動で捨てる仕組みになっています)。

// 簡略化版
const history = (await $.store.get('history')) ?? []
history.push({ bucket, ms: durationMs })
await $.store.set('history', history) // セッションをまたいで保存される

動作確認(第一段階)

modを書いたら、claude plugin validate <modのフォルダ>を実行します。これを実行すると、modが実際にどのイベントをフックし、どのAPI($.以下の呼び出し)を使っているかを一覧表示してくれます。今回は「作者情報が未設定」という警告(警告止まりで動作には影響なし)のみで、エラーはありませんでした。

$ claude plugin validate ~/.claude/mods/turn-eta

Validating plugin manifest: .../turn-eta/.claude-plugin/plugin.json

⚠ Found 1 warning:
❯ author: No author information provided. Consider adding author details for plugin attribution

Validating hooks: .../turn-eta/hooks/hooks.json
❯ ./register.ts hooks: turn.start, turn.complete, tool.call{tool=Agent}
❯ ./register.ts calls: $.agent.list, $.clock.every, $.clock.now, $.store.get, $.store.set, $.ui.status

✔ Validation passed with warnings

開発用フォルダにmodのファイルを置くと、Claude Codeが「このセッションでホットリロードを有効にしますか?」と最初の一度だけ確認してきます。有効にすると、以後はファイルを保存するだけで自動的にmodが再読み込みされ、claudeを再起動せずに動作確認ができます。「保存すれば常に自動反映される」のではなく、「セッション内で最初の一度だけ確認ダイアログが出る」という点が実際の挙動です。実機で動作確認し、ステータスラインに経過時間・残り時間の目安が正しく表示されることを確認しました。

「セッションが終わると消える」問題と恒久化対応

ここで一つ問題に気づきました。作ったmodは開発用の一時フォルダ(セッション単位のディレクトリ)に置いていたため、そのセッションを閉じると読み込まれなくなる仕様だったのです。「次回以降もずっと使いたい」という要望を受けて、恒久的に使えるよう対応しました。

対応内容は次の2点です。

  1. modのフォルダをそのまま~/.claude/mods/以下にコピーする
  2. ~/.claude/settings.jsonのenv.CLAUDE_CODE_PLUGIN_DIRSにそのフォルダのパスを設定する
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/turn-eta"
}
}

(記事では読者ごとに異なる実際のユーザー名を含む絶対パスではなく、~〔ホームディレクトリ〕記法に一般化して紹介しています。)

ここで注意したいのが、この設定はプロセス起動時に読み込まれる環境変数という仕様のため、設定ファイルを書き換えたその瞬間には反映されない、という点です。変更を行ったその場のセッションには適用されず、次にClaude Codeを起動したときから有効になります。「設定を変えてもすぐには反映されないことがある」という、実務上のちょっとしたハマりどころとしてここに記録しておきます。

「サブエージェント待機中は無表示」という気づきと機能拡張

恒久化まで終えて一安心していたところ、別の作業をバックグラウンドのサブエージェントに任せている間、ステータスラインに何も表示されないことに気づきました。

原因を調べると、最初に作ったmodは「メインの会話ターンが開始・終了したタイミング」(turn.start/turn.complete)だけを見ていたため、バックグラウンドで動くサブエージェントの状態はそもそも監視対象に入っていなかったのです。Claude Codeの仕組み上、サブエージェントはメインループとは別の実行ループで動作するため、サブエージェントが裏で動いている間はメインループのturn.start/turn.completeが発火せず、表示が更新されない——これは実装・仕様の両面から見て当然の制約でした。これに気づいたことが、次の機能拡張のきっかけになりました。

対応として、以下の2つを追加しました。

サブエージェントの呼び出しを捉えるフック

モデルがサブエージェントを起動する「Agentツール」を呼んだタイミングをtool.callフックで捉え、起動されたサブエージェントのIDと開始時刻を記録します。

on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
const result = await next(e)
if (typeof result?.agentId === 'string') {
backgroundAgents.set(result.agentId, await $.clock.now())
}
return result
})

1秒間隔のポーリングで実行中サブエージェントを表示

あとは1秒ごとのタイマーで$.agent.list()(起動中のサブエージェント一覧を返すAPI)を呼び、まだ動いているものだけを数えて「裏で◯件実行中(最長◯分)」とステータスラインに表示する仕組みです。

$.clock.every(1000, async () => {
const infos = await $.agent.list()
const running = infos.filter(a =>
['pending', 'running', 'waiting'].includes(a.status)
)
// runningの件数・最長経過時間をステータスラインに表示
})

拡張後もclaude plugin validateで検証を通し、開発用フォルダ・恒久化フォルダの両方に反映しました。「作って満足せず、使ってみて見つかった不足をすぐに直す」——この改善サイクルそのものが、mod開発ならではの面白さだと感じています。

注意点・限界

実際に使ってみて分かった限界もまとめておきます。

  • 残り時間はあくまでヒューリスティックな目安であり、正確な予測の保証ではありません。
  • プロンプトの文字数という単純な指標に基づくため、内容の複雑さまでは反映できません。
  • 恒久化の設定変更は、環境変数の仕様上即時反映されません(次回のClaude Code起動から有効)。
  • 自分の使い方のクセに合わせて、文字数の分類の閾値(80字・300字)を調整すると、目安の精度が上がっていく可能性があります。

まとめ

Claude Codeの標準機能には「残り時間の目安表示」はありません。しかし、mod(プラグイン)機能を使えば、CLIの挙動自体を自分で拡張して、こうした「あったらいいな」を自分の手で実現できます。

今回紹介したturn-eta modは、

  1. ステータスラインへの経過時間・残り時間目安の表示
  2. セッションを超えて使えるようにする恒久化
  3. バックグラウンドのサブエージェント状況の表示拡張

という3段階を経て育てていきました。大事なのは「作って終わり」ではなく、「使ってみて気づいた不足を、すぐに直していく」という姿勢です。同じような「表示されてほしい情報がない」という不満をお持ちの方は、ぜひmod機能を調べて、自分だけのClaude Code拡張を作ってみてください。

Views: 0