背景
Codex に実装を任せるとき, memo.md を残す運用にしていた.
残したいのは完成後の要約だけではない. 途中で迷ったこと, 試して捨てた案, どの確認を通したのか, そういう部分を後から読み返したかった.
実際には漏れた. 実装やテストに集中すると, 最後の memo.md 更新が抜ける.
AGENTS.md には「作業内容は memo.md の末尾に追記すること」と書いていた. それでも抜けたので, 指示文だけで運用するのはやめた.
AGENTS.md だけでは強制にならない
AGENTS.md は方針を書く場所としては便利だ.
どんな粒度で記録するか, どの言語で返すか, commit 前に何を見るか. そういう作業方針を書くには向いている.
ただし, AGENTS.md は検査機構ではない. Codex が忘れても自動では止まらない.
必要だったのは, 「書いてほしい」というお願いではなく, 「書いていなければ終了できない」という仕組みだった.
最初に考えた案
最初に考えたのは, 全 tool 実行を PostToolUse hook で記録する方法だった.
これはすぐにやめた. 全コマンドの入出力を memo.md に流すと, メモではなくログになる. 後から読みたいのはコマンド列ではなく, 何を試して, どこで判断したかだった.
次は, commit 時だけ pre-commit で止める案.
これも足りない. commit しない相談や調査でもメモは残したい. それに, 最終回答の後で止めても少し遅い. その時点では, 途中で考えていたことがもう薄れている.
最終的に, Codex の turn 開始と終了に hook を入れた.
UserPromptSubmit でメモ枠を作る
UserPromptSubmit hook は, ユーザーの入力が Codex に送られるタイミングで動く.
ここで memo.md に今回分の枠を作る.
markdown<!-- codex-memo:pending session=... turn=... -->
- 要件: TODO
- 調査: TODO
- 設計判断: TODO
- 試行錯誤: TODO
- 実装: TODO
- 確認: TODO
- 残課題: TODO
<!-- /codex-memo:pending -->
この時点では中身は TODO でよい. 作業開始時点で「今回の枠」があることを優先した.
最初に枠を作っておけば, 終了時に「今回分が残っているか」を機械的に見られる.
Stop hook で終了を止める
Stop hook は, Codex が最終回答へ進む直前に動く.
ここで memo.md を見て, pending が残っていたり, 必須項目が TODO のままだったりしたら止める.
返す JSON はこの形にした.
json{
"decision": "block",
"reason": "memo.md の今回分が未完了です。要件, 調査, 設計判断, 試行錯誤, 実装, 確認, 残課題を追記してください。"
}
ここで decision: "block" を返すと, Codex はそのまま終われない. メモが未完了なら, memo.md を直してからもう一度終わる流れになる.
どの memo.md を見るか
最初の実装では, hook payload の cwd から Git repository root を探し, そこにある memo.md を見るようにしていた.
これは普通の単一 repo 作業では自然に見える. ただ, 実際に使うと邪魔になる場面があった. memo.md を置いていないフォルダで Codex を開いたときまで hook が動き, メモ作成を求めてしまう.
そこで, 対象を cwd 直下の memo.md に絞った.
text<opened-folder>/memo.md
このファイルがない場合, UserPromptSubmit も Stop も何もしない. memo.md を置いたプロジェクトだけ記録を強制し, 置いていない場所では通常どおり Codex を使えるようにした.
pending を消すだけでは通さない
単純に pending という文字だけを見ると, 雑に消せば通ってしまう.
そこで, UserPromptSubmit の時点で session と turn を cache に保存するようにした.
Stop では, 同じ session/turn の done マーカーを要求する.
markdown<!-- codex-memo:done session=... turn=... -->
- 要件: ...
- 調査: ...
- 設計判断: ...
- 試行錯誤: ...
- 実装: ...
- 確認: ...
- 残課題: なし
<!-- /codex-memo:done -->
項目は空欄にしない. 該当しないものは なし と書く. 本当に何もないのか, 書いていないだけなのかを分けるため.
内容の質までは判定しない. そこまで hook に持たせると, ただの壊れやすい採点になる. 今回は「今回分の記録が存在すること」までを見る.
複数エージェントで誤検出した
あとから問題が出た.
複数の Codex エージェントを動かしていると, あるエージェントが作った pending を別のエージェントの Stop hook が見て止まることがあった. これは運用としてかなり悪い. 自分の作業は書き終わっているのに, 別 turn の未完了枠で止まる.
そこで Stop hook は, 同じ session と turn のブロックだけを見るようにした.
markdown<!-- codex-memo:pending session=... turn=... -->
他の session / turn の pending は無視する. 一方で, pre-commit は最後の安全網なので全 pending を見る. commit する時点では, どのエージェント由来であっても未完了メモを残したくないからである.
この修正の途中で, もう一つ詰まった. 旧実装では UserPromptSubmit が state だけ保存し, 実際には今回 turn のメモ枠を作らない場合があった. その state を新しい Stop hook が読むと, 「今回の turn のメモ枠がない」と言って止まる.
これを避けるため, state に memo_required を持たせた.
- 今回 turn の
pendingを作った, または既に見つけた場合はmemo_required = true - 今回 turn の
doneが既にある場合や, 旧 state で判断できない場合は強制しない
これで, hook を更新した直後に古い state へ引っかかる問題も避けられる.
pre-commit でも止める
Codex の終了時に止めても, 手動で commit する場面はある.
そこで, .git/hooks/pre-commit にも同じ検査を入れた.
bashpython3 scripts/codex_memo_guard.py pre-commit
未完了の pending が残っていれば commit を止める. これは最後の網.
Stop hook は作業終了前に気づくため. pre-commit はそれでも漏れたときに commit を止めるため. 同じ検査でも役割が違う.
通常版とカスタム版の両方に入れた
最初は codex-custom にだけ hook を入れるつもりだった.
ただ, 実際には通常版の Codex でも作業する. 片方だけに入れると, 通常版で作業したときにまた漏れる.
なので, 通常版とカスタム版の両方の config に同じ hook を入れた.
toml[features]
hooks = true
[[hooks.UserPromptSubmit]]
[[hooks.UserPromptSubmit.hooks]]
type = "command"
command = 'python3 /path/to/scripts/codex_memo_guard.py prompt'
[[hooks.Stop]]
[[hooks.Stop.hooks]]
type = "command"
command = 'python3 /path/to/scripts/codex_memo_guard.py stop'
hook は設定しただけでは動かない. 新しい hook や変更された hook は, Codex の /hooks で信頼する必要がある.
ここは面倒だが, lifecycle に任意のスクリプトを差し込む仕組みなので, 勝手に動かない方がいい.
やってみて分かったこと
メモを残すだけなら, 自動ログを吐く方が簡単だった.
でも欲しかったのはログではなく, 後から読める作業記録だった. だから全 tool 実行は保存せず, 要件, 調査, 設計判断, 試行錯誤, 実装, 確認, 残課題を書かせる形にした.
AGENTS.md は「どう書くか」を伝える場所として残す. hook は「書かずに終われない」ようにする場所として使う.
この分担にしてから, 「書くつもりだった」で終わる余地が減った.