Codex CLI v0.120のSessionStartフックで /clear を区別できるようになったので試してみた

Codex CLI v0.120のSessionStartフックで /clear を区別できるようになったので試してみた

2026.04.14

どうも!オペ部の西村祐二です!

Codex CLI v0.120.0 で SessionStart フックが /clear で作り直されたセッションと、通常起動・resume のセッションを区別できるようになりました。Claude Code では以前から source フィールドで区別できていた部分で、個人的にも挙動差が気になっていた箇所だったので、手元で設定を書いて startup / resume / clear の発火状況を確認してみました。

Codex CLI の SessionStart フックとは

Codex Hooks は、Codex CLI のセッションライフサイクルに合わせて任意のコマンドを実行できる仕組みです。このうち SessionStart はセッション開始時に一度だけ実行される hook で、標準出力に書かれたテキストは developer context としてモデル側に注入されます。

Plain text on stdout is added as extra developer context.(Codex Hooks ドキュメント より)

これまで Codex の SessionStart の matcher では startupresume が扱えましたが、v0.120.0 のリリースノートに含まれた PR #17073: Support clear SessionStart sourceclear が加わり、/clear 由来の新規セッションを他と区別できるようになっています。SessionStart hook そのものは Issue #13014(Claude parity を求める feature request、duplicate closed)などで要望が集まっていた機能で、今回の v0.120 でさらに粒度が細かくなった形です。

試してみる

環境

  • Codex CLI v0.120.0
  • macOS (darwin 24.6.0)

1. hooks を有効化する

Codex Hooks はフィーチャーフラグなので、まず ~/.codex/config.toml で有効化します。

~/.codex/config.toml
[features]
codex_hooks = true

有効化できているかは codex features list で確認できます。

$ codex features list | grep codex_hooks
codex_hooks                      under development  true

2. hooks.json を配置する

hook 本体は ~/.codex/hooks.json に書きます。公式ドキュメントによると Codex は active config layer の隣にある hooks.json を自動で検出するため、config.toml 側で参照設定を書く必要はありませんでした。

挙動を観察するため、startup / resume / clear と、その裏で発火する catch-all (.*) を並べた設定にしました。

~/.codex/hooks.json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/session_start.sh",
            "statusMessage": "SessionStart: catch-all"
          }
        ]
      },
      {
        "matcher": "^startup$",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/session_start.sh",
            "statusMessage": "SessionStart: startup"
          }
        ]
      },
      {
        "matcher": "^resume$",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/session_start.sh",
            "statusMessage": "SessionStart: resume"
          }
        ]
      },
      {
        "matcher": "^clear$",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/.codex/hooks/session_start.sh",
            "statusMessage": "SessionStart: clear"
          }
        ]
      }
    ]
  }
}

3. hook スクリプトを用意する

stdin から渡されるペイロードの source を拾ってログに追記し、additionalContext として構造化 JSON を標準出力に返す bash スクリプトを用意しました。

~/.codex/hooks/session_start.sh
#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"

SOURCE="$(printf '%s' "$INPUT" | python3 -c 'import json,sys
try:
    data=json.load(sys.stdin)
except Exception:
    print("unknown"); sys.exit(0)
print(data.get("source") or data.get("hookSpecificOutput",{}).get("source") or "unknown")
')"

TS="$(date +%Y-%m-%dT%H:%M:%S%z)"
{
  echo "[$TS] SessionStart source=$SOURCE"
  echo "$INPUT"
  echo "---"
} >> ~/.codex/hooks/session_start.log

python3 - "$SOURCE" "$TS" <<'PY'
import json, sys
source, ts = sys.argv[1], sys.argv[2]
print(json.dumps({
    "hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": f"[session_start hook] source={source} at {ts}",
    }
}))
PY

最初は素の文字列を stdout に出していたのですが、TUI に hook returned invalid session start JSON output と出てしまったため、先頭が [ のような「JSON っぽい」出力にすると JSON として検証されるようです。hookSpecificOutput.additionalContext 形式で返すと安全でした。

4. 発火状況を確認する

startup / clear / resume の 3 種類の source が本当に区別されるかを順に確認していきます。検証をクリーンに始めるため、事前にログをクリアしておきます。

: > ~/.codex/hooks/session_start.log

4-1. startup の確認

新しいターミナル(codex 未起動の状態)で codex を立ち上げます。設定ファイル変更前に起動したセッションを引き継がないよう、既存の codex セッションは事前に exit で閉じておいてください。

codex

起動直後に何か一言話しかけると、SessionStart hook (completed) の行が表示されます。additionalContext に書いた文字列が hook context: として差し込まれているのが分かります。

CleanShot 2026-04-14 at 23.21.00@2x

catch-all (.*) と ^startup$ の 2 つの matcher にマッチするため、hook (completed) は 2 回表示されます。

4-2. clear の確認

4-1 と同じセッションのまま /clear を入力します。/clear は TUI の表示バッファをリセットするため、hook 自体は発火していても SessionStart hook (completed) の行は画面からいったん消えます。そのまま何か 1 つプロンプトを送ると、新セッションの hook 実行結果が表示されます。

> /clear
(画面がクリアされる)
> hi
● SessionStart hook (completed)
    hook context: [session_start hook] source=clear at 2026-04-14T23:23:34+0900

CleanShot 2026-04-14 at 23.26.04@2x

画面上で確認できない場合でも、~/.codex/hooks/session_start.logsource=clear のエントリが追記されていれば hook は動いています。確認できたら /exit で codex を終了しておきます。

4-3. resume の確認

resume は codex を一度終了させた後に、通常のシェルから実行します。起動中のセッションがあれば /exit で抜けて、コマンドプロンプトに戻っている状態を作ってください。

codex resume --last

--last は直近のセッションを picker なしでそのまま再開するオプションです。再開した画面でプロンプトを送ると、SessionStart hook (completed)source=resume で表示されます。

CleanShot 2026-04-14 at 23.32.27@2x

なお、codex の中から ! codex resume --last のように呼ぶと Error: stdin is not a terminal で失敗します(shell tool 経由だと stdin が TTY として渡らないため)。必ず codex を終了してから通常のシェルで叩いてください。ここは最初にハマりました。

4-4. ログを確認する

3 つの source が揃ったら session_start.log を開きます。

cat ~/.codex/hooks/session_start.log

以下のように source ごとのエントリが並んでいれば成功です(catch-all と厳密 matcher の両方にマッチするため、各 source で 2 エントリずつ記録されます)。

~/.codex/hooks/session_start.log
[2026-04-14T23:08:59+0900] SessionStart source=startup
{"session_id":"019d8c52-...","transcript_path":"/path/to/.codex/sessions/.../rollout-....jsonl","cwd":"/path/to/project","hook_event_name":"SessionStart","model":"gpt-5.4","permission_mode":"default","source":"startup"}
---
[2026-04-14T22:57:24+0900] SessionStart source=clear
{"session_id":"019d8c48-...","transcript_path":"/path/to/.codex/sessions/.../rollout-....jsonl","cwd":"/path/to/project","hook_event_name":"SessionStart","model":"gpt-5.4","permission_mode":"default","source":"clear"}
---
[2026-04-14T23:09:28+0900] SessionStart source=resume
{"session_id":"019d8c52-...","transcript_path":"/path/to/.codex/sessions/.../rollout-....jsonl","cwd":"/path/to/project","hook_event_name":"SessionStart","model":"gpt-5.4","permission_mode":"default","source":"resume"}
---

source フィールドに startup / clear / resume がそれぞれ入っていて、セッションごとに session_idtranscript_path が切り替わる挙動を確認できました。

検証結果の考察

  • stdin で渡されるペイロードは session_id / transcript_path / cwd / model / permission_mode / source を含んでおり、source ごとに処理を分ける情報はここから取れます。source が matcher の比較対象です。
  • matcher は正規表現で評価されます。.* で catch-all を置くと毎回発火し、^clear$ のようにアンカーを付けると意図した source のときだけ発火しました。startup|resume のような書き方だと意図しない source も巻き込むので、ソースごとに処理を分けたい場合はアンカー付きで書き分けるのが安全そうです。
  • stdout に素のテキストを書くと TUI 側で JSON パースエラーになるケースがありました。ドキュメントには plain text が developer context として扱われると記載されていますが、手元では hookSpecificOutput.additionalContext を含む構造化 JSON を返すほうが確実でした。
  • Claude Code の SessionStart hook には compact 相当の source も存在しますが、Codex 側 v0.120 時点で確認できた source は startup / resume / clear の 3 種類で、compact に対応する source は今回の検証範囲では確認できませんでした。

試してみた感想

  • これまでは Codex のラッパースクリプト側で「新規セッションかどうか」を判定して context を差し込むような運用をしていましたが、SessionStart 側で matcher を分けるだけで済むので、その層が不要になって見通しがすっきりしました。
  • additionalContexthook context: として TUI にそのまま見えるのは、hook が動いたことが視覚的に分かって良い体験でした。運用中に「どの hook が走ったか」がログを掘らずに追えるのは助かりそうです。
  • 一方で、matcher が正規表現である都合上、意図せず複数ブロックにマッチさせてしまうミスを踏みやすいと感じました。catch-all を入れるとデバッグは楽ですが、本番運用時は ^clear$ のようにアンカー付きに寄せておいたほうが安全そうです。
  • Claude Code と設計思想が揃ってきているので、両方使っている立場としては hook を移植しやすくなった印象です。

まとめ

Codex CLI v0.120.0 で SessionStart hook の matcher に clear が加わり、/clear 由来の新規セッションを明示的にハンドリングできるようになりました。手元で試した限り、startup / resume / clear の 3 種類すべてが意図通り発火しています。Codex 側でセッション開始時の context 注入を書いている方は、一度 matcher の見直しをしても良さそうです。

誰かの参考になれば幸いです。


参考リンク:

この記事をシェアする

関連記事