Skip to content

macOS: live wrapper PID fails identity validation after exec, risking auto-reap and missing results #105

Description

@zhoushoujianwork

Summary

On macOS, getProcessIdentity() includes comm in the identity and describes the result as immutable. A legitimate exec changes comm while preserving the PID and process start time. Consequently, validateProcessIdentity() returns false for a live, non-reused PID.

This matters for the supported CC_PLUGIN_CODEX_CLAUDE_BIN override when the executable is a shell wrapper. runClaudeTurn() captures its identity immediately after spawning, and reapStaleJobs() interprets an identity mismatch as a dead process and marks the job failed.

Related: #79 and #83 (closed/fixed permission-denied cases). This report concerns an identity mismatch after exec, not EPERM/EACCES.

Environment

  • cc@sendbird / cc-plugin-codex 1.5.0
  • Codex CLI 0.153.4; Claude Code 2.1.223
  • macOS; Node.js 24.5.0
  • Custom Claude executable wrapper configured through CC_PLUGIN_CODEX_CLAUDE_BIN
  • Wrapper reads literal gateway environment settings, then launches Claude; no credentials included here
  • Current upstream main inspected at 19e5651 contains the relevant identity implementation

Deterministic minimal reproduction (no model call)

Run from a checkout of this repository on macOS with node on PATH:

node --input-type=module - <<'NODE'
import { spawn } from 'node:child_process';
import {
  getProcessIdentity,
  validateProcessIdentity,
  isProcessAlive,
} from './scripts/lib/process.mjs';

const child = spawn('/bin/zsh', [
  '-c',
  `read -r trigger; exec /usr/bin/env node -e 'process.stdout.write("ready");setTimeout(()=>{},10000)'`,
], { stdio: ['pipe', 'pipe', 'pipe'] });

try {
  await new Promise(resolve => setTimeout(resolve, 100));
  const before = getProcessIdentity(child.pid);
  const ready = new Promise((resolve, reject) => {
    child.stdout.once('data', resolve);
    child.once('error', reject);
  });
  child.stdin.end('go\n');
  await ready;
  const after = getProcessIdentity(child.pid);
  console.log({
    samePid: true,
    sameStartTime: before.slice(0, 24) === after.slice(0, 24),
    identityChanged: before !== after,
    alive: isProcessAlive(child.pid),
    identityValidated: validateProcessIdentity(child.pid, before),
  });
} finally {
  child.kill('SIGTERM'); // Only this reproduction's own child.
}
NODE

Observed against installed 1.5.0:

{"samePid":true,"sameStartTime":true,"identityChanged":true,"alive":true,"identityValidated":false}

The child remains the same process across exec; it has not exited and its PID has not been reused.

Real-world missing-result incident (supporting evidence, root cause not established)

A long, read-only Claude task launched through the Codex forwarding agent produced this timeline on 2026-09-10 (UTC):

Time Observation
06:11:47 Tracked task started.
06:16:37 Claude tool activity continued.
06:16:44 Job receipt transitioned to failed: Process <pid> died without completing. Auto-reaped. No result field was saved.
06:18:08 The forwarding agent explicitly terminated the still-running companion with SIGTERM; companion exited 143.
06:19:39 The exact original Claude session wrote an end_turn assistant text of 13,077 characters. No additional user prompt was present.

The final answer was recovered from the original native Claude transcript; the tracked job remained failed without that answer.

Limitations: the receipt cleared the original pidIdentity during reaping, so we cannot prove the exec mismatch caused this specific incident. The explicit later termination is also a confounding factor for result delivery; we are not claiming that the plugin spontaneously killed the companion. The confirmed defect is the independently reproduced false identity verdict. The incident demonstrates that a failed tracker state does not by itself prove that underlying Claude work has stopped.

Short real calls succeed: a fresh Codex forwarding agent received an exact fixed-token response from Claude in approximately 8 seconds using the same wrapper, and the completed receipt contained that response. Authentication currently works.

Expected behavior / suggested regression coverage

  • A legitimate exec should not invalidate the identity of the same live process.
  • Preserve PID-reuse protection using an identity based on process birth/start information that remains stable across exec; do not simply discard identity checks.
  • Add a macOS shell-to-executable regression test and retain the existing denied-probe and dead/PID-reuse coverage.
  • Consider preserving diagnostic evidence for the reaping decision so the original expected/observed identity or probe failure can be investigated.
  • Treat wrapper/supervisor liveness and underlying Claude completion carefully; surface missing-result/unknown outcomes truthfully without automatically rewriting historical receipts as successful.

Private prompts, checkout paths, credentials, native transcripts, and session identifiers have been omitted.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions