Codex Hooksとは?AIエージェントに安全ガードを追加する実装例

Codex Hooksとは?AIエージェントに安全ガードを追加する実装例

原
公開日:2026/08/17
読了目安:6分

はじめに

最近、案件先でも AI エージェントの活用が増えてきました。

Skills や Agents のように「AIにどう動いてもらうか」を工夫する話はよく出ます。一方で、実際にチームで使っていくと、次のような不安も出てきます。

  • API key や個人情報をうっかりプロンプトに貼ってしまわないか
  • 本番環境っぽいコマンドをAIが実行しようとしないか
  • git reset --hardgit push --force のような危険操作をAIが提案・実行しないか
  • AIへの「お願い」だけで、本当にチームのルールを守れるのか

そこで便利なのが Codex Hooks です。

Hooks は、Codex のライフサイクル中に自分たちのスクリプトを差し込む仕組みです。公式ドキュメントでも、API key の貼り付けブロック、チャットログ送信、ターン終了時の検証などの用途が例として挙げられています。

参考: OpenAI Docs - Codex Hooks

この記事では、Hooks の概要、使うメリット、実際に作った hook のコード、そして導入時の注意点を紹介します。


Hooksとは

Hooks は、Codex が動く途中の特定タイミングで、任意のスクリプトを実行できる仕組みです。

たとえば、次のようなタイミングがあります。

イベントいつ動くか使いどころ
UserPromptSubmitユーザーがプロンプトを送信したときAPI key・電話番号・住所などの検知
PreToolUseCodex がツールを使う直前Bash 実行前の危険コマンドチェック
PermissionRequest権限承認が必要になったとき承認前の追加チェック
PostToolUseツール実行後実行結果の記録や検証
Stop1ターンが終了するとき作業完了前の確認
SessionEndメインスレッド終了時ログ保存やサマリ生成

今回使ったのは主にこの2つです。

  • UserPromptSubmit: プロンプトに API key や個人情報が含まれていたら止める
  • PreToolUse: Bash 実行前に危険な git 操作や環境ファイル参照を止める

Hooksの流れ

Codex Hooks の実行フロー 入力時とツール実行前に、決定的なチェックポイントを差し込む ユーザー入力 プロンプト送信 UserPromptSubmit API key / 個人情報を検知 Codex 思考・実行計画 PreToolUse Bash実行前に検査 ツール実行 問題なければ実行 ブロック 貼り付け事故を止める 実行拒否 危険コマンドを止める OK OK 検知 検知 ポイント: AGENTS.md や Skills は「AIへの指示」。Hooks は、実行経路にスクリプトでチェックを差し込む。

ポイントは、AIに「気をつけて」とお願いするだけではなく、 AIが動く途中に機械的なチェックポイントを置ける ことです。


AGENTS.mdやSkillsとの違い

Hooks は、AGENTS.md や Skills と役割が違います。

仕組み役割性質
AGENTS.mdリポジトリのルールや方針をAIに伝える指示・文脈
Skills再利用できる作業手順や専門知識をAIに渡す手順書
HooksAIの行動前後にスクリプトを実行する機械的なチェック

たとえば 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 --hard
  • git push --force
  • git 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 の守備範囲を図にすると、こんな感じです。

Codex Hooks が効く範囲 Hooks は Codex のライフサイクルに入る操作へ効く。Codex以外の経路には別対策が必要。 Codex 経由: Hooks が効く Codex CLI / IDE extension Codexランタイムで動く UserPromptSubmit 入力時にAPI key・個人情報を検査 PreToolUse Bash実行前に危険操作を検査 検知・ブロック チームルールをコード化 Codex以外: Hooks は基本効かない Cursor Composer Cursor独自の実行経路 Grok / 他AIツール Codexライフサイクル外 通常のターミナル操作 Codexを通らない操作 別対策が必要 DLP / secret scanning / pre-commit / CI まとめ: Hooks は「Codexを安全に運用するためのガード」。全AIツール共通の防御ではない。

Hooks は「AI全般に効くセキュリティ機能」ではなく、 Codex が動くときに Codex の行動へ差し込む仕組み です。

ここを誤解しないのが大事です。


まとめ

Codex Hooks は、AIエージェントをチームで安全に運用するための実用的な仕組みです。

今回作った hook では:

  • API key や個人情報を含むプロンプトを止める
  • prod / production を含む入力やコマンドを警告する
  • .env 読み取りや危険な git 操作を実行前に止める
  • ログには秘密情報を残さない

ということを実現しました。

AI活用は「うまく使う」だけでなく、「安全に運用する」段階に入ってきています。

Hooks は、そのためのチェックポイントを自分たちで作れる仕組みです。

個人利用でも便利ですが、特にチーム開発では、AGENTS.md や Skills と組み合わせて、運用ルールをコード化する選択肢としてかなり有効だと思います。