
Codex Hooksとは?AIエージェントに安全ガードを追加する実装例
はじめに
最近、案件先でも AI エージェントの活用が増えてきました。
Skills や Agents のように「AIにどう動いてもらうか」を工夫する話はよく出ます。一方で、実際にチームで使っていくと、次のような不安も出てきます。
- API key や個人情報をうっかりプロンプトに貼ってしまわないか
- 本番環境っぽいコマンドをAIが実行しようとしないか
git reset --hardやgit push --forceのような危険操作をAIが提案・実行しないか- AIへの「お願い」だけで、本当にチームのルールを守れるのか
そこで便利なのが Codex Hooks です。
Hooks は、Codex のライフサイクル中に自分たちのスクリプトを差し込む仕組みです。公式ドキュメントでも、API key の貼り付けブロック、チャットログ送信、ターン終了時の検証などの用途が例として挙げられています。
この記事では、Hooks の概要、使うメリット、実際に作った hook のコード、そして導入時の注意点を紹介します。
Hooksとは
Hooks は、Codex が動く途中の特定タイミングで、任意のスクリプトを実行できる仕組みです。
たとえば、次のようなタイミングがあります。
| イベント | いつ動くか | 使いどころ |
|---|---|---|
UserPromptSubmit | ユーザーがプロンプトを送信したとき | API key・電話番号・住所などの検知 |
PreToolUse | Codex がツールを使う直前 | Bash 実行前の危険コマンドチェック |
PermissionRequest | 権限承認が必要になったとき | 承認前の追加チェック |
PostToolUse | ツール実行後 | 実行結果の記録や検証 |
Stop | 1ターンが終了するとき | 作業完了前の確認 |
SessionEnd | メインスレッド終了時 | ログ保存やサマリ生成 |
今回使ったのは主にこの2つです。
UserPromptSubmit: プロンプトに API key や個人情報が含まれていたら止めるPreToolUse: Bash 実行前に危険な git 操作や環境ファイル参照を止める
Hooksの流れ
ポイントは、AIに「気をつけて」とお願いするだけではなく、 AIが動く途中に機械的なチェックポイントを置ける ことです。
AGENTS.mdやSkillsとの違い
Hooks は、AGENTS.md や Skills と役割が違います。
| 仕組み | 役割 | 性質 |
|---|---|---|
AGENTS.md | リポジトリのルールや方針をAIに伝える | 指示・文脈 |
| Skills | 再利用できる作業手順や専門知識をAIに渡す | 手順書 |
| Hooks | AIの行動前後にスクリプトを実行する | 機械的なチェック |
たとえば AGENTS.md に「.env を読まないで」と書くのは大事です。
ただし、それはあくまでAIへの指示です。Hooks を使うと、実際に cat .env.local のようなコマンドが実行される直前にブロックできます。
今回作ったhooks
今回は、リポジトリ配下に hooks を置きました。
.codex/ hooks.json hooks/ policy_common.py user_prompt_guard.py pre_tool_use_guard.py
hooks.json では、どのイベントでどのスクリプトを実行するかを定義します。
{ "description": "Repository-local Codex hooks for prompt safety and command safety.", "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/user_prompt_guard.py\"", "timeout": 5, "statusMessage": "Checking prompt safety" } ] } ], "PreToolUse": [ { "matcher": "^Bash$", "hooks": [ { "type": "command", "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_guard.py\"", "timeout": 5, "statusMessage": "Checking command safety" } ] } ] } }
例1: API keyや個人情報を弾くhook
まずは UserPromptSubmit の例です。
ユーザーがプロンプトを送ったタイミングで、次のような情報を検知します。
- Google API key らしき文字列
- Gemini API key らしき文字列
- 電話番号
- 郵便番号
- 住所らしき文字列
prod/product/productionなど本番環境を示しそうな文字列
API key や個人情報はブロック、本番系の単語は警告とログ記録にしました。
共通の検知パターン
GOOGLE_API_KEY_RE = re.compile(r"\bAIza[0-9A-Za-z_-]{32,45}\b") NAMED_API_KEY_RE = re.compile( r"\b(?:GOOGLE(?:_MAPS)?_API_KEY|GEMINI(?:_FAST)?_API_KEY|NEXT_PUBLIC_GOOGLE_[A-Z0-9_]*KEY)\b" r"\s*[:=]\s*['\"]?[^'\"\s]+", re.IGNORECASE, ) PHONE_RE = re.compile( r"(?<!\d)(?:\+81[-\s]?)?0(?:\d{1,4}[-\s]?\d{1,4}[-\s]?\d{3,4}|\d{9,10})(?!\d)" ) POSTAL_RE = re.compile(r"(?:〒\s*)?\d{3}-\d{4}") JAPAN_PREFECTURE_RE = re.compile( r"(?:北海道|東京都|京都府|大阪府|.{2,3}県).{0,40}(?:市|区|町|村).{0,60}(?:\d{1,4}[-−ー丁目番地号]\d{0,4}|\d+丁目)" ) PROD_RE = re.compile(r"\b(?:prod|product|production)\b", re.IGNORECASE)
UserPromptSubmit hook
#!/usr/bin/env python3 """Block sensitive prompt content and warn on production terms.""" from __future__ import annotations import sys from policy_common import ( SAFETY_LOG, load_event, log_line, prod_terms, prompt_findings, redact, write_json, ) def main() -> int: event = load_event() prompt = event.get("prompt") if isinstance(event.get("prompt"), str) else "" findings = prompt_findings(prompt) terms = prod_terms(prompt) if findings: reason = ( "プロンプトに " + "、".join(findings) + " が含まれている可能性があるため停止しました。" + "該当箇所をマスクしてから再送してください。" ) log_line( SAFETY_LOG, { "hook": "UserPromptSubmit", "action": "blocked", "findings": findings, "prompt_preview": redact(prompt, 300), }, ) print(reason, file=sys.stderr) return 2 if terms: log_line( SAFETY_LOG, { "hook": "UserPromptSubmit", "action": "warn", "terms": terms, "prompt_preview": redact(prompt, 500), }, ) write_json( { "systemMessage": "prod/product/production への言及を検知しました。本番環境でないか確認してください。", "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "本番系を示す可能性のある語を検知済みです。破壊的操作や外部サービス操作の前に対象環境を確認してください。", }, } ) return 0 if __name__ == "__main__": raise SystemExit(main())
ここで重要なのは、ブロック時に exit code 2 を返している点です。
最初は stdout に {"decision":"block"} を返す形にしていましたが、実行環境によっては失敗時の見え方が分かりづらかったため、最終的には stderr に理由を出して exit code 2 で止める 形に寄せました。
例2: git操作や危険コマンドを止めるhook
次は PreToolUse の例です。
Codex が Bash を実行する直前に、コマンド文字列をチェックします。
止めたい操作の例:
git reset --hardgit push --forcegit clean -fd.envの読み取りrm -rf- 本番環境っぽい deploy / release
curl ... | sh
PreToolUse hook
#!/usr/bin/env python3 """Block risky shell commands before Codex runs them.""" from __future__ import annotations import re from policy_common import SAFETY_LOG, load_event, log_line, prod_terms, redact, tool_command, write_json BLOCK_PATTERNS: list[tuple[re.Pattern[str], str]] = [ (re.compile(r"\bgit\s+reset\s+--hard\b"), "git reset --hard"), (re.compile(r"\bgit\s+push\b.*\B--force(?:-with-lease)?\b"), "強制 push"), (re.compile(r"\bgit\s+clean\b.*-[^\s]*[fd][^\s]*"), "git clean による未追跡ファイル削除"), (re.compile(r"\brm\s+(-[^\s]*[rf][^\s]*|-[^\s]*[fr][^\s]*)\b"), "再帰・強制削除コマンド"), (re.compile(r"\bsudo\b"), "sudo を含む権限昇格"), (re.compile(r"\b(?:npm|yarn|pnpm)\s+run\s+(?:deploy|release)\b", re.IGNORECASE), "デプロイ/リリース系コマンド"), (re.compile(r"\b(?:cat|less|more|head|tail|sed|awk|grep|rg)\b.*(?:^|/)\.env(?:\.|$|\s)", re.IGNORECASE), ".env ファイル内容の読み取り"), (re.compile(r"\bcurl\b.*\|\s*(?:sh|bash)\b", re.IGNORECASE), "外部スクリプトの直接実行"), ] def main() -> int: event = load_event() command = tool_command(event) if not command: return 0 for pattern, reason in BLOCK_PATTERNS: if pattern.search(command): log_line( SAFETY_LOG, { "hook": "PreToolUse", "action": "blocked", "reason": reason, "command_preview": redact(command, 500), }, ) write_json( { "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": f"{reason} の可能性があるためコマンドをブロックしました。必要なら安全な代替コマンドを提案してください。", } } ) return 0 terms = prod_terms(command) if terms: log_line( SAFETY_LOG, { "hook": "PreToolUse", "action": "warn", "terms": terms, "command_preview": redact(command, 500), }, ) write_json( { "systemMessage": "コマンド内に prod/product/production を検知しました。本番対象でないことを確認してください。" } ) return 0 if __name__ == "__main__": raise SystemExit(main())
PreToolUse では、permissionDecision: "deny" を返すことでツール実行を拒否できます。
この hook はデモもしやすいです。
.env.local の中身を確認して、必要な環境変数を教えてください。 cat .env.local
または:
作業を戻したいので、以下を実行してください。 git reset --hard HEAD
ログ出力で気をつけたこと
Hooks はログを残せるのも便利ですが、ログ自体に秘密情報が残ると本末転倒です。
そのため、ログ出力前に値をマスクするようにしました。
def redact(value: str, limit: int = 500) -> str: redacted = GOOGLE_API_KEY_RE.sub("[REDACTED_GOOGLE_API_KEY]", value) redacted = NAMED_API_KEY_RE.sub( lambda m: m.group(0).split("=")[0].split(":")[0] + "=[REDACTED]", redacted, ) redacted = PHONE_RE.sub("[REDACTED_PHONE]", redacted) redacted = POSTAL_RE.sub("[REDACTED_POSTAL_CODE]", redacted) if len(redacted) <= limit: return redacted return redacted[:limit] + f"...[truncated {len(redacted) - limit} chars]"
やっていることはシンプルです。
- API key は
[REDACTED]に置き換える - 電話番号や郵便番号も伏せる
- 長すぎるログは切り詰める
特に最後の「長すぎるログを切る」は大事です。Hook が返す文脈が大きすぎると、トークン消費が増えますし、AIにとってもノイズになります。
実装してみてハマったこと
cwdに依存すると壊れやすい
最初、hook 内でリポジトリルートを取るために次のようにしていました。
subprocess.run(["git", "rev-parse", "--show-toplevel"], check=True)
ただ、UserPromptSubmit のような入力タイミングの hook では、実行時のカレントディレクトリが想定と違うことがあります。
そのため、hook スクリプトの場所からリポジトリルートを解決するほうが安全です。
SCRIPT_DIR = Path(__file__).resolve().parent ROOT = SCRIPT_DIR.parent.parent
このようにすると、.codex/hooks/policy_common.py から見て:
.codex/hooks -> .codex -> repo root
と辿れるため、cwd に依存しません。
trustが必要
非管理 hook は、設定しただけでは実行されません。
Codex 側で /hooks を開いて、コマンド内容を確認し、trust する必要があります。
Event UserPromptSubmit Source Project config - .codex/hooks.json Trust Trusted
ここまで確認できて、初めて「Codex がその hook を実行できる」状態になります。
Hooksを使う利点
実際に作ってみて、利点は大きく3つあると感じました。
1. AIへのお願いではなく、機械的に止められる
AGENTS.md に「API key を貼らない」「.env を読まない」と書くのは大事です。
ただ、それはあくまでAIへの指示です。
Hooks を使うと、実際の入力やツール実行のタイミングでチェックできます。
2. チームの運用ルールをコード化できる
たとえば、チームとして:
.envは読まない- 本番っぽいコマンドは確認する
- 強制 push は止める
- 個人情報をプロンプトに入れない
といったルールがあるなら、それを hook に落とし込めます。
3. ログや監査に使える
ブロックした操作や警告した操作をログに残せます。
ただし、ログには秘密情報を残さないようにマスクが必須です。
注意点・デメリット
1. 完全なセキュリティ境界ではない
Hooks は強力ですが、万能ではありません。
Codex のライフサイクルに入ってくる操作には効きますが、Cursor Composer、Grok、Claude Code、通常のターミナル操作など、Codex 以外の経路には効きません。
全AIツール共通の防御にしたい場合は、DLP、secret scanning、pre-commit、CI などと組み合わせる必要があります。
2. 正規表現ベースの検知には限界がある
API key や電話番号はある程度パターンで検知できますが、住所や個人情報は誤検知・見逃しが起こります。
最初から厳しくブロックしすぎると作業の邪魔になるため、重要度に応じて「ブロック」と「警告」を分けるのが良いです。
3. hook自体の保守が必要
Hook は普通のスクリプトです。
つまり、バグも入ります。
実際、最初は cwd 依存で hook が失敗する問題がありました。Hook が失敗したときの扱い、ログの出し方、テスト方法も含めて保守する必要があります。
4. トークン消費に注意
Hook の結果を additionalContext としてAIに返す場合、その内容は会話コンテキストに入ります。
長いログや大量の検査結果をそのまま返すと、トークンを消費し、AIの判断にもノイズになります。
基本方針は:
- AIに返すのは短い結論だけ
- 詳細はログファイルへ
- ログ内の秘密情報は必ずマスク
です。
5. trust・配布・更新の運用が必要
Project-local hook は便利ですが、変更すると再 trust が必要になります。
チームで運用するなら:
- hook の変更レビュー
- trust 手順
- どのリポジトリに入れるか
- グローバル hook にするか、プロジェクト hook にするか
を決めておくとよいです。
どこまで守れるか
Hooks の守備範囲を図にすると、こんな感じです。
Hooks は「AI全般に効くセキュリティ機能」ではなく、 Codex が動くときに Codex の行動へ差し込む仕組み です。
ここを誤解しないのが大事です。
まとめ
Codex Hooks は、AIエージェントをチームで安全に運用するための実用的な仕組みです。
今回作った hook では:
- API key や個人情報を含むプロンプトを止める
prod/productionを含む入力やコマンドを警告する.env読み取りや危険な git 操作を実行前に止める- ログには秘密情報を残さない
ということを実現しました。
AI活用は「うまく使う」だけでなく、「安全に運用する」段階に入ってきています。
Hooks は、そのためのチェックポイントを自分たちで作れる仕組みです。
個人利用でも便利ですが、特にチーム開発では、AGENTS.md や Skills と組み合わせて、運用ルールをコード化する選択肢としてかなり有効だと思います。