How Codex-Plugin-CC Handles Process Termination Across Operating Systems

Codex-Plugin-CC uses platform-specific strategies to terminate process trees, employing taskkill on Windows and process-group signaling on Unix-like systems, with graceful fallbacks to prevent resource leaks.

The openai/codex-plugin-cc repository provides a robust cross-platform solution for managing child processes through its self-contained process library located in plugins/codex/scripts/lib/process.mjs. This implementation ensures reliable cleanup of process trees across Windows, macOS, and Linux without leaking system resources or leaving zombie processes.

Cross-Platform Process Termination Architecture

Core Implementation in process.mjs

The termination logic centers on the terminateProcessTree(pid, options) function exported from plugins/codex/scripts/lib/process.mjs. This helper validates input parameters—returning early if pid is not a finite number—and delegates to platform-specific implementations based on process.platform or an optional override.

Platform Detection and Overrides

By default, the library detects the operating system via process.platform, but callers can specify options.platform to force Windows (win32) or Unix-like behavior. This injection capability supports unit testing by allowing developers to simulate different environments without modifying global state.

Windows Process Termination Strategy

Primary taskkill Approach

On Windows platforms, the library executes taskkill /PID <pid> /T /F through the internal runCommand wrapper, which forces shell usage to resolve built-in commands. The /T flag terminates the entire process tree, while /F forces immediate termination. Success (status === 0) returns a result object with method: "taskkill".

Handling Missing Processes and Fallbacks

If taskkill reports that the target process does not exist—detected via the looksLikeMissingProcessMessage regex helper—the function returns attempted: true with delivered: false. When the taskkill binary itself is missing (ENOENT error), the library falls back to Node's native process.kill(pid), setting method: "kill". Any other taskkill errors are re-thrown to preserve exception context for upstream handlers.

Unix-like Process Termination Strategy

Process Group Signaling

For macOS, Linux, and other POSIX systems, the primary strategy sends SIGTERM to the entire process group using process.kill(-pid, "SIGTERM"). The negative PID signals all processes sharing the same group ID, effectively terminating the whole tree in one operation. This approach is recorded with method: "process-group".

Individual Process Fallback

When the group kill fails with ESRCH (no such process group), the library falls back to signaling the individual PID via process.kill(pid, "SIGTERM"). If this secondary attempt also fails with ESRCH, the result indicates the signal could not be delivered. Non-ESRCH errors from the initial group kill trigger the same fallback logic.

API Return Structure and Error Handling

Every termination attempt returns a uniform object regardless of platform:

{
  attempted: true|false,
  delivered: true|false,
  method: "taskkill" | "kill" | "process-group" | "process" | null,
  result: <spawnSync result> // present only for taskkill path
}

This consistent shape allows higher-level components like codex-companion.mjs to reason about termination status without platform-specific branching. The runCommand function normalizes execution options including working directory, environment variables, and stdio handling, while formatCommandFailure generates human-readable error messages from failed spawnSync results.

Practical Implementation Examples

The following examples demonstrate the public API for terminating processes and mocking dependencies:

import { terminateProcessTree } from "./plugins/codex/scripts/lib/process.mjs";

/* Terminate a Unix-style process tree */
const result = terminateProcessTree(12345);
console.log(`Method: ${result.method}, Delivered: ${result.delivered}`);

/* Force Windows behavior for testing */
const winResult = terminateProcessTree(6789, { platform: "win32" });

/* Inject custom command runner for unit testing */
const mockRun = (cmd, args) => ({ error: { code: "ENOENT" }, status: null });
const mocked = terminateProcessTree(5555, { runCommandImpl: mockRun });

Summary

  • Platform abstraction: The terminateProcessTree function in plugins/codex/scripts/lib/process.mjs automatically selects Windows or Unix termination strategies based on process.platform.
  • Windows execution: Uses taskkill /T /F for complete tree termination, falling back to process.kill only when the binary is missing.
  • Unix signaling: Prioritizes process-group SIGTERM (-pid) for batch termination, with individual PID fallback on ESRCH errors.
  • Consistent API: Returns uniform result objects with attempted, delivered, and method fields across all platforms and error conditions.
  • Testability: Supports platform overrides via options.platform and dependency injection via options.runCommandImpl.

Frequently Asked Questions

How does codex-plugin-cc terminate processes on Windows?

On Windows, it spawns taskkill /PID <pid> /T /F to forcefully terminate the process and its children. If taskkill is unavailable, it falls back to Node's process.kill(pid). The function detects missing processes via regex matching against taskkill output through the looksLikeMissingProcessMessage helper.

What happens when terminating a non-existent process on Linux?

The library first attempts process.kill(-pid, "SIGTERM") on the process group. If this returns ESRCH (no such process), it falls back to signaling the individual PID. If both fail with ESRCH, the result object contains attempted: true and delivered: false, allowing callers to distinguish between "process already gone" and "termination failed."

Can I test the termination logic without spawning real processes?

Yes. The terminateProcessTree function accepts options.runCommandImpl to override the command execution function, and options.platform to force specific OS behavior. This allows unit tests to mock taskkill responses or simulate ENOENT errors without shell execution.

Where does the high-level Codex companion use this termination logic?

The codex-companion.mjs script imports these utilities to manage child processes during plugin execution. It relies on the uniform return values from terminateProcessTree to determine whether cleanup succeeded before proceeding with subsequent operations.

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 →