Skip to content

SessionStart hook output is plain text; Codex rejects it as invalid JSON and drops the ruleset #156

Description

@Stropper1980

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions