2026年9月24日

【2026年9月版】Antigravity活用術~Hook完全攻略!ライフサイクルイベントによる自動ガードレールと品質統制~


Content

こんにちは!みっちーです!


以前の「Skill実践入門」の記事で、Rules・Skills・Hooksを比較する表の中に「Hooks」をチラッと登場させていたのを覚えていますか?今回はその正体を、余すところなく掘り下げてご紹介します!



AIエージェントにコーディングを任せていて、「うわ、また規約違反のコードが出てきた……」とゲンナリした経験はないでしょうか?


インデントのスタイルが混在している、命名規則が守られていない、プロジェクト固有のLintルールを無視した書き方をしてくる…。エージェントは基本的に指示(Rules)を読んで書いてくれますが、100%守ってくれる保証は、残念ながらありません。


さらに厄介なのが、そのあとの「修正ループ」です。ユーザー側で規約違反に気づいて「ここはこう直して」と指摘すると、エージェントは律儀に該当箇所を読み直し、修正案を考え、コードを書き直します。この一往復のためだけに、数千〜数万トークンが消費されます。私自身、Antigravityのエージェントと大規模なリファクタリングを進めていたときに、似たようなスタイル違反を何度も指摘しては直させるというやり取りを繰り返し、「これ、最初から自動で直ってくれていたら何分の一のトークンで済んだんだろう……」と考えるだけで気が滅入ったことがあります。


「じゃあGEMINI.mdAGENTS.mdにコーディング規約をもっと細かく書けばいいのでは?」――そう思われるかもしれません。しかし、長時間のセッションでコンテキストが肥大化すると、プロンプトで与えた指示は自己注意機構(Attention)の希薄化によって見落とされるリスクが常に存在します。規約を守らせるための指示を増やせば増やすほど、逆にコンテキストを圧迫し、他の重要な指示が埋もれてしまうという本末転倒な事態にもなりかねません。


実は、この「規約違反 → 指摘 → 修正 → トークン浪費」というループを、根本から断ち切る仕組みがAntigravityには用意されています。エージェントがファイルを保存した直後に、RuffやPrettierといったLinter/Formatterを自動で割り込ませ、規約違反をその場で機械的に矯正してしまう――そんなことが公式ドキュメントにも載っている定番パターンとして実現できるんです!


そして、AI駆動開発(AI-Driven Development: AIDD)としてエージェントの自律性が高まるほど直面するのが、「安全性と品質のガバナンス」という分厚い壁です。


車や鉄道に例えるなら、プロンプト指示(Rules)は道路脇に立てられた「速度標識」にすぎません。エージェントは普段は標識を守って走ってくれますが、前述の通り、複雑な推論タスクや長大なコンテキストの中では見落としのリスクがどうしても残ります。その時に本当に必要なのは、標識を増やすことではなく、規約違反を自動で検知・矯正する「自動検査ライン」や、危険な操作を物理的に遮断する「非常ブレーキ」です。


この「自動検査ライン」と「非常ブレーキ」の役割を一手に担うのが、今回徹底解説するAntigravityの「Agent Lifecycle Hooks(ライフサイクルフック)」です!


AIDDを現場で安全に乗りこなすためには、以下の「AIDD三位一体(Triad)」の設計が欠かせません。


  • Rules(方針・頭脳):コーディング規約や方針を伝える「速度標識」(GEMINI.md
  • Skills(手順・手足):複数ステップの作業ノウハウを段階的に展開する「運行マニュアル」(skills/
  • Hooks(安全弁・自動検査ライン):Linter/Formatterの自動適用や危険操作の物理遮断によって品質と安全性を強制する「決定論的ガードレール」(hooks.json

本記事では、Antigravity Hookの基礎アーキテクチャから全5大イベントの詳細、protojson準拠の入出力仕様、Windows UTF-8環境も考慮した実践的な完全Pythonスクリプト、そして現場で役立つ運用ベストプラクティスまで、余すところなく徹底解説します!

なぜAIコーディングにHook(ライフサイクルフック)が不可欠なのか?

自律型AIエージェントを活用する開発現場において、なぜプロンプト指示(Rules)だけでは不十分で、Hookによる統制が不可欠なのでしょうか。その理由は、「Soft Guardrail(指示的制御)」と「Hard Guardrail(強制的制御)」の根本的な違いにあります。

比較項目 プロンプト統制(Rules / Instructions) ライフサイクルフック(Hooks)
防御レイヤー Soft Guardrail(確率的・推奨的) Hard Guardrail(決定論的・強制的)
実行主体 LLM(プロンプト解釈) OS / 外部独立スクリプト(外部プロセス)
遵守の確実性 高(ただしコンテキスト圧迫やハルシネーションで逸脱の余地あり) 決定論的に評価される(ただしルール網羅性はスクリプト次第)
危険操作への対応 不可(モデルが生成したコマンドのまま実行) 可能(実行直前にブロックし、修正を促して再試行させる)
外部ツール自動連携 エージェントの自発的なツール呼び出しに依存 ツール完了直後に外部スクリプトを自動連動(Lint/Format等)
ループ終了制御 エージェントの自己判断で終了 完了条件(DoD)未達時にセッション終了を強制阻止

1. プロンプト統制(Soft Guardrail)の限界とその構造的リスク

プロンプトにどれほど「rm -rf は絶対に使わないでください」「Windows環境では Remove-Item -Recurse -Force を避けてください」と記述しても、マルチターンの推論が数十回に及ぶ大規模な開発タスクでは、コンテキスト長の圧迫や自己注意機構(Attention)の希薄化によって、禁止事項が見落とされるリスクがゼロにはなりません。

LLMはあくまで次に続くトークンの確率を予測して出力しているため、エージェントが「この一時ディレクトリを丸ごと消せばタスクが早く終わる」と判断した瞬間、悪気なく禁止コマンドを出力してしまうことがあります。これがプロンプト統制(Soft Guardrail)の構造的限界です。

2. 物理的な安全弁(Hard Guardrail)としての決定論的制御

一方のHookは、エージェントの思考プロセスとは完全に切り離されたOSプロセスレベルの割り込み機構です。

エージェントがシェル実行ツール(run_command)を呼び出した瞬間、Antigravityのランタイムが実行直前にインターセプトします。スクリプトがコマンド文字列を検査し、危険パターンを検知して deny 判定を返した場合、OSにコマンドを渡す前に実行を停止します。モデルの機嫌やトークン数に一切左右されない、決定論的(Deterministic)な評価に基づく安全弁が手に入ります(ただし後述の通り、検知パターン自体の網羅性には限界がある点には留意してください)。

3. シフトレフト品質管理(Shift-Left Quality)の自動化

Hookの恩恵は危険コマンドの防御にとどまりません。エージェントがファイルを生成・修正した直後(PostToolUse)に自動で RuffPrettier を走らせてスタイルを整えたり、タスクを勝手に完了しようとした際(Stop)にユニットテストの実行ログを検査して未検証なら終了を突き返したりと、開発品質の自動統制パイプラインとしても大活躍します。

人間がコードレビューで「インデントが崩れています」「テストを走らせてからPRを出してください」と指摘していた初歩的な手戻りを、エージェントの自己完結ループの中で自動解消できます。

Antigravity Hookのアーキテクチャと全5大イベント詳解

AntigravityのHookシステムは、エージェントが思考し、ツールを実行し、ユーザーに応答するまでの実行ループ(Execution Loop)の要所要所に用意されたライフサイクルイベントで構成されています。

イベント名 発火タイミング Matcherによるフィルタ 主な役割・ユースケース
PreToolUse ツール実行の直前 ツール名単位で正規表現フィルタ可 危険コマンドの遮断(deny)、承認要求
PostToolUse ツール実行の完了直後 ツール名単位で正規表現フィルタ可 自動コード整形(Ruff / Prettier)、差分ロギング、静的監査
PreInvocation LLMモデル推論の直前 対象外(フラット配列で定義) 最新のAPI仕様やGit差分などの動的コンテキスト注入(ephemeralMessage)
PostInvocation LLMモデル推論の完了直後 対象外(フラット配列で定義) LLM出力の監査、不適切なレスポンスの検知
Stop エージェントのセッション終了時 対象外(フラット配列で定義) 完了の定義(DoD)検証、テスト未実行の終了阻止、バックグラウンド監視

1. ツール単位のイベント(PreToolUse / PostToolUse)

PreToolUsePostToolUse は、エージェントが利用するツール(run_commandwrite_to_filereplace_file_content など)の実行前後にフックを仕掛けるイベントです。

特定ツールだけを対象にするため、設定ファイル内で matcher プロパティ(正規表現)を指定します。例えば “matcher”: “run_command” と記述すればコマンド実行時のみ、“matcher”: “write_to_file|replace_file_content” と記述すればファイル編集時のみ外部スクリプトを呼び出せます。

2. 推論・セッション単位のイベント(PreInvocation / PostInvocation / Stop)

一方、PreInvocationPostInvocationStop の3つは、特定のツール呼び出しではなく、エージェントの思考サイクルそのものに紐づくイベントです。そのため、Matcherによるグルーピングは行わず、フック定義オブジェクトの配列をフラットに記述します。

特に Stop イベントは、エージェントが「タスク完了しました!」と自己申告した瞬間に発火するため、「本当にテストをパスしたか?」「やり残した非同期タスクはないか?」を検証する最後の砦として極めて強力です。

設定ファイル(hooks.json)の構文と入出力プロトコル

Hookの具体的な設定方法と、Antigravityプロセスと外部スクリプトがやり取りする通信プロトコルについて詳しく見ていきましょう。

1. 設定ファイルの配置場所とスコープ設計

Hookは hooks.json というJSONファイルで定義します。配置場所は以下の2種類が存在し、階層的に読み込まれます。

  1. グローバル設定(マシン全体)~/.gemini/config/hooks.json開発者個人の端末全体で常時有効化するセキュリティガードレール。どのプロジェクトを開いていても、危険な破壊コマンドを端末レベルで遮断します。外部への機密情報送信(curlでの外部送信等)の検知は、BLOCK_PATTERNSに該当パターンを追加することで拡張できます。
  2. ワークスペース設定(プロジェクト固有)<Workspace_Root>/.agents/hooks.jsonプロジェクトのリポジトリ内にコミットしてチーム全員で共有する設定。コードフォーマッターやプロジェクト固有のビルド検証・DoDルールを定義します。

グローバル設定とワークスペース設定の両方にHookが存在する場合、それらはマージされて順次実行されます。セキュリティに関する判定では「フェイルセーフ原則」が働き、いずれか1つのHookでも deny(拒否)を返せば、ツール実行は即座に遮断されます。

2. hooks.jsonの基本構造

以下は、現場で標準的に用いられる hooks.json の完全な定義例です。

{
"security-guardrail": {
"enabled": true,
"PreToolUse": [
{
"matcher": "run_command",
"hooks": [
{
"type": "command",
"command": "python ./scripts/hooks/pre_command_guard.py",
"timeout": 15
}
]
}
]
},
"auto-code-formatter": {
"enabled": true,
"PostToolUse": [
{
"matcher": "write_to_file|replace_file_content",
"hooks": [
{
"type": "command",
"command": "python ./scripts/hooks/post_edit_formatter.py",
"timeout": 30
}
]
}
]
},
"definition-of-done-checker": {
"enabled": true,
"Stop": [
{
"type": "command",
"command": "python ./scripts/hooks/stop_task_validator.py",
"timeout": 20
}
]
}
}

ここで注意したいのが相対パスの解決基準です。hooks.json内のスクリプトパスは、そのhooks.jsonファイル自身が置かれているディレクトリを基準に解決されます。そのため、上記のワークスペース設定例(<Workspace_Root>/.agents/hooks.json)の場合、実際のスクリプトは <Workspace_Root>/.agents/scripts/hooks/… に配置する必要があります。一方、プロジェクト固有の相対フォルダが存在しないグローバル設定(~/.gemini/config/hooks.json)でこの相対パス表記をそのまま使うと解決に失敗するため、グローバル設定では必ず絶対パスでスクリプトを指定してください。

3. protojson準拠の入出力スキーマ(camelCaseの厳守)

Hookスクリプトは、Antigravityプロセスから標準入力(stdin)経由でJSONを受け取り、判定結果を標準出力(stdout)にJSONで書き出すことで通信します。

【重要】protojson契約によるcamelCaseルールとtoolCall構造Antigravityの内部通信プロトコルは protojson に準拠しているため、JSONのキー名はすべて camelCase です。公式ドキュメントで示されているツール実行イベントの構造は、toolCall.name および toolCall.args のネストされた形です。将来的な内部実装の変更に備え、スクリプト側では念のため toolName / toolInput という代替キーも防御的に拾えるようにしておくと安全です(ただし現行のAntigravity公式仕様としては toolCall.name / toolCall.args が正式な形式です)。

ツール実行イベントで渡される標準入力JSONの代表的な構造は以下のとおりです。

{
"conversationId": "a5a19bb2-d599-4a76-9df2-b7afa4f37fc8",
"workspacePaths": ["c:/Users/dev-user/my-project"],
"transcriptPath": "c:/Users/dev-user/.gemini/antigravity/transcript.jsonl",
"artifactDirectoryPath": "c:/Users/dev-user/.gemini/antigravity/artifacts",
"modelName": "auto",
"stepIdx": 8,
"toolCall": {
"name": "run_command",
"args": {
"CommandLine": "rm -rf ./build",
"Cwd": "c:/Users/dev-user/my-project"
}
}
}

4. 判定ステータス(Decision)とWindows UTF-8の注意点

スクリプトが標準出力で返す判定結果は、ライフサイクルイベントの種類に応じて以下の形式で行います。

  • ツール実行を許可する場合(PreToolUse){“decision”: “allow”}
  • ツール実行を拒否・遮断する場合(PreToolUse){“decision”: “deny”, “reason”: “具体的な拒否理由と代替案”}
  • セッション終了を阻止し作業を継続させる場合(Stop){“decision”: “continue”, “reason”: “未達の完了条件と指示”}(※Stopフックで作業を継続させるには必ず continue を返す必要があります。それ以外の値は終了許可とみなされます)
  • セッション正常終了を許可する場合(Stop){“decision”: “allow”} または {}

※ 本記事では実務で頻出する allow / deny / continue の3つに絞って解説していますが、公式仕様ではPreToolUseの decision にはこのほかにも、キャッシュされた許可設定に関わらず必ずユーザーに確認を求める ask / force_ask、過去に承認済みのリソースでない限り拒否する deny_unless_prior_grant といった値も用意されています。より柔軟な承認フローを組みたい場合は、これらの値も検討してみてください。なお PostToolUse の出力には decision フィールド自体が存在せず、常に空の {} を返す仕様です。

また、Windows環境でPythonスクリプトを実行する場合、標準入出力がデフォルトで CP932(Shift-JIS) に設定されているため、日本語の拒否理由や日本語ファイルパスを扱った瞬間に UnicodeDecodeErrorUnicodeEncodeError が発生してスクリプトがクラッシュすることがあります。

これを防ぐため、すべてのPythonフックスクリプトの先頭で必ず以下のUTF-8再構成を実行するのが鉄則です!

import sys

# Windows環境における標準入出力のUTF-8再構成
if sys.platform == "win32":
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")

コピペで即稼働!実践フックスクリプト完全実装(Python版)

ここからは、現場のリポジトリにそのままコピペして即座に運用開始できる、3つの実践的なPythonフックスクリプトの完全実装コードを紹介します。

1. 【PreToolUse】破壊的コマンド防御「pre_command_guard.py」

Linux/macOS(Bash)とWindows(PowerShell / CMD)の双方で危険視される代表的な破壊的コマンド、不可逆なGit操作、外部シェルの直接パイプ実行を正規表現で検知します(正規表現ベースの検知には限界があり、bash -c 経由の間接実行や変数展開、Base64エンコードされたペイロードなどをすり抜ける可能性があるため、完全な防御ではない点に留意してください)。検知した場合は即座に deny(遮断)します。

#!/usr/bin/env python3
"""
scripts/hooks/pre_command_guard.py
Antigravity PreToolUse ガードレールスクリプト
破壊的コマンドの検知と実行遮断(Windows/Linux/macOS対応)
"""
import sys
import json
import re

# Windows環境におけるUTF-8文字化け・エンコーディングエラー防止
if sys.platform == "win32":
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")

# 即時遮断(deny)する危険コマンドパターン
BLOCK_PATTERNS = [
# Linux / macOS 破壊的削除(短縮フラグ・verbose結合フラグ・ロングオプション対応)
(r"\brm\s+-(?:[a-zA-Z]*r[a-zA-Z]*f|[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\b", "再帰的・強制ファイル削除(rm -rf 等)"),
(r"\brm\s+.*--(?:recursive|force)\b", "再帰的・強制ファイル削除のロングオプション(rm --recursive / --force 等)"),
(r"\brm\s+-r\s+-f\b|\brm\s+-f\s+-r\b", "再帰・強制フラグを分割指定したrm削除"),
# Windows CMD 破壊的削除(/s /q を独立したフラグトークンとして厳密検知し、rmdir build/src/queue 等の誤検知を回避)
(r"\b(?:del|rmdir|rd)\b.*(?:(? # Windows PowerShell 破壊的削除(エイリアス ri/rm/rd/del/erase と省略形フラグ -Rec* / -For* を広めにカバー。網羅的ではないため過信は禁物)
# ※ 省略形マッチのため --format や -Foreground、再帰を伴わない単一ファイル指定の -Force のみのケースなども誤検知(false positive)し得るが、これは許容範囲のトレードオフとする
(r"\b(?:Remove-Item|ri|rm|rd|rmdir|del|erase)\b.*-(?:rec\w*|for\w*)\b", "PowerShellの再帰的・強制削除(Remove-Item / ri / rm / rd / del / erase の -Recurse / -Force 等)"),
# Git 不可逆操作
(r"\bgit\s+reset\s+--hard\b", "作業ツリーとインデックスを破棄する git reset --hard"),
(r"\bgit\s+clean\s+-(?:[a-zA-Z]*f[a-zA-Z]*)", "未追跡ファイルを強制削除する git clean -f"),
(r"\bgit\s+push\s+.*(?:--force(?!-with-lease\b|-if-includes\b)\b|-f\b)", "リモート履歴を上書きする git push --force(--force-with-lease / --force-if-includesは除外)"),
# 危険なシェル直パイプ実行・動的実行
(r"\bcurl\b.*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b", "外部スクリプトの直接シェルパイプ実行(curl | bash 等)"),
(r"\bwget\b.*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b", "外部スクリプトの直接シェルパイプ実行(wget | sh 等)"),
(r"\|\s*iex\b", "PowerShellのパイプライン動的実行(| iex)"),
(r"\bInvoke-Expression\b", "PowerShellの動的コード実行(Invoke-Expression)"),
(r"\biex\b\s*\(", "PowerShellの動的コード実行短縮形(iex)"),
# インフラ破棄系
(r"\bterraform\s+destroy\b", "インフラリソースの全破棄(terraform destroy)"),
(r"\bgcloud\s+projects\s+delete\b", "Google Cloudプロジェクトの削除(gcloud projects delete)"),
]

def evaluate_command(command_line: str) -> dict:
"""コマンド文字列を正規表現で走査し、安全性を判定する"""
for pattern, description in BLOCK_PATTERNS:
if re.search(pattern, command_line, re.IGNORECASE):
return {
"decision": "deny",
"reason": (
f"【セキュリティポリシー違反】危険な操作 '{description}' を検出したため実行を即時遮断しました。\n"
f"検出対象: '{command_line}'\n"
f"代替手順: 不要ファイルは一括削除せず対象ファイルを個別に指定するか、"
f"バックアップを作成した上で安全な操作手順を選択してください。"
)
}
return {"decision": "allow"}

def main():
try:
# stdin から JSON ペイロードを読み込み
input_data = json.load(sys.stdin)
except Exception as e:
# パース失敗時も安全側に倒して遮断
result = {
"decision": "deny",
"reason": f"入力JSONペイロードの解析に失敗しました: {str(e)}"
}
print(json.dumps(result, ensure_ascii=False))
sys.exit(0)

# protojson スキーマ準拠(toolCall.name / args および toolName / toolInput を防御的に取得)
tool_call = input_data.get("toolCall") or {}
tool_name = tool_call.get("name") or input_data.get("toolName", "")
tool_input = tool_call.get("args") or input_data.get("toolInput", {})

if tool_name == "run_command":
cmd = tool_input.get("CommandLine") or tool_input.get("commandLine") or ""
result = evaluate_command(cmd)
else:
result = {"decision": "allow"}

# stdout に JSON を出力(終了コードは常に0)
print(json.dumps(result, ensure_ascii=False))
sys.exit(0)

if __name__ == "__main__":
main()

2. 【PostToolUse】ファイル保存時の自動整形「post_edit_formatter.py」

エージェントが write_to_filereplace_file_content でファイルを書き換えた直後に自動発火し、拡張子に応じて Ruff(Python)や Prettier(JavaScript/TypeScript/JSON/HTML/CSS/Markdown)を適用します。なお、Windows環境ではnpxの実行ファイル名が npx.cmd であるため、これを明示的に解決しないとPrettierの呼び出しがサイレントに空振りすることがあります。下記のスクリプトではOS判定によりnpx.cmdを明示的に指定し、この無音の失敗を防いでいます。同様の問題はRuff側にもあり、pip install直後はインストール先のScriptsディレクトリがまだPATHに通っていないことがあるため、こちらもsys.executable -m ruffという形でPython本体経由で呼び出すことでPATHに依存しない実行を実現しています(PATH頼みのコマンド名がサイレントに失敗するという同種の問題を、実行元インタプリタ経由の呼び出しという同じ解法で防ぐアプローチです)。

#!/usr/bin/env python3
"""
scripts/hooks/post_edit_formatter.py
Antigravity PostToolUse 自動コードフォーマットフック
ファイル編集直後にプロジェクト標準のフォーマッターを自動適用
"""
import sys
import json
import subprocess
import os

# Windows環境におけるUTF-8文字化け・エンコーディングエラー防止
if sys.platform == "win32":
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")

def main():
try:
input_data = json.load(sys.stdin)
except Exception:
print("{}")
sys.exit(0)

# 直前のツール実行でエラーが発生していた場合はフォーマットをスキップ
if input_data.get("error"):
print("{}")
sys.exit(0)

# protojson スキーマ準拠(toolCall.name / args および toolName / toolInput を防御的に取得)
tool_call = input_data.get("toolCall") or {}
tool_name = tool_call.get("name") or input_data.get("toolName", "")
tool_input = tool_call.get("args") or input_data.get("toolInput", {})

if tool_name in ["write_to_file", "replace_file_content"]:
target_file = (
tool_input.get("TargetFile") or
tool_input.get("targetFile") or
tool_input.get("AbsolutePath") or
tool_input.get("absolutePath")
)

if target_file and os.path.exists(target_file):
_, ext = os.path.splitext(target_file)
ext = ext.lower()

try:
# Python ファイルの自動フォーマット (Ruff)
# check --fix はコードを書き換えるため、整形(format)より先に走らせる
if ext == ".py":
subprocess.run([sys.executable, "-m", "ruff", "check", "--fix", target_file], capture_output=True, timeout=15)
subprocess.run([sys.executable, "-m", "ruff", "format", target_file], capture_output=True, timeout=15)

# Web系フロントエンドファイルの自動フォーマット (Prettier)
elif ext in [".js", ".jsx", ".ts", ".tsx", ".json", ".html", ".css", ".md"]:
# Windowsではnpxの実体がnpx.cmdのため明示解決し、--no-installでダウンロード待ちによる無音タイムアウトを防ぐ
npx_cmd = "npx.cmd" if sys.platform == "win32" else "npx"
subprocess.run([npx_cmd, "--no-install", "prettier", "--write", target_file], capture_output=True, timeout=20)
except Exception:
pass

# PostToolUse は空の JSON オブジェクトを出力して終了
print("{}")
sys.exit(0)

if __name__ == "__main__":
main()

これはまさに、冒頭で触れた「規約違反 → 指摘 → 修正 → トークン浪費」というループを、その場で断ち切る仕組みです。エージェントが規約に違反したコードを書いても、人間が指摘するより先にRuffやPrettierが機械的に矯正してしまうため、少なくともスタイル・整形面の指摘→修正の往復は発生しなくなります。

ただし正直に言うと、これで冒頭の悩みがすべて解決するわけではありません。命名規則(変数名やクラス名の付け方)のような違反は、そもそもRuffやPrettierの–fixで自動修正できるルールがほとんど存在しません。このスクリプトが解決するのはあくまでインデントや引用符といったスタイル・整形面の違反であり、命名規則違反そのものを自動で直してくれるわけではない点には注意してください。

また、このスクリプトは現状 capture_output=True でRuff/Prettierの実行結果を握りつぶしており、フォーマッターが直しきれなかった違反があってもエージェント側には一切通知されません。この取りこぼしを可視化するには、Stopフックの reason フィールド経由で未解決のLint出力をエージェントに突き返す、といった仕組みを別途組み合わせる必要があります。加えて、エージェントの保存直後にファイル内容がフォーマッターによって書き換えられるため、エージェント側が認識している「保存直後の内容」とファイルの実体がずれてしまい、後続の編集ツール呼び出しが期待通りの内容と一致せず失敗・リトライする——という新たなトークン浪費の芽になり得る点も覚えておくとよいでしょう。

3. 【Stop】テスト未実行の終了を阻止「stop_task_validator.py」

エージェントが作業を終えようとした際、バックグラウンドタスクが未完了(fullyIdle == false)であったり、ログ(transcript.jsonl)を走査してテストコマンド(pytestnpm test 等)が一度も実行されていなかったりする場合に、{“decision”: “continue”, “reason”: “…”} を返してセッション終了を阻止し、作業継続を強制します(継続条件は前述の通りです)。なお、ここで示す transcript.jsonl のスキーマ(toolCalls / tool_calls 等)はあくまでベストエフォートな例であり、実際のランタイム出力で必ず検証してから本番運用に組み込んでください。

下記で追加するリトライ上限は、そのスキーマ想定が外れて判定が空振りし続けた場合の安全弁でもあります。このリトライカウンタは常にリクエスト内の conversationId をキーにして管理し、上限(MAX_RETRIES)に達した場合は強制的に終了を許可(allow)すると同時にカウンタをリセットするため、同じ会話内であっても次のStopサイクルからは再び通常のDoDチェックが有効になります。つまり無効化されるのはあくまで「その1回のStop試行」だけであり、以降のサイクルや別会話まで恒久的にDoDチェックが無効化されるわけではありません。また、カウンタファイル自体の書き込みに失敗した場合は「再試行回数を正しく追跡できない」状態とみなし、無限ループを避けるため即座に終了を許可(フェイルセーフ)します。

#!/usr/bin/env python3
"""
scripts/hooks/stop_task_validator.py
Antigravity Stop ライフサイクルフック(DoD: 完了の定義検証)
テスト未実行やバックグラウンドタスク未完了による誤った作業終了を阻止
"""
import sys
import json
import os
import tempfile

# Windows環境におけるUTF-8文字化け・エンコーディングエラー防止
if sys.platform == "win32":
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")

def check_verification_performed(transcript_path: str) -> bool:
"""transcript.jsonl を解析し、テストや検証の実行記録があるか確認"""
if not os.path.exists(transcript_path):
return True

test_keywords = [
"pytest", "python -m unittest", "npm test", "yarn test",
"go test", "cargo test", "mvn test", "gradle test",
"ruff check", "eslint"
]

has_test_execution = False
try:
with open(transcript_path, "r", encoding="utf-8") as f:
for line in f:
if not line.strip():
continue
try:
step = json.loads(line)
tool_calls = step.get("toolCalls") or step.get("tool_calls") or []
for tc in tool_calls:
args = (
tc.get("args") or
tc.get("toolInput") or
(tc.get("toolCall") or {}).get("args") or
{}
)
cmd = args.get("CommandLine") or args.get("commandLine") or ""
if any(kw in cmd for kw in test_keywords):
has_test_execution = True
return True
except Exception:
continue
except Exception:
return True

return has_test_execution

# transcriptのスキーマ想定が外れる等で「continue」判定が空振りし続けた場合に、
# セッションが永久にループしないようにするための再試行上限
MAX_RETRIES = 3

def _counter_path(conversation_id: str, artifact_dir: str, transcript_path: str) -> str:
"""
再試行カウンタファイルのパスを決定する。
transcriptPathは空になり得るため使わず、必ず存在するconversationIdをキーにする。
"""
base_dir = artifact_dir or (os.path.dirname(transcript_path) if transcript_path else tempfile.gettempdir())
return os.path.join(base_dir, f".stop_retry_{conversation_id}.count")

def get_retry_count(counter_path: str) -> int:
"""継続(continue)判定の再試行回数をカウンタファイルから読み込む"""
try:
with open(counter_path, "r", encoding="utf-8") as f:
return int(f.read().strip() or "0")
except Exception:
return 0

def increment_retry_count(counter_path: str):
"""
再試行回数をインクリメントしてカウンタファイルに保存する。
戻り値は (書き込みに成功したか, インクリメント後のカウント)。
"""
count = get_retry_count(counter_path) + 1
try:
with open(counter_path, "w", encoding="utf-8") as f:
f.write(str(count))
return True, count
except Exception:
return False, count

def reset_retry_count(counter_path: str):
"""正常終了(allow)確定時にカウンタをリセットし、次サイクルをクリーンな状態から開始させる"""
try:
if os.path.exists(counter_path):
os.remove(counter_path)
except Exception:
pass

def check_retry_cap_or_allow(counter_path: str):
"""
continue判定の共有リトライ上限チェック。
カウンタファイルの書き込み自体に失敗した場合、「再試行回数を正しく追跡できない」
状態と判断し、検知不能な無限ループに陥るリスクを避けるため即座にallow側へ
フェイルセーフする(安全性チェックの厳格さより、ハングしないことを優先する)。
上限を超えた場合は、このStop試行だけを強制的にallowしつつカウンタをリセットし、
同じ会話内でも次のStopサイクルからは通常のDoDチェックへ復帰できるようにする。
上限内であればNoneを返し、呼び出し側でcontinue応答を継続させる。
"""
write_ok, count = increment_retry_count(counter_path)
if not write_ok:
return {"decision": "allow"}
if count > MAX_RETRIES:
# このStop試行のみ強制許可し、次サイクルは再びチェックが効くようリセットする
reset_retry_count(counter_path)
return {"decision": "allow"}
return None

def main():
try:
input_data = json.load(sys.stdin)
except Exception:
print(json.dumps({"decision": "allow"}))
sys.exit(0)

transcript_path = input_data.get("transcriptPath", "")
conversation_id = input_data.get("conversationId", "unknown")
artifact_dir = input_data.get("artifactDirectoryPath", "")
counter_path = _counter_path(conversation_id, artifact_dir, transcript_path)

# 1. バックグラウンドタスクの稼働状況を確認
fully_idle = input_data.get("fullyIdle", True)
if not fully_idle:
capped_result = check_retry_cap_or_allow(counter_path)
if capped_result is not None:
print(json.dumps(capped_result))
sys.exit(0)
response = {
"decision": "continue",
"reason": (
"【タスク未完了】バックグラウンドで実行中の非同期プロセスが存在します。\n"
"manage_task ツール等でプロセスの終了状態を確認し、完了してからタスクを終了してください。"
)
}
print(json.dumps(response, ensure_ascii=False))
sys.exit(0)

# 2. テスト・検証の実施履歴を確認(DoDチェック)
if transcript_path and not check_verification_performed(transcript_path):
capped_result = check_retry_cap_or_allow(counter_path)
if capped_result is not None:
print(json.dumps(capped_result))
sys.exit(0)
response = {
"decision": "continue",
"reason": (
"【品質統制ルール違反】コード変更に対するテストまたは検証コマンドが一度も実行されていません。\n"
"関連するユニットテスト(pytest, npm test 等)や静的検証スクリプトを実行し、"
"正常にパスすることを確認してからタスクを完了してください。"
)
}
print(json.dumps(response, ensure_ascii=False))
sys.exit(0)

# 全条件をクリアした場合は正常終了を許可し、次サイクルに備えてカウンタをリセットする
reset_retry_count(counter_path)
print(json.dumps({"decision": "allow"}))
sys.exit(0)

if __name__ == "__main__":
main()

エンタープライズ開発における運用ベストプラクティスと落とし穴

チーム開発やエンタープライズ環境でAntigravity Hookを本格運用するにあたり、押さえておくべき代表的な運用パターンとトラブルシューティングのポイントをまとめました。

1. エンタープライズ向け4大運用パターン

  1. ゼロトラスト・ターミナルガード(Zero-Trust Terminal Guard)開発者が利用する端末のグローバル設定(~/.gemini/config/hooks.json)に PreToolUse ガードレールを配置。本番インフラへの誤操作などの破壊的コマンドを端末レベルで遮断します。意図しない機密流出(外部へのcurl送信等)も、検知パターンをBLOCK_PATTERNSに追加すれば同様に防御できます。
  2. シフトレフト・フォーマッター(Shift-Left Formatter)リポジトリルートの .agents/hooks.jsonPostToolUse を定義。エージェントがコードを生成するたびにプロジェクト標準のコードスタイルへ即時自動変換し、レビュー負荷を削減します。
  3. JITコンテキスト注入(Just-In-Time Context Injection)PreInvocation を活用し、最新のOpenAPI仕様書やGitブランチの差分ステータス、DBスキーマ情報をモデル推論直前の ephemeralMessage として動的注入します。常に最新状態を反映した推論が可能になります。
  4. 完了の定義(DoD)ゲートウェイStop フックを用いて、少なくとも一度はテスト関連コマンドが実行された形跡があるかを確認し、なければ終了を差し戻す、という最低限のDoDチェックを構築できます(本記事のサンプルはあくまで「テストコマンドが実行されたか」の痕跡確認であり、ビルド/型チェック/カバレッジ基準のような本格的なCI相当の判定を行うには、判定ロジック自体の拡張が必要です)。

2. 現場でハマる罠とアンチパターン

注意点・落とし穴 発生する問題 推奨される対策
終了コード(Exit Code)への依存 「遮断したいから」とスクリプトを非ゼロの終了コードで終わらせて制御しようとする設計 公式ドキュメントが明示的に定義しているのは、標準出力に書き出す {“decision”: “deny”, “reason”: “…”}(PreToolUse)や {“decision”: “continue”, “reason”: “…”}(Stop)というJSONの契約のみ。終了コードに応じた遮断挙動は一部の非公式な検証記事で報告されているに過ぎず公式仕様には明記がないため、バージョン間で変わる可能性がある。制御は必ず sys.exit(0) + 標準出力のJSONで行い、終了コードの挙動には依存しないのが安全。
作業ディレクトリ(CWD)と相対パス スクリプト内の相対パス解決が失敗してモジュールが見つからない Hookの実行時カレントディレクトリ(CWD)は hooks.json が存在するディレクトリ。ワークスペース設定(.agents/hooks.json)なら、スクリプトは <Workspace_Root>/.agents/scripts/hooks/… に配置し python ./scripts/… のように相対パスで呼び出す。グローバル設定(~/.gemini/config/hooks.json)にはプロジェクト固有の相対フォルダが存在しないため、代わりに絶対パスでスクリプトを指定する。
重い処理によるエージェントのフリーズ 同期(Synchronous)実行のため、Hookスクリプトが重いと推論全体がフリーズする Hookは軽量な判定に留め、設定ファイルの timeout(秒)を必ず適切に指定する。重い静的解析はバックグラウンド化を検討する。
不親切な拒否理由(reason)による迷走 エージェントが同じ失敗コマンドを何度も再試行して無限ループに陥る denycontinue を返す際の reason には単に「禁止」「未完了」とだけ書くのではなく、「なぜ遮断・継続なのか」「代わりにどのコマンドや修正を行うべきか」を具体的に提示する。

まとめ:Rules・Skills・Hooksの三位一体で築く安全なAIDD

今回は、Googleの次世代AIコーディング環境「Antigravity」におけるAgent Lifecycle Hooksの全容と、安全なガードレール・品質統制の実装方法について詳しく解説しました。

AIエージェントによる開発が当たり前になるこれからの時代、エンジニアの仕事は「コードを一行ずつキーボードで打ち込む作業」から、「エージェントが自律的かつ安全に疾走できる環境と安全弁を設計すること」へとシフトしていきます。

要素 設定対象 役割とメタファー 特徴
Rules(方針) GEMINI.md, AGENTS.md 「速度標識」「行動方針」 常時コンテキストにロード。コーディング規約や設計思想をガイドする頭脳。
Skills(手順) skills/<name>/SKILL.md 「運行マニュアル」「手順書」 段階的開示(Progressive Disclosure)。依頼時のみ読み込まれる手足。
Hooks(安全弁) hooks.json 「非常ブレーキ」「自動検査ライン」 外部独立プロセス。危険な意図しない一括削除を遮断し、最低限のテスト実施確認を促すHard Guardrail。

「予期せぬ破壊的変更が怖いから使わない」のではなく、「Hookという強固な安全弁があるからこそ、エージェントに最大限の自律性を与えて全開でアクセルを踏める」。これこそが、エンタープライズ水準のAIDDが目指すべき理想の姿です。

まずはリポジトリの .agents/hooks.json に今回紹介した post_edit_formatter.py を仕掛けて「規約違反の自動矯正」を体験するもよし、端末の ~/.gemini/config/hooks.jsonpre_command_guard.py を仕掛けて「安全弁」を体験するもよし。まずは1つ、安心・安全なAI駆動開発の新しい一歩を踏み出してみませんか?

システムサポートでは、Google Cloudの導入や活用を支援しております。

Google Cloudを導入したい・導入したけど使いこなせていない…という方は、お気軽にご相談ください!

Google Cloud 導入・活動支援に関するご相談はこちら

2026年9月24日 【2026年9月版】Antigravity活用術~Hook完全攻略!ライフサイクルイベントによる自動ガードレールと品質統制~

Category Google Cloud

ご意見・ご相談・料金のお見積もりなど、
お気軽にお問い合わせください。

お問い合わせはこちら