Follow-up to #91, which is now fixed: ${CLAUDE_PLUGIN_ROOT} resolves and the hook no longer exits 1. The hook runs — but Codex still discards it, for a different reason.
Observed
i-have-adhd 0.3.0, codex-cli 0.154.0, Node v25.2.1, macOS.
Every Codex session opens with:
• Hook failed
└ hook returned invalid session start JSON output
The hook exits 0, but writes plain text to stdout:
$ R=~/.codex/plugins/cache/i-have-adhd/i-have-adhd/0.3.0
$ printf '{"hook_event_name":"SessionStart","cwd":"/tmp","session_id":"t","source":"startup"}' \
| CLAUDE_PLUGIN_ROOT="$R" node "$R/hooks/always-on.mjs"
ADHD MODE ACTIVE (always-on). The ruleset below applies to every response. "stop adhd mode" turns...
Claude Code accepts plain stdout from a SessionStart hook and treats it as context. Codex does not — it requires a JSON envelope, and on anything else it drops the entire hook output. The ruleset never reaches the model.
The failure is silent in the way that matters: the error line doesn't name which hook failed, and the plugin looks installed and enabled. On a machine with several plugins it is not obvious that ADHD mode simply isn't on in Codex.
Expected
Either output shape is accepted by Codex:
- empty stdout, or
- a single JSON object:
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"ADHD MODE ACTIVE ..."}}
Suggested fix: ship hooks/hooks.codex.json
Codex reads a plugin's hooks/hooks.codex.json in preference to hooks/hooks.json when present. That is the mechanism remember@claude-plugins-official (0.29.1) uses, and it works — its Codex session_start hook returns cleanly on the same machine where this one fails.
That keeps the Claude Code path untouched: hooks.json keeps emitting plain text, and a Codex-only entry emits the envelope. A thin wrapper around the existing always-on.mjs is enough — no change to the ruleset itself.
Verified workaround (not a fix)
For anyone hitting this before it lands: register a wrapper in your own ~/.codex/hooks.json that runs the plugin hook and wraps its stdout.
// envelope.mjs <command> [args...] — runs the command, wraps plain-text stdout in the envelope.
// Empty stdout stays empty; stdout that is already JSON passes through untouched.
Confirmed working: the ruleset lands in the session as a role=developer item, verified in
~/.codex/sessions/rollout-*.jsonl.
Two caveats. The plugin's own hook still runs alongside it, so the Hook failed line stays. And if Codex's automatic sync of hooks/skills/plugins is enabled, it removes the wrapper: that sync mirrors the Claude Code configuration into Codex, and a Codex-only entry has no counterpart to mirror, so it is dropped on the next sync. Turning that sync off — keeping only chats and projects synced — makes the wrapper persist.
Edit: an earlier version of this issue blamed a Codex-run cleanup of "non-working hooks" for removing the wrapper. That was wrong; the sync is the cause.
Happy to open a PR with the hooks.codex.json if that shape is welcome.
Follow-up to #91, which is now fixed:
${CLAUDE_PLUGIN_ROOT}resolves and the hook no longer exits 1. The hook runs — but Codex still discards it, for a different reason.Observed
i-have-adhd0.3.0,codex-cli0.154.0, Node v25.2.1, macOS.Every Codex session opens with:
The hook exits 0, but writes plain text to stdout:
Claude Code accepts plain stdout from a
SessionStarthook and treats it as context. Codex does not — it requires a JSON envelope, and on anything else it drops the entire hook output. The ruleset never reaches the model.The failure is silent in the way that matters: the error line doesn't name which hook failed, and the plugin looks installed and enabled. On a machine with several plugins it is not obvious that ADHD mode simply isn't on in Codex.
Expected
Either output shape is accepted by Codex:
{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"ADHD MODE ACTIVE ..."}}Suggested fix: ship
hooks/hooks.codex.jsonCodex reads a plugin's
hooks/hooks.codex.jsonin preference tohooks/hooks.jsonwhen present. That is the mechanismremember@claude-plugins-official(0.29.1) uses, and it works — its Codex session_start hook returns cleanly on the same machine where this one fails.That keeps the Claude Code path untouched:
hooks.jsonkeeps emitting plain text, and a Codex-only entry emits the envelope. A thin wrapper around the existingalways-on.mjsis enough — no change to the ruleset itself.Verified workaround (not a fix)
For anyone hitting this before it lands: register a wrapper in your own
~/.codex/hooks.jsonthat runs the plugin hook and wraps its stdout.Confirmed working: the ruleset lands in the session as a
role=developeritem, verified in~/.codex/sessions/rollout-*.jsonl.Two caveats. The plugin's own hook still runs alongside it, so the
Hook failedline stays. And if Codex's automatic sync of hooks/skills/plugins is enabled, it removes the wrapper: that sync mirrors the Claude Code configuration into Codex, and a Codex-only entry has no counterpart to mirror, so it is dropped on the next sync. Turning that sync off — keeping only chats and projects synced — makes the wrapper persist.Edit: an earlier version of this issue blamed a Codex-run cleanup of "non-working hooks" for removing the wrapper. That was wrong; the sync is the cause.
Happy to open a PR with the
hooks.codex.jsonif that shape is welcome.