Claude Code modのprompt.changeで履歴呼び出しとundoを拾う
prompt.changeは、入力欄の文字がタイプ以外の理由で変わったときに呼ばれるmodのフックです。拾える4つの経路と、prompt.submit・prompt.editとの違い、型定義での確かめ方をまとめます。
prompt.changeは「タイプ以外」で入力欄が変わった瞬間を拾うフック
prompt.change は、Claude Code v2.1.300で加わったmodのフックです。入力欄の文字が、キーボードで打った編集以外の理由で変わったときに呼ばれます。changelogが挙げる経路は4つです。
| 経路 | 入力欄で起きること | 対応する操作 |
|---|---|---|
| 履歴の呼び出し | 入力欄で起きること過去のプロンプトが入力欄に入る | 対応する操作空の入力欄で Up、または Ctrl+R で選ぶ |
| クリア | 入力欄で起きること入力欄の文字が消える | 対応する操作Esc を2回、または Ctrl+C の1回目 |
| 取り消し(undo) | 入力欄で起きること直前の入力に戻る | 対応する操作Ctrl+_(Ctrl+Shift+-) |
| プラグインによる入力(fill) | 入力欄で起きることプラグインが入力欄に文字を入れる | 対応する操作modの prompt.fill など |
右列の操作はインタラクティブモードのキー一覧に載っているもので、changelogの4語がどのキーに当たるかを並べたものです。changelogは「a clear」と書くだけで、/clear コマンドを指すとは書いていません。/clear は新しいセッションを始めるコマンドなので、ここでは入力欄を空にする操作として扱います。
履歴の呼び出しで入力欄に入る文字列の範囲も、インタラクティブモードのページに書かれています。Up で呼び出せるのは、同じプロジェクトで過去のセッションに打ったプロンプトまでです。履歴は作業ディレクトリごとに保存されます。/clear で新しいセッションを始めると、新しいセッションのプロンプトが先に並び、前のセッションのものはその後ろに続きます。同じプロンプトを2回続けて送っても履歴は1件にまとまるので、Up は直前とは別のプロンプトへ進みます。貼り付けを含むプロンプトを呼び出した場合は、再送信のときに貼り付けた全文があらためて送られます。
このフックが埋めるのは、prompt.edit と prompt.submit のあいだです。prompt.edit はユーザーが入力欄を編集したときに呼ばれ、prompt.submit は送信の直前に呼ばれます。履歴を呼び出して Enter を押すまでのあいだ、入力欄の中身はタイプなしで入れ替わります。prompt.change は、そうした変化を見るためのフックです。
似たイベントとの切り分け
入力欄まわりのイベントは複数あります。リファレンスのイベント表は、次のように並べています。
| イベント | 呼ばれるとき | 返せるもの |
|---|---|---|
prompt.edit | 呼ばれるときユーザーが入力欄を編集する | 返せるものnext(e) |
prompt.fill・prompt.suggest | 呼ばれるとき文字が下書きとして、または薄い提案として入力欄に入る直前 | 返せるもの文字を変えた next(e) |
prompt.submit | 呼ばれるときプロンプトが送信される | 返せるもの書き換え・context の追加・{ drop: reason } |
prompt.change | 呼ばれるときタイプ以外の理由で入力欄の文字が変わる | 返せるものリファレンスの表に行がない |
最後の行が要点です。prompt.change は、changelogの1行には現れますが、modのリファレンスのイベント表には載っていません。prompt.fill は、文字が入る直前に変えられるフックです。一方の prompt.change は、changelogの文面が「raised when」なので、起きた変化を知らせるイベントとして使うのが自然です。ただし、prompt.change が変更前に呼ばれるのか、変更後に呼ばれるのかは、changelogにもリファレンスにも書かれていません。
リファレンスのprompt系イベントには、ほかに prompt.autocomplete・prompt.compose・prompt.section・prompt.context・prompt.attachment・prompt.mention があります。このうち compose・section・context・attachment は、システムプロンプトやClaudeに渡す文脈・メッセージを扱うイベントで、入力欄の文字とは別の話です。prompt.mention は@で指したファイルを読む直前に呼ばれます。
入力欄に近いのは prompt.autocomplete です。リファレンスによると、候補の行を選ぶとその text が入力中の単語に上書きされます。この上書きが prompt.change として呼ばれるのかどうかは、どちらのページにも書かれていません。次節のログ用フックを両方のイベントに付けて、手元で確かめられます。
送信側の書き換えと中止はprompt.submitの記事にあります。ここでは、入力欄が変わったことを知る側に絞ります。
最小のフック: 呼ばれたことだけを記録する
イベントの入力や返り値の形が文書にない以上、最初の一歩は「何も変えずに通す」フックです。観測の型は、modのeventsページの tool.call の例と同じで、next(e) をそのまま返します。
// hooks/register.js: prompt.changeが呼ばれた回数を画面に出す例
let count = 0
export function register(on) {
on('prompt.change', async ($, e, next) => {
count += 1
$.ui.log('prompt.change が ' + count + ' 回呼ばれました')
// 何も変えず、そのまま次のハンドラへ渡す
return next(e)
})
}$.ui.log は、トランスクリプトに薄い行を出すmods APIのメソッドです。eventsページの tool.call の例では、● my-mod: Claude is about to use Bash のようにmod名付きで表示されると説明されています。
register を export し、引数の on でイベントごとにフックを登録する外枠は、modの作り方のページの最初の例と同じです。コードの置き場所は hooks/hooks.json の modules に書きます。
claude plugin validate ./my-mod出力の hooks: の行に prompt.change が並べば、登録は読めています。イベント名を綴り違えると、"prompt.changes" is not an event のようなエラーになります。この検証は、フックのコードを実行せずに静的に行われます。
動かして確かめるには、--plugin-dir でmodを読み込んだセッションで、次の操作を順に試します。
- 1
履歴を呼び出す
何か打って
Enterで送り、空の入力欄でUpを押す - 2
入力を消す
文字を打ったまま
Escを2回押す - 3
取り消す
文字を打ってから
Ctrl+_を押す - 4
普通に打つ
普通にキーを打つ
1から3でログが増え、4では増えないと想定できますが、実際の挙動は手元のバージョンで見てください。changelogは「typed edit以外」としか書いていないので、貼り付けやIME確定が打鍵として扱われるかは文書から決められません。
型定義が、このフックの契約を知る唯一の手がかり
modのドキュメントは、イベントと入力・返り値の正確な形について、次の立場を取っています。--plugin-dir でmodを読み込んだ対話セッションでは、動かしているバージョンの型が .claude-plugin/types/ に書き出されます。そして、ドキュメントとファイルが食い違ったら、ファイルを信じるように書かれています。
prompt.change の入力に何のフィールドがあるかは、ここで調べます。
grep -n "prompt.change" -B2 -A12 \
./my-mod/.claude-plugin/types/claude-code/index.d.ts確かめたい点は4つです。
- イベント
eに、入力欄の新しい文字列を持つフィールドがあるか - 変更前の文字列や、変更の経路(履歴・クリア・undo・fill)を区別するフィールドがあるか
- 返り値が
next(e)だけなのか、prompt.fillのように文字を差し替えられる形なのか - 型の隣にコメントと例があるか(
claude-code/index.d.tsは、各mods APIメソッドにコメントと例が付いたファイルだと説明されています)
この4点の答えで、フックの用途が観測だけか、書き換えまで含むかが決まります。ここを確かめずに e.text を読む書き方をコピーすると、prompt.submit の形を prompt.change に持ち込むことになります。
型定義が書き出されるのは、--plugin-dir で読み込んだ対話セッションか、Claudeに書かせたmodです。
使いどころは「入力欄に連動して何かを描き直す」場面
観測だけのフックにも居場所はあります。入力欄の中身をもとに何かを表示するmodは、タイプのたびに呼ばれる prompt.edit だけでは取りこぼします。履歴の呼び出しやundoで文字が入れ替わったときに prompt.edit が呼ばれるかどうかは、リファレンスに記載がありません。手元で試せば分かる確認事項です。
たとえば、入力欄の内容に応じた行を描くpaneがあるとします。描き直しの要求は、mods APIの $.ui.invalidate('ui.render') で出せます。再描画は毎秒10回までに間引かれ、それより速い呼び出しはまとめられます。次は書き方の例です。
// 書き方の例: 入力欄が変わったら、自分のpaneの再描画を頼む
on('prompt.change', async ($, e, next) => {
$.ui.invalidate('ui.render')
return next(e)
})paneの描き方はmodでpaneを描く記事で扱っています。prompt.change は、そのpaneを入力欄の状態に追従させる入口として使える位置にあります。
なお、v2.1.300ではプロンプト欄の装飾に virtualText も加わりました。入力欄に描かれるだけで、下書きにも送信されるプロンプトにも入らない文字です。changelogの1行以外に説明がないため、prompt.change と組み合わせる例はここでは書きません。
実行時間・失敗時・登録の数え方は他のフックと同じ
リファレンスのLimits表で個別の上限が付いているのは prompt.edit(50ミリ秒)だけで、prompt.change の行はありません。そのため、modのフック共通の規則が当てはまります。
| 項目 | 規則 |
|---|---|
| 実行時間の上限 | 規則フック自身の実行時間は10秒。prompt.edit だけが50ミリ秒 |
.catch ハンドラ | 規則実行時間は1秒 |
| 失敗したとき | 規則next を呼ぶ前なら、そのフックは飛ばされて次のハンドラが動く |
| 同じイベントの登録 | 規則matcherなしで同じイベントを2回登録すると、モジュールの読み込みが失敗する |
matcherは、on の第2引数にイベントのフィールドと比べるオブジェクトを渡す仕組みです。値・配列・正規表現が使え、eventsページの例はすべて tool.call の tool フィールドです。prompt.change にどのフィールドがあり、matcherで絞れるのかは文書にないので、型定義で確かめるまでは絞り込みを前提にしません。同じイベントを2回登録したくなったら、1つのフックにまとめて中で分けます。
// matcherなしで2回 on() すると読み込みに失敗するため、1つにまとめる
on('prompt.change', async ($, e, next) => {
$.ui.log('入力欄が変わりました')
$.ui.invalidate('ui.render')
return next(e)
})実行時間の表で50ミリ秒と別扱いなのは prompt.edit だけです。入力欄に近いイベントでも prompt.change が同じ短さかどうかは、表から読めません。重い処理は入れず、$.ui.log や再描画の要求のような軽い操作に留めるのが安全です。
失敗時の挙動は、prompt.submit と違って「止める」用途がないので、通常は影響が小さい部類です。それでも、next を呼ぶ前に例外を出すとそのフックは飛ばされ、1行のメッセージが残ります。my-mod: prompt.change hook skipped: threw Error: boom のような形です。
フックが期待どおりに動かないときの切り分けはmodが動かないときの記事に症状別の表があります。
テストでは、まず登録が読めているかを見る
claude plugin test は、セッションもサインインもなしにフックを検証します。ただし、prompt.change を発火するスタブの書き方は、modのリファレンスにもeventsページにも載っていません。そのため、発火側のテストを書く前に、claude plugin validate の hooks: 行で登録が読めていることを先に見ます。テストキットの書き方はmodのtestの記事にあります。発火の形が型定義にあれば、そこから書けます。
テストの実行は、modのディレクトリで次のコマンドを打つだけです。セッションにもサインインにもネットワークにも依存しません。
claude plugin test出力にはテストごとの結果と件数が並びます。公式の例では、次の形です。
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.19s]見るのは (pass) と (fail) の行、それと末尾の pass と fail の件数です。かっこ内のミリ秒は実行のたびに変わります。この例はツール呼び出しを $.tool.call で発火させて、フックの結果を確かめています。入力欄の変化に当たる呼び出しがあるかは、型定義の $ の項目で探すことになります。
まとめ
prompt.change は、入力欄の文字が履歴呼び出し・クリア・undo・プラグインの入力で変わったときに呼ばれる、観測向きのフックです。prompt.edit が拾えない経路を埋める位置にありますが、入力の形も返り値も文書に載っていません。最初は next(e) を返すだけのフックで呼ばれ方を観察し、型定義で契約を確かめてから、書き換えや連動表示に進むのが確実です。