How the Codex Plugin Spawns Detached Background Workers for Long-Running Tasks

The Codex plugin launches detached background workers using Node.js child_process.spawn with detached: true, stdio: "ignore", and windowsHide: true, followed by child.unref() to allow the parent CLI to exit while the worker continues running.

The openai/codex-plugin-cc repository implements reliable background task execution through process detachment. When users invoke codex task … --background, the system must create persistent workers that survive the original CLI session. Understanding how the Codex plugin spawns detached background workers reveals a carefully designed architecture using Node.js process management primitives.

The Core spawnDetachedTaskWorker Implementation

In plugins/codex/scripts/codex-companion.mjs, the spawnDetachedTaskWorker function handles the actual subprocess creation. This utility constructs the worker command and configures spawn options to ensure complete independence from the parent process.

The function executes the same script (codex-companion.mjs) with the task-worker subcommand, passing the job ID and working directory:

function spawnDetachedTaskWorker(cwd, jobId) {
  const scriptPath = path.join(ROOT_DIR, "scripts", "codex-companion.mjs");
  const child = spawn(process.execPath, [scriptPath, "task-worker", "--cwd", cwd, "--job-id", jobId], {
    cwd,
    env: process.env,
    detached: true,
    stdio: "ignore",
    windowsHide: true
  });
  child.unref();
  return child;
}

After spawning, the function immediately returns the child process object, allowing the caller to extract the PID for tracking purposes.

Process Isolation and Detachment Options

The Codex plugin uses specific Node.js spawn options to guarantee the worker can operate autonomously.

Independent Process Groups

Setting detached: true creates a new process group on POSIX systems, preventing SIGINT signals from propagating to the child when the parent terminates. This enables the background worker to continue execution even if the user closes the terminal or exits the CLI.

Standard I/O Disconnection

The stdio: "ignore" configuration disconnects all standard streams (stdin, stdout, stderr) between parent and child. Without this setting, the parent process would maintain open file descriptors, potentially blocking exit until the child terminates.

Cross-Platform Window Management

On Windows systems, the windowsHide: true flag suppresses the creation of console windows for background tasks. This provides a seamless experience where detached workers run silently without flashing terminal windows.

Recording and Tracking Background Jobs

Once spawned, the worker's PID must persist for lifecycle management. The enqueueBackgroundTask function in codex-companion.mjs writes a JSON job file containing the process identifier:

function enqueueBackgroundTask(cwd, job, request) {
  const child = spawnDetachedTaskWorker(cwd, job.id);
  const queuedRecord = {
    ...job,
    status: "queued",
    phase: "queued",
    pid: child.pid ?? null,
    logFile,
    request
  };
  writeJobFile(job.workspaceRoot, job.id, queuedRecord);
  upsertJob(job.workspaceRoot, queuedRecord);
  return { payload: { jobId: job.id, status: "queued", title: job.title, summary: job.summary, logFile } };
}

This record enables the job-control module to query status and terminate specific workers by their stored PID.

Cross-Platform Termination Logic

Terminating detached workers requires platform-specific handling implemented in plugins/codex/scripts/lib/process.mjs. The terminateProcessTree function delivers signals appropriately for each operating system.

On POSIX systems, the code sends SIGTERM to the negative PID (process group) using process.kill(-pid, "SIGTERM"), targeting all child processes simultaneously. On Windows, it executes taskkill with the /T flag to terminate the entire process tree.

import { terminateProcessTree } from "./process.mjs";

function cancelJob(job) {
  if (job.pid) {
    const result = terminateProcessTree(job.pid);
    // result = { attempted: true, delivered: true/false, method: "..."}
  }
}

This approach ensures clean shutdown regardless of whether the worker spawned additional subprocesses.

Summary

  • The spawnDetachedTaskWorker function in codex-companion.mjs creates background workers using child_process.spawn with detached: true and stdio: "ignore".
  • Calling child.unref() immediately after spawning allows the Node.js event loop to exit without waiting for the background task.
  • Job records store the worker PID (child.pid) to enable later status checks and termination via terminateProcessTree.
  • The termination utility in process.mjs handles cross-platform cleanup using SIGTERM on POSIX and taskkill on Windows.
  • Similar detachment patterns appear in app-server.mjs for spawning detached application server processes.

Frequently Asked Questions

What is the purpose of child.unref() in the Codex plugin?

The child.unref() method tells the Node.js event loop that the parent process should not remain active solely to keep the child process running. According to the openai/codex-plugin-cc source code, this call immediately follows the spawn operation in spawnDetachedTaskWorker, ensuring the CLI can return control to the user while the background worker continues independently.

How does the Codex plugin handle process termination on Windows?

On Windows, the plugin uses the windowsHide: true spawn option to prevent console windows from appearing, and relies on the terminateProcessTree function in process.mjs to execute taskkill with the /T flag. This kills the entire process tree, matching the POSIX behavior of sending signals to process groups.

Where is the background worker PID stored in the Codex plugin?

The PID is stored in the JSON job file created by enqueueBackgroundTask within codex-companion.mjs. The record includes pid: child.pid ?? null, allowing the job control system to reference the specific process ID for status queries or cancellation requests later.

Can detached Codex workers survive if the parent CLI process crashes?

Yes. Because the plugin uses detached: true when spawning, the child process runs in its own process group and is not dependent on the parent's lifecycle. The stdio: "ignore" configuration further ensures no open file descriptors tie the processes together, making the workers resilient to parent process termination.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →