Skip to content

fix: Codex SessionStart hook rejects no-op fallback as invalid JSON#3127

Merged
thedotmack merged 2 commits into
mainfrom
fix/codex-session-start-invalid-json
Jul 4, 2026
Merged

fix: Codex SessionStart hook rejects no-op fallback as invalid JSON#3127
thedotmack merged 2 commits into
mainfrom
fix/codex-session-start-invalid-json

Conversation

@thedotmack

Copy link
Copy Markdown
Owner

Summary

  • hook-command.ts fell back to a bare {continue:true, suppressOutput:true} whenever a hook errored before its handler ran (missing session_id, invalid cwd, missing transcript path) — regardless of which hook event fired
  • Codex's strict SessionStart validator rejects that bare shape as "hook returned invalid session start JSON output", since the context handler is the SessionStart producer on every platform
  • buildNoOpResult(event) now attaches a minimal valid hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: '' } when the fallback fires for the context handler, keeping the no-op harmless everywhere else

This is the remaining half of the bug tracked in #2972 — the description-field parse rejection was already fixed separately, but the continue:true leak on the no-op fallback path was not. Supersedes #2953 (unmerged, only patched the shipped codex-hooks.json shell script rather than the root cause in worker-service.cjs).

Closes #2947
Closes #2972

Test plan

  • bun test — 2225 pass, 0 fail (added coverage for buildNoOpResult in tests/hook-command.test.ts)
  • tsc --noEmit clean
  • npm run build-and-sync — rebuilt bundle, restarted worker, confirmed fix compiled into plugin/scripts/worker-service.cjs
  • End-to-end: piped a Codex SessionStart payload with no session_id through the built worker-service.cjs hook codex context — before: {"continue":true} (invalid); after: {"continue":true,"hookSpecificOutput":{"hookEventName":"SessionStart"}} (valid)

🤖 Generated with Claude Code

https://claude.ai/code/session_011YMT5iCF1kfikq7fetR6NF

hook-command.ts fell back to a bare {continue:true, suppressOutput:true}
whenever a hook errored before its handler ran (missing session_id,
invalid cwd, missing transcript path). Codex's strict SessionStart
validator rejects that shape outright ("hook returned invalid session
start JSON output"), since `context` is the SessionStart handler on every
platform. buildNoOpResult() now attaches a minimal valid
hookSpecificOutput for that path so Codex's startup hook no longer fails.

Closes #2947
Closes #2972

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YMT5iCF1kfikq7fetR6NF
@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes Codex SessionStart no-op fallback output. The main changes are:

  • Adds buildNoOpResult for early graceful fallback paths.
  • Emits minimal SessionStart hookSpecificOutput for the context hook.
  • Preserves empty-string additionalContext in Codex output formatting.
  • Adds regression coverage for the fallback shape and adapter serialization.
  • Regenerates the bundled worker script with the same behavior.

Confidence Score: 5/5

Safe to merge with minimal risk.

The change is narrowly scoped to no-op fallback serialization and is covered by targeted regression tests.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • The base run of the contract-validation harness was executed to capture the initial state, which showed base stdout continuing true but missing the required SessionStart hookSpecificOutput and produced an INVALID validation.
  • The after-run capture was executed to obtain the head state, which showed continue true and a hookSpecificOutput with hookEventName SessionStart and empty additionalContext, yielding a VALID validation.
  • Checks demonstrated that neither the base nor the head artifacts leaked hookSpecificOutput into the non-context fallback.
  • The additionalContext_empty_string_preserved status was observed to flip from false in the base state to true in the head state.
  • The helper script artifact documenting the runtime harness used to generate the before/after captures was recorded.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/cli/hook-command.ts Adds buildNoOpResult so early graceful fallback for the context hook emits minimal SessionStart-specific output.
src/cli/adapters/codex.ts Preserves empty-string additionalContext in Codex output formatting so minimal SessionStart payloads remain schema-valid.
plugin/scripts/worker-service.cjs Rebuilds the bundled worker with the same no-op fallback and empty-string serialization changes.
tests/hook-command.test.ts Adds unit coverage for context no-op SessionStart fallback and ensures other hook events retain the bare no-op shape.
tests/hook-lifecycle.test.ts Adds Codex adapter coverage confirming explicit empty additionalContext is serialized.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Codex
  participant HookCommand as hookCommand(context)
  participant Adapter as codexAdapter
  participant Handler as contextHandler

  Codex->>HookCommand: SessionStart payload
  HookCommand->>Adapter: normalizeInput(raw)
  alt input rejected before handler runs
    Adapter-->>HookCommand: AdapterRejectedInput
    HookCommand->>HookCommand: buildNoOpResult("context")
    HookCommand->>Adapter: formatOutput(no-op SessionStart)
    Adapter-->>Codex: valid SessionStart no-op JSON
  else input accepted
    Adapter-->>HookCommand: NormalizedHookInput
    HookCommand->>Handler: execute(input)
    Handler-->>HookCommand: HookResult
    HookCommand->>Adapter: formatOutput(result)
    Adapter-->>Codex: formatted hook JSON
  end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Codex
  participant HookCommand as hookCommand(context)
  participant Adapter as codexAdapter
  participant Handler as contextHandler

  Codex->>HookCommand: SessionStart payload
  HookCommand->>Adapter: normalizeInput(raw)
  alt input rejected before handler runs
    Adapter-->>HookCommand: AdapterRejectedInput
    HookCommand->>HookCommand: buildNoOpResult("context")
    HookCommand->>Adapter: formatOutput(no-op SessionStart)
    Adapter-->>Codex: valid SessionStart no-op JSON
  else input accepted
    Adapter-->>HookCommand: NormalizedHookInput
    HookCommand->>Handler: execute(input)
    Handler-->>HookCommand: HookResult
    HookCommand->>Adapter: formatOutput(result)
    Adapter-->>Codex: formatted hook JSON
  end
Loading

Reviews (2): Last reviewed commit: "fix: preserve empty-string additionalCon..." | Re-trigger Greptile

codexAdapter.formatOutput used a truthy check on additionalContext,
silently dropping the key whenever it was an explicit empty string
(HookResult types it as a required string, not optional). The new
SessionStart no-op fallback from buildNoOpResult('context') hit this
exactly, emitting hookSpecificOutput without additionalContext at all.
Switch to a typeof check so the key is preserved whenever the caller
set it, matching the type contract.

Addresses review feedback on #3127.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YMT5iCF1kfikq7fetR6NF
@thedotmack
thedotmack merged commit ceae87a into main Jul 4, 2026
5 checks passed
@thedotmack
thedotmack deleted the fix/codex-session-start-invalid-json branch July 4, 2026 04:50
adambenhassen pushed a commit to adambenhassen/claude-mem-privacy that referenced this pull request Jul 9, 2026
codexAdapter.formatOutput used a truthy check on additionalContext,
silently dropping the key whenever it was an explicit empty string
(HookResult types it as a required string, not optional). The new
SessionStart no-op fallback from buildNoOpResult('context') hit this
exactly, emitting hookSpecificOutput without additionalContext at all.
Switch to a typeof check so the key is preserved whenever the caller
set it, matching the type contract.

Addresses review feedback on thedotmack#3127.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YMT5iCF1kfikq7fetR6NF
adambenhassen pushed a commit to adambenhassen/claude-mem-privacy that referenced this pull request Jul 9, 2026
codexAdapter.formatOutput used a truthy check on additionalContext,
silently dropping the key whenever it was an explicit empty string
(HookResult types it as a required string, not optional). The new
SessionStart no-op fallback from buildNoOpResult('context') hit this
exactly, emitting hookSpecificOutput without additionalContext at all.
Switch to a typeof check so the key is preserved whenever the caller
set it, matching the type contract.

Addresses review feedback on thedotmack#3127.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant