開發者交付任務時,需要確認分支、檢查未提交變更,並在修改完成後執行 Lint。這些步驟看似簡單,工作一忙就可能漏掉。
掛鉤(Hooks)可以在 Codex 工作階段的指定時間點自動呼叫命令,讓固定檢查跟著任務一起執行。
Hook 是 Codex 工作流程旁的自動化層。它可以讀取事件資料、執行專案腳本,並將結果送回 Codex。
適合放入 Hook 的工作具備耗時短、結果明確、重複性高等特徵,例如顯示 Git 狀態、執行 Lint、保存檢查紀錄,或在驗證失敗時阻止回合直接結束。
Codex Hooks 提供多種生命週期事件。SessionStart 發生在工作階段啟動或恢復時,UserPromptSubmit 發生在使用者送出提示詞(Prompt)時,PreToolUse 與 PostToolUse 位於工具執行前後,Stop 則在 Codex 準備結束目前回合時觸發。每個事件收到的輸入與可回傳的決定並不相同。
今天我們會使用兩個事件,SessionStart 負責讀取目前分支與工作目錄狀態,把起始條件交給 Codex。Stop 負責執行專案既有的 Lint。Lint 失敗時,Stop Hook 會要求 Codex 繼續處理一次,再讓開發者檢查結果。
Hook 可以放在使用者層級的 ~/.codex/hooks.json,也可以放在專案的 .codex/hooks.json。
專案規則應放進儲存庫內,讓團隊共同維護。Codex 只會在受信任的專案中載入專案 Hook。第一次載入或內容變更後,可用 /hooks 查看實際命令並重新確認信任。
Codex 從錯誤分支開始修改,或把既有未提交內容當成自己的變更,後續 Diff 會很難判讀。
SessionStart Hook 可以先執行唯讀 Git 命令,取得分支名稱與 git status --short。這項檢查不清理檔案,也不切換分支,只把現況加入工作階段上下文。
工作目錄有變更時,不必一律阻止工作開始。那些內容可能是開發者正在處理的工作。Hook 應清楚列出狀態,讓 Codex 保留既有變更並避免覆寫。開發者負責判斷是否要先建立 Commit、切換工作樹或中止任務。
SessionStart 的標準輸出可以成為額外的開發者上下文,因此腳本回傳簡短文字即可。輸出應控制在分支與狀態摘要,不要把大型差異(Diff)全部塞進上下文,也不要輸出 Token、環境變數或其他敏感資料。
Stop 會在 Codex 準備結束回合時執行,適合放置短時間、可重複的驗證。
今天我們會呼叫 npm run lint,沿用專案已定義的規則。專案若使用 pnpm lint、ruff check . 或其他命令,只要替換腳本中的命令即可。
Stop Hook 成功時必須輸出合法 JSON 或保持無輸出。失敗時可以回傳 decision: "block" 與原因,要求 Codex 再處理一輪。這裡的 block 代表延後結束回合,並用原因建立新的延續提示詞,它不會撤銷已完成的檔案修改。
事件資料中的 stop_hook_active 可判斷這個回合是否已被 Stop Hook 延續過。
範例只允許自動延續一次,第二次失敗會顯示警告並結束,避免 Lint 無法自動修正時形成循環。開發者接著可查看錯誤、修改需求或親自處理。
先建立 .codex/hooks/checks.py。同一支腳本會依 hook_event_name 分派檢查,並把每次結果以 JSON Lines(JSONL)寫入 Git 內部路徑。
紀錄不會出現在工作目錄的差異,仍可用 Git 命令找出並查看。
#!/usr/bin/env python3
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
def run(*args):
return subprocess.run(args, text=True, capture_output=True)
def git(*args):
return run("git", *args)
def record(event, status, detail):
log_path = git("rev-parse", "--git-path", "codex-hook-results.jsonl")
if log_path.returncode != 0:
return
path = Path(log_path.stdout.strip())
path.parent.mkdir(parents=True, exist_ok=True)
entry = {
"time": datetime.now(timezone.utc).isoformat(),
"event": event,
"status": status,
"detail": detail,
}
with path.open("a", encoding="utf-8") as output:
output.write(json.dumps(entry, ensure_ascii=False) + "\n")
data = json.load(sys.stdin)
event = data.get("hook_event_name")
if event == "SessionStart":
branch = git("branch", "--show-current")
status = git("status", "--short")
if branch.returncode or status.returncode:
record(event, "warning", "Git 狀態讀取失敗")
print("Git 起始檢查失敗,修改前請先確認目前目錄。")
sys.exit(0)
branch_name = branch.stdout.strip() or "detached HEAD"
changes = status.stdout.strip()
state = changes if changes else "工作目錄乾淨"
record(event, "passed", f"{branch_name}: {state}")
print(f"Git 起始狀態:分支 {branch_name}\n{state}")
sys.exit(0)
if event == "Stop":
lint = run("npm", "run", "lint")
output = (lint.stdout + "\n" + lint.stderr).strip()[-4000:]
if lint.returncode == 0:
record(event, "passed", "npm run lint 通過")
print(json.dumps({"systemMessage": "任務後 Lint 已通過。"}, ensure_ascii=False))
sys.exit(0)
record(event, "blocked", output)
if not data.get("stop_hook_active", False):
print(json.dumps({
"decision": "block",
"reason": "npm run lint 失敗。請讀取錯誤、做最小修正並重新驗證。"
}, ensure_ascii=False))
else:
print(json.dumps({
"systemMessage": "Lint 再次失敗,請由開發者檢查紀錄與錯誤輸出。"
}, ensure_ascii=False))
腳本只保留 Lint 輸出的最後 4,000 個字元,避免紀錄與回傳內容過大。
Hook 紀錄放在 git rev-parse --git-path codex-hook-results.jsonl 指向的位置。一般儲存庫會落在 .git/codex-hook-results.jsonl,Git worktree 也能取得正確的內部路徑。
.codex/hooks.json 綁定兩個事件接著建立 .codex/hooks.json,將兩個事件都指向剛才的 Python 腳本。
Hook 執行時的目前目錄可能是儲存庫內的子目錄,因此命令先用 git rev-parse --show-toplevel 找出專案根目錄,再組合腳本路徑。
{
"description": "Check Git state before work and run lint before a turn ends.",
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/checks.py\"",
"timeout": 10,
"statusMessage": "Checking Git starting state"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/checks.py\"",
"timeout": 120,
"statusMessage": "Running project lint"
}
]
}
]
}
}
這份設定以 macOS 與 Linux 的 Shell 命令為例。Windows 專案可在每個處理器加入 commandWindows,使用 PowerShell 找出 git rev-parse --show-toplevel 的結果,再以 py -3 執行同一支腳本。
團隊應依固定開發環境提交可直接執行的命令,避免每位成員自行猜測 Python 啟動方式。
多個符合條件的 Hook 會一起啟動,設定檔的排列順序不代表執行順序。彼此有先後依賴的檢查應放在同一支腳本中依序執行。
本例只有一個 Stop 處理器,Lint 結果不會和另一個 Hook 競爭。
新增設定後,開啟新的 Codex 工作階段並執行 /hooks。先核對事件、命令、腳本位置與 timeout,再信任專案 Hook。
信任會綁定 Hook 定義的雜湊,設定變更後,Codex 會暫停執行它,等開發者重新檢查與確認。
第一次測試可在乾淨分支啟動工作階段,確認畫面顯示分支與「工作目錄乾淨」。
接著建立一項未提交的小變更,再重新啟動,確認 Codex 收到檔案狀態。這兩次測試都不應自動切換分支、還原內容或建立 Commit。
任務後測試要先執行一次 npm run lint,確認專案命令可用。再暫時加入一個能被 Lint 穩定抓到的錯誤,請 Codex 執行小型任務。回合結束時應出現阻擋理由,Codex 取得一次修正機會。完成測試後記得要還原刻意加入的錯誤。
git status --short
npm run lint
git rev-parse --git-path codex-hook-results.jsonl
tail -n 10 "$(git rev-parse --git-path codex-hook-results.jsonl)"
紀錄中的 passed 表示對應命令成功,warning 表示起始狀態無法讀取,blocked 表示 Lint 回傳非零結束碼。
它只能證明 Hook 有執行及命令結果,不能取代完整測試、人工審查或持續整合紀錄。
Hook 會隨工作階段自動執行,命令內容必須比一般臨時指令保守。適合的操作包含唯讀檢查、專案既有驗證與本機紀錄。
部署、推送 Git、刪除資料、修改正式環境、讀取大量祕密或自動核准權限,都不應藏在生命週期事件裡。
專案 Hook 屬於儲存庫內容,分支切換或拉取更新都可能改變它。/hooks 的信任確認提供一個檢查點,開發者仍要閱讀腳本及它呼叫的下游命令。npm run lint 這類命令也可能被 package.json 改寫,因此 Review 時要一併檢查。
Hook 可補上固定流程中的遺漏,不能當成完整的安全邊界。沙箱、批准政策、持續整合權限與人工審查各自處理不同風險。
Hook 失敗時,較安全的處理方式是留下清楚結果、停止自動延續,讓開發者根據錯誤決定後續動作。