AI-DLC v2 の承認ゲートや監査ログが Kiro のフックでどう実装されているか調べた

AI-DLC v2 の承認ゲートや監査ログが Kiro のフックでどう実装されているか調べた

ゆるWeb勉強会での登壇準備でAI-DLC v2のソースコードを読んでいて、Kiroのフック機能の活用方法が気になったので、その実装パターンを紹介します。
2026.08.24

いわさです。

先日ゆるWeb勉強会@札幌で AI-DLC(AI-Driven Development Life Cycle)について登壇しました。

https://dev.classmethod.jp/articles/yuruweb-31-iwasa/

登壇の準備で awslabs/aidlc-workflows v2 のソースコードを読んでいたのですが、Kiro(AWS が提供している AI エージェント IDE)のフック機能でワークフロー制御を実装していました。
フックとスキルと TypeScript ツールを組み合わせてフレームワーク全体を表現するやり方は、自分がフックを高度に使いたい場面の参考になりそうだなと。

前回の登壇記事で「14 体のエージェントによる分業」と「自己学習ループ」を持ち帰りポイントとして挙げていたので、これらがフックで実現されているのか、それとも別の仕組みなのかも気になっていました。

今回は AI-DLC v2 のソースを読んで気づいたことを紹介します。

Kiro のフック機能

Kiro のフック機能は .kiro/hooks/*.json に配置する JSON ファイルで定義します。
エージェントのセッション中に特定のイベントが起きたとき、シェルコマンドやプロンプトを自動実行できます。

https://kiro.dev/docs/hooks/

フック定義のフォーマットは Kiro IDE の世代で変わっています。

  • IDE 0.x: .kiro.hook ファイル(JSON 1行の legacy format)
  • IDE 1.x: .kiro/hooks/*.json(v1 スキーマ。CLI 3.x と共通)

https://kiro.dev/docs/ide/whats-new-v1/hooks/

本記事では IDE 1.0 で導入された v1 スキーマを対象にしています。

なお、Claude Code にも同様のフック機構があります(.claude/settings.json 内に定義する形式)。
「イベント発生時にシェルコマンドを実行する」「exit code でブロックできる」という考え方は共通ですが、定義ファイルのフォーマット自体は別物です。
AI-DLC は両方に対応していて、アダプター層でそれぞれの差異を吸収しています。

v1 の定義はこういう構造です。

{
  "version": "v1",
  "hooks": [
    {
      "name": "format-on-save",
      "trigger": "PostFileSave",
      "matcher": "\\.ts$",
      "action": {
        "type": "command",
        "command": "prettier --write {{filePath}}"
      }
    }
  ]
}

使えるトリガーは以下のものがあります。

  • SessionStart — セッション開始時
  • Stop — エージェントの応答完了時
  • UserPromptSubmit — ユーザーがプロンプトを送信したとき
  • PreToolUse — ツール実行前(exit code 2 でブロック可能)
  • PostToolUse — ツール実行後
  • PostFileCreate / PostFileSave / PostFileDelete — ファイル操作時

コマンドアクションが exit code 2 を返すと、PreToolUseUserPromptSubmit のイベントをブロックできます。

https://kiro.dev/docs/hooks/types/

AI-DLC v2 のフックを見てみる

AI-DLC v2 は 9 つのフックを登録しています。
全フックの JSON 定義で "command" に指定されているのが bun .kiro/hooks/aidlc-kiro-adapter.ts <target名> という共通のコマンドで、この aidlc-kiro-adapter.ts が target 名に応じて各処理に振り分ける構成です。
AI-DLC v1 はワークフローの進行を LLM にプロンプトで伝えて判断させていたのですが、v2 は TypeScript で状態遷移を管理する設計に変わっています[1]
LLM に「承認待ちだから止まって」と伝えても無視されることがありますが、コードが exit 2 を返せばブロックされるので、制御をコード側に寄せたということみたいです。

トリガー フック名 やっていること
SessionStart session-start ワークフローの現在状態をエージェントに渡す
UserPromptSubmit record-human-turn 「人間が入力した」ことをマーカーに記録
PreToolUse enforce-approval-gate 承認待ちの間、ツール呼び出しをブロック
PostToolUse(write系) write-audit-log ファイル書き込みを監査ログに記録
PostToolUse(shell) sync-workflow-state 状態ファイルを同期
PostToolUse(shell) rebuild-stage-graph ステージグラフを再構築
PostToolUse(subagent) log-subagent サブエージェント完了をログ
Stop continue-workflow 残作業の確認
Stop session-end セッション終了をログ

以下、読んでいて特に参考になったところを紹介します。

承認ゲートの仕組み

AI-DLC の承認ゲート(ステージの区切りで人間の OK を要求する仕組み)は、フックで実現されています。

enforce-approval-gatePreToolUse に登録されていて、matcher 指定なしです。
matcher を指定しないとエージェントのあらゆるツール呼び出し(ファイル読み書き、シェル実行、etc.)で実行されるので、ゲートが開いている間はエージェントが何もできなくなります。
オーケストレーターがステージ完了時に状態ファイル(aidlc-state.md)にゲートを開くと、エージェントがツールを呼ぼうとするたびにこのフックが実行されてブロックします。
ユーザーが何か入力すると record-human-turnUserPromptSubmit)がマーカーを更新するので、次のツール呼び出しではブロックされなくなります。

aidlc-enforce-approval-gate.json
{
  "version": "v1",
  "hooks": [
    {
      "name": "aidlc-enforce-approval-gate",
      "trigger": "PreToolUse",
      "action": {
        "type": "command",
        "command": "bun .kiro/hooks/aidlc-kiro-adapter.ts enforce-approval-gate"
      }
    }
  ]
}

TypeScript 側の処理を簡略化するとこんな感じです。

aidlc-kiro-adapter.ts(簡略化)
if (target === "enforce-approval-gate") {
  try {
    const pd = process.cwd();
    const content = existsSync(sp) ? readFileSync(sp, "utf-8") : null;
    if (isAutonomousMode(content)) return 0;
    if (!hasOpenGate(content)) return 0;
    if (humanActedSinceGate(pd)) return 0;
    process.stderr.write("An approval gate is open...");
    return 2; // ブロック
  } catch {
    return 0; // エラーなら止めない
  }
}

catch で return 0 しているので、たとえば状態ファイルが壊れていたり読み込みに失敗したりしても、エージェントの動作を止めません。
フックのバグでエージェントが永久にブロックされてしまうと復旧が面倒なので、「判断できないときは通す」という方針にしているようです。
9 つのフック全部がこのパターンでした。

監査ログの仕組み

aidlc-write-audit-log.json
{
  "version": "v1",
  "hooks": [
    {
      "name": "aidlc-write-audit-log",
      "trigger": "PostToolUse",
      "matcher": "fs_write|str_replace|fs_append",
      "action": {
        "type": "command",
        "command": "bun .kiro/hooks/aidlc-kiro-adapter.ts audit-and-sensors"
      }
    }
  ]
}

matcher で fs_write|str_replace|fs_append に絞っているので、ファイル読み込みやシェル実行では動きません。
PostToolUse を使うフックが 4 つあるのですが、それぞれ matcher を変えて別ファイルにしています。matcher なしだと全ツールで動いてしまうのでこの分け方は大事そうです。

なお、Kiro IDE の PostToolUse ではツール入力(ファイルパス)がペイロードに入ってこないらしく、ツールの応答メッセージ(「Created the xxx file.」みたいな文言)から正規表現でパスを抽出しています。
Kiro IDE がフックに渡す情報にファイルパスが含まれていないので、代わりにツールが返すメッセージの文面を解析してパスを得ているということのようです。

aidlc-kiro-adapter.ts
function extractWrittenPath(toolResult: string): string {
  const s = toolResult.trim();
  let m = s.match(/^Created the (.+) file\.$/);
  if (m) return m[1].trim();
  m = s.match(/^Appended the text to the (.+) file\.$/);
  if (m) return m[1].trim();
  m = s.match(/^Replaced text in (.+?)(?:\s+\([^)]*\))?$/);
  if (m) return m[1].trim();
  return "";
}

14 体のエージェントとフックの関係

前回の登壇記事で「14 体のエージェントに分業させている」ことにも触れていました。
これもソースを見てみたのですが、エージェント定義は .kiro/agents/ に配置されたカスタムエージェント(.md ファイル)です。

.kiro/agents/
├── aidlc-architect-agent.md
├── aidlc-developer-agent.md
├── aidlc-quality-agent.md
├── aidlc-operations-agent.md
└── ... (全14体)

各エージェントの呼び出しはフックではなく、conductor(メインのスキル)が Kiro のサブエージェント機能を使って委任しています。
ステージ定義に mode: subagentlead_agent: aidlc-developer-agent のようなフィールドがあって、conductor がそれを見て適切なエージェントにタスクを渡す仕組みです。

フックが関わるのは呼び出し「後」だけです。
テーブルにあった log-subagent がそれで、サブエージェントの完了を監査ログに記録しています。呼び出し自体の制御には関与していません。

自己学習ループはフックではなかった

前回の登壇記事で「ワークフローを繰り返す中で学習する仕組み」を持ち帰りポイントとして挙げていたのですが、ソースを追ったところ、これはフックではありませんでした。

ステージプロトコル(スキルファイルに書かれた LLM への指示)で、承認ゲートの直前に「今回学んだことはあるか?」と問いかけて、エージェントが候補を出し、ユーザーが承認すると TypeScript ツール(aidlc-learnings.tspersist コマンド)で team.md / project.md に追記される仕組みでした。

重複防止のために cid マーカー(<!-- cid:stage-slug:candidate-id -->)が埋め込まれたり、組織ルール(org.md)と矛盾する学びは弾かれたりする仕組みも入っていました。

フックではなくスキル(LLM への指示)で実装しているのは、「何を学んだか」の判断には文脈が必要だからだと思います。
フックは「イベントが起きたら問答無用で実行する」ものなので、学習のような判断を伴う処理には向いていないです。

コンテキスト注入とステアリングの使い分け

session-start フックは SessionStart トリガーで動いて、ワークフローの今の状態をエージェントに渡します。

Kiro にはステアリングファイル(.kiro/steering/)というコンテキスト注入の仕組みもあります。
ステアリングは常に同じ内容を渡すもの(静的なルールや制約)ですが、SessionStart フックはその時点の状態を計算して渡せます。
AI-DLC では「今どのステージにいるか」が毎回変わるので、フックで動的に生成して渡しているみたいです。

aidlc-kiro-adapter.ts
if (target === "session-start") {
  try {
    const parsed = JSON.parse(result.stdout);
    if (parsed.additionalContext) {
      process.stdout.write(parsed.additionalContext);
    }
  } catch {
    if (result.stdout) process.stdout.write(result.stdout);
  }
  return 0;
}

さいごに

本日は AI-DLC v2 のソースを読んで、フック・スキル・TypeScript ツールがどう組み合わさっているかを紹介しました。

フックが担っているのは「承認ゲート」と「監査ログ」で、「自己学習ループ」はスキル + TypeScript ツールの担当でした。
自己学習ループの仕組みは別の機会にもう少し掘り下げてみたいと思います。

自分のプロジェクトで試すなら、まずは PostToolUse でファイル書き込み後にリンターを走らせるところからかなと思っています。
stdout に結果を返せばエージェントのコンテキストに入るので、「規約違反があったから直して」みたいなフィードバックループが組めます。
監査ログ的な使い方も、エージェントに自由にコードを書かせつつ変更履歴だけ追跡しておきたいプロジェクトで使えそうです。
もっと踏み込むなら PreToolUse + exit 2 で、プロンプトでは守りきれないルールを仕組みとしてブロックする使い方もできそうです。

脚注
  1. v1 vs v2 の比較 — 登壇記事参照 ↩︎

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事