CH 21 · フックプラグインとインターセプト:ツール実行前に手を加える
本章のゴール
CH 20 ではモデルに自分のツールを呼び出させました。本章ではさらに一歩踏み込みます:ツール実行の前後で自分のロジックを差し込む——誰が何を呼んだかを記録し、呼ぶべきでないツールを遮断し、通すか拒否するかを決定する。
これが「フックプラグイン」です。dsh の権限システム、サンドボックス、監査機能の基礎であり、「すべてはプラグイン」を最も典型的に体現したものです。
ツール呼び出しは一直線ではない
CH 11 で一つの概念に触れました:モデルがツールを呼ぶと言っても直接実行されるのではなく、拡張可能なパイプラインを通ります。公式はこれを「守られたパイプライン」としました——各段階はプラグインがインターセプト・拡張できます。
ツール呼び出しは次の段階を経ます:
モデルがツールを呼ぶ
↓
pre-execute → ポリシーゲート:許可 / 拒否 / 確認(権限・サンドボックス・インターセプトはすべてこの層)
↓
guard → 単調ガード:一度拒否したら後続リスナーで覆せない(最終防衛線)
↓
execute → ツールを実際に実行
↓
post-execute → 結果変換:戻り値の書き換え、内容の追加
↓
result → 読み取り専用の観測:結果をちらっと見る、変更は不可
↓
結果がモデルへ戻る「フック」はある段階に取り付けられるプラグインです:ctx.on('tools/xxx', ...) で対応イベントを購読し、イベント内でやりたいことを実行します。
各拡張ポイントが何ができるか、公式ドキュメントに明確な表があります:
| 拡張ポイント | 役割 | 典型的な用途 |
|---|---|---|
tools/pre-execute | ツール実行前の意思決定層 | 許可 / 拒否 / 確認、権限ゲート |
ctx.tools.guard() | 単調な最終拒否 | 後続リスナーで覆せないハードリミット |
tools/execute | ディスパッチ周期全体を包む | タイムアウト・リトライ・指標収集の追加 |
tools/post-execute | 結果を明示的に変換 | 表示内容の置換、モデル可視コンテキストの追加 |
tools/result | 不変結果を読み取り専用で観測 | 監査ログ・統計、変更不可 |
pre-execute はウォーターフォール型イベントです:リスナーは next()(許可)または { kind: 'deny', reason: '...' }(拒否)を返せます。
ハンズオン:「監査 + インターセプト」フックプラグインを書く
記録と、指定ツールの拒否を両方行うフックプラグインを書きます。拒否リストは設定可能にします——「プラグインは設定可能」という機能も同時に練習します。
ステップ 1:ディレクトリ作成、依存パッケージインストール
プラグインを配置したいワークスペースで:
New-Item -ItemType Directory -Path "hook-demo\src" -Force
cd "E:\software-workspace\DeepSeek harness demo\hook-demo" # 自分のディレクトリに置き換え
npm init -y
npm install @deepseek-ai/schemasteryschemastery は設定スキーマを定義するライブラリです(プラグインを設定可能にするには、これで設定の形とデフォルト値を宣言します)。package.json に "type": "module" を追加するのを忘れずに。
ステップ 2:フックプラグインを書く
hook-demo\src\audit.js を作成:
import Schema from '@deepseek-ai/schemastery'
export const name = 'audit-hook'
export const Config = Schema.object({
denyTools: Schema.array(Schema.string()).default([]),
})
export function apply(ctx, config) {
ctx.on('tools/pre-execute', (exec, next) => {
console.log(`[audit] ツールが呼び出されます: ${exec.name}`)
if (config.denyTools.includes(exec.name)) {
return { kind: 'deny', reason: `ポリシー:本セッションでは ${exec.name} の呼び出しを禁止します` }
}
return next()
})
}一行ずつ分解:
Config:プラグインの設定可能項目を宣言。denyToolsは文字列配列、デフォルトは空配列。apply(ctx, config)のconfigはユーザー設定とデフォルトをマージした結果です。ctx.on('tools/pre-execute', ...):ツール実行前イベントを購読。ツールが呼ばれるたびに必ずここを経由します。console.log:監査——このツールが呼び出されようとしていることを記録します。config.denyTools.includes(exec.name):このツールが拒否リストにある場合、{ kind: 'deny', reason }を返して遮断します。return next():それ以外なら許可して、パイプラインを続行させます。
ステップ 3:cordis.yml で設定を渡す
hook-demo\cordis.yml を作成(パスは自分のものに置き換え、スペースは %20 に変換):
- insert:
- id: audit
name: 'file:///E:/your-workspace/hook-demo/src/audit.js'
config:
denyTools: ['pwsh']ここで config が pwsh(PowerShell)を拒否リストに追加しています。注目:プラグインコードは一文字も変えていないのに、振る舞いが変わりました——これがまさに設定の意味であり、公式の設計原則である「ハードコードされた調整可能パラメータは持たない」の体現です:cordis.yml で変更できる値はコードに書き込むべきではありません。
ステップ 4:実行して効果を確認
headless で一度実行し、モデルに pwsh を呼ばせます:
cd 自分のワークスペースディレクトリ
dsh --profile headless --patch "./hook-demo/cordis.yml" "pwsh で Get-ChildItem を実行して現在のディレクトリを一覧表示してください"実際にターミナルに出力された内容:

二行の黄色い [audit] ログが、我々のフックが記録している様子です:モデルはまず skill を呼び、次に pwsh を呼びました——ツール呼び出しはすべて我々が仕掛けたフックを通過しています。pwsh は拒否リストにあるので deny され、モデルは拒否を認識して「本セッションでは pwsh の呼び出しが禁止されている」と自発的に報告し、代替案まで提示しました。
一つのプラグインで監査(見える)とインターセプト(制御できる)を両立。これがフックの力です。
dsh に任せる:一発のプロンプトで完了
このプラグインは dsh に書かせる方が早いです。Web UI の入力欄に直接送ります:
現在のワークスペースでフックプラグインを一つ書いてください:ツールが呼び出される前にログを一行出力し、設定で指定したツールを拒否できるようにします。公式仕様に従って実装し、headless を一度走らせて記録とインターセプトの両方が動作することを検証し、最後に結果を教えてください。dsh は自分で公式ドキュメントを読み、プラグインを書き、記録とインターセプトの両方が機能することを検証します。あなたがやるのは確認だけです。

これが実際にこのプロンプトを走らせた結果です:まず実装要点を自力で整理しました——プラグインの名前付きエクスポートは name / inject / apply のみ、inject: ['tools'] でツールレジストリを確実に用意する、ctx.on('tools/pre-execute', ...) でフックを仕掛ける(公式の permission-gate 例と一致)、拒否は { kind: 'deny', reason } を返す、許可は await next() で通す——「拒否リストは設定経由、コード変更不要」まで気が回っています。

この軌跡図はその作業の全工程です:write でプラグインファイル作成、pwsh で自身の検証スクリプト実行(8 項目すべてパス)、todo_write でタスクリスト更新……一つひとつがツール呼び出しです。
よくある落とし穴
| 問題 | 何が起きているか | 対処方法 |
|---|---|---|
return next() を忘れた | ウォーターフォールイベントで next が呼ばれず、パイプラインが停滞 | pre-execute / post-execute は必ず next() または判断を返す |
| deny 後もモデルがリトライし続ける | モデルはこのツールが永続的に利用不可と分かっていない | reason を明確に書くと、モデルがそれを見て別案に切り替える |
| 設定が効かない | cordis.yml の config の書き方が誤り | フィールド名と schema の型が一致しているか確認 |
Cannot find package '@deepseek-ai/schemastery' が出る | プラグインディレクトリで依存をインストールしていない | npm install @deepseek-ai/schemastery を実行 |
この章で学んだこと
- [ ] ツール呼び出しパイプラインの主な段階(pre-execute / execute / post-execute / result)を説明できる
- [ ]
ctx.on('tools/pre-execute', ...)を使ってフックプラグインを書ける - [ ]
{ kind: 'deny', reason }を返してツールを遮断し、next()で許可できる - [ ]
Config+ Schemastery を使ってプラグインを設定可能にできる(拒否リスト) - [ ] 「ハードコードされた調整可能パラメータを持たない」という設計原則を説明できる
