Skip to content

Codex --background killed by SessionEnd hook when wrapped in Claude Code Agent subagent #345

@qtec-technology

Description

@qtec-technology

Summary

When /codex:adversarial-review --background (or /codex:rescue --background) is invoked from a Claude Code Agent subagent (e.g. Agent(subagent_type: "codex:codex-rescue")), the Codex job is terminated by the SessionEnd hook the moment the subagent's session ends — which is almost immediately, because the subagent's only task is to fire --background and return. Result: no notification arrives, job state shows running: [] and latestFinished: null, and output files remain 0 bytes.

Calling the same Codex --background command directly from the main Claude Code session (via Bash tool or top-level slash command) works as documented — push notification fires on completion.

Environment

  • Claude Code: 2.1.91
  • Codex Plugin: 1.0.4 (codex@openai-codex)
  • Codex CLI: 0.130.0
  • Node.js: 24.13.1 (nvm-windows)
  • OS: Windows Server 2025 (sandbox = "elevated")
  • Auth: ChatGPT Team

Steps to reproduce

In a Claude Code session running on Windows:

  1. Invoke the rescue subagent with a --background task from the main session:
    Agent(
      subagent_type: "codex:codex-rescue",
      prompt: "--background --task \"Review the current branch diff against main\""
    )
    
  2. Observe the subagent returns immediately with an agentId and a "Codex is running in background" message.
  3. Wait 5–30 minutes (longer than any normal Codex review would take).
  4. Run node scripts/codex-companion.mjs status --all --json.

Expected

status --all --json reports the job as running (or eventually completed), and on completion a push notification reaches the main Claude Code session.

Actual

{
  "running": [],
  "latestFinished": null,
  "recent": [],
  "needsReview": false
}

Job output file (<temp>/claude/.../tasks/<id>.output) stays at 0 bytes. No notification ever arrives. The job is gone.

Root cause (from reading the plugin source)

The Agent subagent has its own sessionId, distinct from the main session. The subagent fires Codex via Bash → codex-companion.mjs task --background, which tags the job with the subagent's sessionId and returns immediately. The subagent then exits, triggering SessionEnd hook for that subagent's session.

scripts/session-lifecycle-hook.mjs lines 41–74 (cleanupSessionJobs):

function cleanupSessionJobs(cwd, sessionId) {
  // ...
  const removedJobs = state.jobs.filter((job) => job.sessionId === sessionId);
  // ...
  for (const job of removedJobs) {
    const stillRunning = job.status === "queued" || job.status === "running";
    if (!stillRunning) continue;
    try {
      terminateProcessTree(job.pid ?? Number.NaN);
    } catch { /* ignore teardown failures */ }
  }
  // ...
}

The newly-launched Codex job matches job.sessionId === sessionId (subagent's id) and is still queued/running, so terminateProcessTree(job.pid) kills the entire Codex process tree before Codex can write its completion state. From the main session's perspective, the job simply vanishes.

Why the same command works from the main session

When node codex-companion.mjs task --background is called directly from the main session (Bash tool, top-level slash command), the job is tagged with the main session's sessionId. The main session does not end during the Codex wait, so SessionEnd cleanup never fires. Codex runs to completion and pushes a notification normally.

Proposed fixes (any one, in order of preference)

  1. Detect subagent context and refuse --background. If codex-companion.mjs task --background is invoked from a session that the plugin can identify as a transient Agent subagent (heuristic: very short session lifetime, or specific env var set by Claude Code for subagent invocations), error out with: --background is unsafe inside a subagent because SessionEnd will terminate the job. Use --wait instead. Lets users opt into Pattern B (Agent + --wait).

  2. Re-tag the job with the parent session before forking. If a parent session ID is discoverable (e.g. via CLAUDE_PARENT_SESSION_ID env or similar), tag the job with the parent's id so SessionEnd of the subagent ignores it.

  3. Document the limitation prominently in the codex:codex-rescue subagent prompt. Today the prompt says:

    "If the request includes --background, run the codex:codex-rescue subagent in the background."

    This is ambiguous — it does not warn that wrapping Codex --background inside an Agent subagent will cause the job to be killed by SessionEnd. Recommend rewording to clarify the correct execution pattern (either Bash from main session for fire-and-forget, or run_in_background=true on the Agent tool itself + Codex --wait inside).

Workaround for users

Until fixed, use one of:

Pattern A (recommended for fire-and-forget review):

Bash → node ~/.claude/plugins/cache/openai-codex/codex/<ver>/scripts/codex-companion.mjs task --background --task "..."

(Called from main session — no Agent wrapper. Notification will fire on completion.)

Pattern B (recommended for review whose result feeds back into main session work):

Agent(run_in_background=true, subagent_type: "codex:codex-rescue", prompt: "--wait --task ...")

(Codex --wait inside subagent → subagent waits → returns Codex result → harness notifies main session.)

Pattern C (BROKEN):

Agent(subagent_type: "codex:codex-rescue", prompt: "--background --task ...")

(SessionEnd kills Codex on subagent exit.)

References

  • Plugin source: scripts/session-lifecycle-hook.mjs:41-74 (cleanupSessionJobs)
  • Plugin source: scripts/lib/tracked-jobs.mjs:142-204 (job lifecycle)
  • Architecture doc: docs/architecture.md (hooks table)
  • Empirical evidence: real session 2026-05-22, PR #358 Stage 4 review in qtec-technology/axon (private repo, but the symptom is fully reproducible against any branch)

Happy to PR any of the proposed fixes (1, 2, or 3) if maintainers indicate a preference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions