Hooks:ユーザーフック実行チェーン
役割
Cline の hooks システムは、主 agent loop の複数のライフポイント (prompt 送信前、ツール使用前、ツール使用後、タスク開始/終了、通知送信、圧縮前) でユーザー独自のシェルスクリプトを差し込めるようにする。スクリプトは stdin で構造化 JSON 入力を受け取り、stdout で JSON を返して Cline の挙動を制御できる。最も重要なのは cancel: true で直前のアクションを阻止できる点と、contextModification で対話に追加コンテキストを差し込める点である。全体は 3 層からなる。HookExecutor がエントリとしてディスパッチし (executeHook:58)、HookFactory + HookRunner が具体スクリプトの発見と実行を担い (HookRunner exec:289)、HookProcess が底层の子プロセスを管理する (HookProcess:89)。
これは Task ライフサイクル全体にまたがる。PreToolUse / PostToolUse は ToolExecutor がツール実行前後に呼び (PreToolUse executeHook:73)、UserPromptSubmit / TaskStart / TaskResume / TaskCancel は主 Task が対応ライフポイントで呼ぶ (UserPromptSubmit:1214)。Notification は NotificationHook モジュールが一元送出する (emitNotificationHook:50)。hook の「ブロック可能」セマンティクスはキャンセル可能点でのみ有効で、Notification のような fire-and-forget の hook は cancel を返しても無視される。
設計動機
- 9 種の標準 hook:
PreToolUse/PostToolUse/UserPromptSubmit/TaskStart/TaskResume/TaskCancel/TaskComplete/Notification/PreCompact(Hooks interface:102) で、主要ライフポイントを網羅。 - JSON in / JSON out:hook スクリプトが受け取る stdin は
clineVersion/hookName/timestamp/workspaceRoots/userIdなどのメタデータ付き JSON (completeParams:198) で、出力も JSON。 - 3 種の返却フィールド:
cancelはアクションをブロック、contextModificationはコンテキスト追加、errorMessageはエラー説明を添付 (HookExecutionResult:33)。 - cancellable フラグ:呼び出し元がこの hook でアクションをキャンセルできるかを決める。
NotificationはisCancellable: false(not cancellable:61)、PreToolUse は true。 - プロセス隔離 + レジストリ:
HookProcessRegistryが全アクティブ hook プロセスを追跡し、terminateAllが task キャンセル時に一括 kill する (HookProcessRegistry:17)。 - クロスプラットフォームランチャー:
getHookLaunchConfigは Windows PowerShell と Unix shebang を切り分け (getHookLaunchConfig:44)、ユーザーが.sh/.ps1/.jsいずれでも実行できるようにする。 - stdout ストリーミングエコー:HookProcess は stdout/stderr を行単位で emit し、
streamCallbackが[source stream path]プレフィックスを付けて UI にエコーする (streamCallback:124)。長時間 hook でも進捗が見える。 - JSON 抽出のフォールバック:hook スクリプトが JSON の前後にデバッグログを混ぜて出力した場合、後ろから括弧の対応を走査して最後の完全な JSON オブジェクトを取り出す (
JSON extraction:379)。
主要ファイル
Hooks interface:102— 9 種の hook 名 + 各入力データ型。HookRunner:165— 抽象 runner。runメソッドは stateless で再利用可能。completeParams:198— hook 入力に clineVersion / hookName / timestamp / workspaceRoots / userId を補完。ConcreteHookRunner exec:289— 実際に HookProcess を起動してスクリプトを実行する中核メソッド。new HookProcess:324— hook 子プロセスを構築し、abort signal と streamCallback をバインド。honor JSON regardless of exit code:454— 合法 JSON があれば exitCode より優先。executeHook:58— エントリ。統一エラー処理、UI 状態、cancel セマンティクス。reorderHookAndToolMessages:109— PreToolUse 時に hook UI をツール UI の上に並べ替え。cancel handling:166—cancel: true受信時に hook 状態を cancelled にして返す。HookProcess:89— EventEmitter を継承し、子プロセスを起動して行単位で emit。HookProcess.run:123— 実際に子プロセスを spawn し、stdin/stdout/stderr/exit をバインド。timeout:187— タイムアウトで SIGTERM。エラーメッセージにスクリプトパスを含む。HookProcessRegistry:17— 静的レジストリ。task キャンセル時に一括 terminate。PreToolUseHookCancellationError:5— 専用例外クラス。上位の catch が hook キャンセルと通常失敗を区別できる。emitNotificationHook:50— Notification hook の fire-and-forget ラッパー。PreToolUse caller:73— ツール実行前の hook 呼び出し。cancel なら例外を投げて cancelTask。PostToolUse caller:471— ツール実行後の hook 呼び出し。cancel は後続を中断するのみ。
データフロー
PreToolUse を例に取る。これが最も完全な「ブロック可能」チェーンである。ToolExecutor は実際にツールを実行する前に ToolHookUtils.executePreToolUseHook を呼ぶ:
// apps/vscode/src/core/task/tools/utils/ToolHookUtils.ts
const { executeHook } = await import("@core/hooks/hook-executor")
const pendingToolInfo: any = { tool: block.name }
if (block.params.path) pendingToolInfo.path = block.params.path
if (block.params.command) pendingToolInfo.command = block.params.command
// ...
const preToolResult = await executeHook({
hookName: "PreToolUse",
hookInput: {
preToolUse: { toolName: block.name, parameters: block.params },
},
isCancellable: true,
say: config.callbacks.say,
setActiveHookExecution: config.callbacks.setActiveHookExecution,
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
messageStateHandler: config.messageState,
taskId: config.taskId,
hooksEnabled,
model: getHookModelContext(config.api, config.services.stateManager),
toolName: block.name,
pendingToolInfo,
})
if (preToolResult.cancel === true) {
await config.callbacks.clearActiveHookExecution()
await config.callbacks.cancelTask()
throw new PreToolUseHookCancellationError(preToolResult.errorMessage || "PreToolUse hook requested cancellation")
}
if (preToolResult.contextModification) {
ToolHookUtils.addHookContextToConversation(config, preToolResult.contextModification, "PreToolUse")
}この処理は PreToolUse caller:73 付近にある。executeHook に入ったらまず hooksEnabled を見 (early return:72)、オフならそのまま返す。次に HookFactory.hasHook でこの hook 名にスクリプトが設定されているか確認 (hasHook:80)。未設定でも即返す。設定されていれば HookProcess を作って子プロセスを spawn する:
// apps/vscode/src/core/hooks/HookProcess.ts
this.childProcess = spawn(launchConfig.command, launchConfig.args, {
stdio: ["pipe", "pipe", "pipe"],
shell: launchConfig.shell,
detached: launchConfig.detached,
cwd: this.cwd,
windowsHide: true,
})
this.timeoutHandle = setTimeout(() => {
if (this.childProcess && !this.isCompleted) {
this.childProcess.kill("SIGTERM")
reject(new Error(`Hook execution timed out after ${this.timeoutMs}ms.`))
}
}, this.timeoutMs)
this.childProcess.stdout?.on("data", (data) => {
this.stdoutBuffer += output
this.handleOutput(output, didEmitEmptyLine, "stdout")
})
// ...
this.childProcess.on("close", (code, signal) => {
this.exitCode = code
// resolve / reject
})
this.childProcess.stdin?.write(inputJson)
this.childProcess.stdin?.end()この処理は spawn child:176 付近にある。子プロセス終了後、HookRunner.exec が stdout を parseJsonOutput に渡す (parseJsonOutput:349)。合法 JSON があれば JSON を優先 (exitCode が非 0 でも優先) (JSON priority:454)。なければ exitCode で判断する。cancel: true は HookExecutor.executeHook まで伝播し、ToolHookUtils が受け取ると PreToolUseHookCancellationError を投げて cancelTask を呼ぶ。
境界と失敗
- PreToolUse cancel は task 全体を abort:単にこのツールをスキップするだけでなく、
cancelTaskで agent loop 全体を止める (cancelTask on cancel:100)。 - PostToolUse cancel は task を abort しない:
runPostToolUseHookは cancel 受信時も say error して true を返すだけ。ツールは実行済みでロールバックできない (post cancel soft:494)。 - Notification hook は cancel と contextModification を無視:この種の hook は一方向通知で、出力フィールドは明示的に無視される (
ignore unsupported output:70)。 - abort signal は即 reject:
HookProcessは abortSignal を監視し、abort されたら即座に子プロセスを kill して reject する。close を待たない (abortHandler:139)。 - タイムアウトはスクリプトパスでエラー:timeout エラーメッセージは
this.scriptPathとタイムアウトミリ秒を含み、ユーザーがどの hook が固まったかを特定しやすい (timeout error:192)。 - 出力は 1MB 上限:
MAX_HOOK_OUTPUT_SIZEが無限出力をブロックし、超過すれば切り詰め通知を emit (output truncation:311)。 - contextModification の切り詰め:
MAX_CONTEXT_MODIFICATION_SIZEが hook による過大コンテキスト注入を防ぎ、超過すれば切り詰めて通知を付ける (context truncation:363)。 - PreToolUse は attempt_completion をスキップ:
attempt_completionは終了ツールで、PreToolUse ブロックを経ない (skip attempt_completion:30)。 - NoOp hook は proto デフォルトを返す:スクリプトが見つからない hook は
cancel: false / contextModification: "" / errorMessage: ""を返し、executor はこれを正常 no-op とみなす (NoOp runner:238)。 - JSON 抽出は右から左へ走査:hook スクリプトが前にデバッグログを出して初回 JSON.parse が失敗した時、
parseJsonOutputは括弧の対応で最後の完全なオブジェクトを探す (brace scan:385)。このフォールバックが重要。
まとめ
Hooks システムは Cline がユーザーのために残した「agent loop の主要点にスクリプトを差し込む」拡張ポイントである。主 Task がどのタイミングで hook を発火させるかは agent-loop/task-class、ツール実行器が PreToolUse / PostToolUse をどう直列化するかは cap-tools、StateManager が hook に workspaceRoots などのメタデータをどう提供するかは state-manager を参照のこと。