Problem
.codex/hooks/changelog_advisory.py(28行、PostToolUse / apply_patch|Edit|Write)は
tools/aggregate_changelog.sh check を実行し、戻り値が 0 以外なら一律 exit 2 を返す。
result = subprocess.run([str(root / "tools" / "aggregate_changelog.sh"), "check"], ...)
if result.returncode == 0:
return 0
sys.stderr.write(result.stderr or result.stdout)
return 2
Codex hook の exit 2 は編集のブロックである。したがって次の2つが区別されない:
- チェッカが動いて「フラグメントが無い」と判定した(正当な advisory 所見)
- チェッカがそもそも動かなかった(環境の失敗)
tools/aggregate_changelog.sh:2 は
(( BASH_VERSINFO[0] >= 4 )) || { echo "aggregate_changelog.sh requires Bash 4 or newer" >&2; exit 1; }
で始まる。macOS 既定の bash は 3.2 なので、シェバン #!/usr/bin/env bash が
homebrew の bash を拾えなかった場合、スクリプトは内容を一切見ずに exit 1 する。
それがそのまま exit 2 になり、編集がブロックされる。
実測(2026-08-28)
Codex エージェントの編集が次の表示でブロックされた:
• Edited rust/fslc/src/main.rs (+1 -1)
• PostToolUse hook (blocked)
feedback: aggregate_changelog.sh requires Bash 4 or newer
編集内容は changelog と無関係(Rust の関数呼び出し1行)。フラグメントの状態も適切だった。
にもかかわらず「フラグメント違反」と同じ経路でブロックされ、
エージェントに返ったメッセージは requires Bash 4 or newer だけである。
これは「あなたの編集に問題がある」とも「フックが動かなかった」とも読めない。
なぜ単なる環境問題ではないか
このフックの判定は ambient state で変わる。 同一セッション中、同じ種類の編集が
通ることもブロックされることもあった。subprocess.run はスクリプトを直接実行するので、
シェバンが解決する bash はフックプロセスが継承した PATH 次第である。
homebrew の bash が PATH にあれば通り、無ければブロックされる。
AGENTS.md の不変条件はこの形を明示的に否定している:
「A control whose verdict depends on ambient state is not a control.」
このフックはまさにそれで、しかも編集をブロックする側にある。
名前が changelog_advisory で docstring も
"Return canonical changelog-checker feedback" と述べているのに、
実際には advisory ではなくゲートとして働いている点も、宣言と執行の不一致である。
提案
1. 「動かなかった」と「違反を見つけた」を分ける。
aggregate_changelog.sh が起動不能(Bash バージョン不足など)なら、
それは advisory を提供できなかったということなので、編集をブロックしない。
stderr に理由を出して exit 0 を返す。違反を見つけたときだけ exit 2 にする。
区別の手段としては、aggregate_changelog.sh が
「実行不能」に専用の終了コード(例 3)を使うのが最も明示的。
現行の exit 1 は「違反あり」と共用されている。
2. bash 4 を明示的に探す。
#!/usr/bin/env bash に頼らず、フック側で bash 4+ の実体を解決してから渡す
(/opt/homebrew/bin/bash などを候補に、BASH_VERSINFO を確認)。
見つからなければ 1 の経路で「advisory を提供できなかった」として通す。
3. 名前と挙動を一致させる。
ブロックする設計を維持するなら advisory という名前を変える。
advisory を維持するなら exit 2 を使わない。どちらでもよいが、両方ではいけない。
受け入れ条件
参考
.codex/hooks/hooks.json の PreToolUse には cargo_pre_tool_use.py があり、
これが cargo コマンドを cargo_lock.py で自動的に包む。その自動包摂が
明示呼び出しと衝突して自己デッドロックする件は #946。
Refs #922, #924, #929, #946.
Problem
.codex/hooks/changelog_advisory.py(28行、PostToolUse /apply_patch|Edit|Write)はtools/aggregate_changelog.sh checkを実行し、戻り値が 0 以外なら一律 exit 2 を返す。Codex hook の exit 2 は編集のブロックである。したがって次の2つが区別されない:
tools/aggregate_changelog.sh:2は(( BASH_VERSINFO[0] >= 4 )) || { echo "aggregate_changelog.sh requires Bash 4 or newer" >&2; exit 1; }で始まる。macOS 既定の bash は 3.2 なので、シェバン
#!/usr/bin/env bashがhomebrew の bash を拾えなかった場合、スクリプトは内容を一切見ずに exit 1 する。
それがそのまま exit 2 になり、編集がブロックされる。
実測(2026-08-28)
Codex エージェントの編集が次の表示でブロックされた:
編集内容は changelog と無関係(Rust の関数呼び出し1行)。フラグメントの状態も適切だった。
にもかかわらず「フラグメント違反」と同じ経路でブロックされ、
エージェントに返ったメッセージは
requires Bash 4 or newerだけである。これは「あなたの編集に問題がある」とも「フックが動かなかった」とも読めない。
なぜ単なる環境問題ではないか
このフックの判定は ambient state で変わる。 同一セッション中、同じ種類の編集が
通ることもブロックされることもあった。
subprocess.runはスクリプトを直接実行するので、シェバンが解決する bash はフックプロセスが継承した PATH 次第である。
homebrew の bash が PATH にあれば通り、無ければブロックされる。
AGENTS.md の不変条件はこの形を明示的に否定している:
「A control whose verdict depends on ambient state is not a control.」
このフックはまさにそれで、しかも編集をブロックする側にある。
名前が
changelog_advisoryで docstring も"Return canonical changelog-checker feedback" と述べているのに、
実際には advisory ではなくゲートとして働いている点も、宣言と執行の不一致である。
提案
1. 「動かなかった」と「違反を見つけた」を分ける。
aggregate_changelog.shが起動不能(Bash バージョン不足など)なら、それは advisory を提供できなかったということなので、編集をブロックしない。
stderr に理由を出して exit 0 を返す。違反を見つけたときだけ exit 2 にする。
区別の手段としては、
aggregate_changelog.shが「実行不能」に専用の終了コード(例 3)を使うのが最も明示的。
現行の exit 1 は「違反あり」と共用されている。
2. bash 4 を明示的に探す。
#!/usr/bin/env bashに頼らず、フック側で bash 4+ の実体を解決してから渡す(
/opt/homebrew/bin/bashなどを候補に、BASH_VERSINFOを確認)。見つからなければ 1 の経路で「advisory を提供できなかった」として通す。
3. 名前と挙動を一致させる。
ブロックする設計を維持するなら
advisoryという名前を変える。advisory を維持するなら exit 2 を使わない。どちらでもよいが、両方ではいけない。
受け入れ条件
aggregate_changelog.shが起動不能な環境で、changelog と無関係な編集がブロックされない(起動不能を再現するには PATH から bash 4 を外す)
参考
.codex/hooks/hooks.jsonの PreToolUse にはcargo_pre_tool_use.pyがあり、これが cargo コマンドを
cargo_lock.pyで自動的に包む。その自動包摂が明示呼び出しと衝突して自己デッドロックする件は #946。
Refs #922, #924, #929, #946.