-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Codex --background killed by SessionEnd hook when wrapped in Claude Code Agent subagent #345
Description
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:
- Invoke the rescue subagent with a
--backgroundtask from the main session:Agent( subagent_type: "codex:codex-rescue", prompt: "--background --task \"Review the current branch diff against main\"" ) - Observe the subagent returns immediately with an agentId and a "Codex is running in background" message.
- Wait 5–30 minutes (longer than any normal Codex review would take).
- 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)
-
Detect subagent context and refuse
--background. Ifcodex-companion.mjs task --backgroundis 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). -
Re-tag the job with the parent session before forking. If a parent session ID is discoverable (e.g. via
CLAUDE_PARENT_SESSION_IDenv or similar), tag the job with the parent's id so SessionEnd of the subagent ignores it. -
Document the limitation prominently in the
codex:codex-rescuesubagent prompt. Today the prompt says:"If the request includes
--background, run thecodex:codex-rescuesubagent in the background."This is ambiguous — it does not warn that wrapping Codex
--backgroundinside 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, orrun_in_background=trueon the Agent tool itself + Codex--waitinside).
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 fix: reap codex app-server MCP subtree on POSIX shutdown #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.