# How Process Termination Works for Background Codex Jobs in the OpenAI Codex Plugin

> Understand how OpenAI Codex Plugin terminates background jobs using taskkill and SIGTERM signals. Learn about the centralized terminateProcessTree helper for efficient process management.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: internals
- Published: 2026-07-18

---

**The OpenAI Codex Plugin uses a centralized `terminateProcessTree` helper in `process.mjs` to stop background jobs, employing `taskkill` on Windows and `SIGTERM` signals to process groups on Unix-like systems.**

Background Codex jobs—long-running companion processes spawned by the plugin—require reliable cleanup when users cancel runs, sessions end, or servers shut down. According to the `openai/codex-plugin-cc` source code, the plugin handles this through a single, platform-aware termination utility that abstracts away OS differences. This approach ensures orphaned processes are eliminated whether the job exits cleanly or needs forceful termination.

## The Core Termination Helper (`terminateProcessTree`)

The entire termination logic resides in `plugins/codex/scripts/lib/process.mjs`. The **`terminateProcessTree`** function serves as the sole API for killing process trees across all platforms.

### Platform-Specific Termination Strategies

The helper detects the operating system and applies distinct strategies:

**Windows (`taskkill`)**: The function executes `taskkill /PID <pid> /T /F` via the internal `runCommandImpl` utility. If the process is already gone (detected by `looksLikeMissingProcessMessage`), it returns `delivered: false` but marks the attempt as successful. If the `taskkill` binary is missing (`ENOENT`), it falls back to Node.js `process.kill(pid)`, which sends a `SIGKILL` equivalent on Windows.

**Unix-like (Linux/macOS)**: The function sends **`SIGTERM`** to the entire process group using `kill(-pid, "SIGTERM")`. If this fails with `ESRCH` (no such process), it retries with a plain `process.kill(pid, "SIGTERM")` directed at the individual process.

### Return Value Structure

Every call returns a standardized object indicating what happened:

```javascript
{
  attempted: true,          // Boolean indicating a signal or command was issued
  delivered: <boolean>,     // Did the OS confirm the process was killed?
  method: "taskkill" | "kill" | "process-group" | "process",
  result: <raw runCommand result>   // Present only for Windows taskkill path
}

```

## Where Termination Is Triggered

The `terminateProcessTree` helper is imported and invoked across four critical locations to ensure comprehensive cleanup.

### Session Lifecycle Cleanup

In `plugins/codex/scripts/session-lifecycle-hook.mjs`, the function is called during session teardown. When a Codex session finishes, the lifecycle hook passes `job.pid` to `terminateProcessTree` to guarantee any associated companion process is destroyed.

### Server-Side Job Management

The `plugins/codex/scripts/lib/app-server.mjs` file uses the helper when the server needs to stop a running job. This includes timeout scenarios or explicit cancel requests from the client.

### Companion Process Self-Termination

In `plugins/codex/scripts/codex-companion.mjs`, the companion process itself imports the helper to terminate previously started child jobs, enabling self-cleanup when the companion needs to restart or shut down gracefully.

### Unit Test Coverage

The `tests/process.test.mjs` file validates all execution paths: the Windows `taskkill` integration, the fallback when binaries are missing, the handling of already-exited processes, and the Unix group-kill behavior.

## Implementing Process Termination

Here are practical examples of using the termination helper in different contexts:

**Canceling a job from a UI command:**

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

async function cancelJob(job) {
  // `job.pid` holds the PID of the spawned Codex process
  const outcome = terminateProcessTree(job.pid);
  console.log("Cancellation outcome:", outcome);
}

```

**Automatic cleanup when a session ends:**

```javascript
async function onSessionEnd(session) {
  if (session.job?.pid) {
    const result = terminateProcessTree(session.job.pid);
    console.log(`Session cleanup: ${result.delivered ? "killed" : "already stopped"}`);
  }
}

```

**Testing the termination logic:**

```javascript
import { test } from "node:test";
import { spawn } from "node:child_process";
import { terminateProcessTree } from "../plugins/codex/scripts/lib/process.mjs";

test("terminateProcessTree kills a dummy process on Unix", async (t) => {
  const child = spawn("sleep", ["30"]);
  const outcome = terminateProcessTree(child.pid);
  t.assert(outcome.delivered, "process should be terminated");
});

```

## Error Handling and Edge Cases

The implementation handles several failure modes specific to long-running background jobs:

- **Missing processes**: When `taskkill` reports the PID does not exist, or when `kill()` returns `ESRCH`, the function returns `attempted: true` but `delivered: false`, allowing callers to distinguish between "kill succeeded" and "process was already gone."
- **Missing binaries**: On Windows, if `taskkill` is not found in `PATH`, the code catches `ENOENT` and falls back to Node.js `process.kill()`.
- **Process groups**: The Unix implementation targets the negative PID (process group) to ensure child processes spawned by the Codex companion are also terminated, preventing orphans.

## Summary

- The **openai/codex-plugin-cc** repository centralizes process termination in `plugins/codex/scripts/lib/process.mjs` via the `terminateProcessTree` function.
- **Windows** uses `taskkill /T /F` with a fallback to `process.kill()` if the executable is missing.
- **Linux/macOS** uses `kill(-pid, "SIGTERM")` to target the entire process group, with a retry on the individual process if the group signal fails.
- The function is invoked during **session teardown**, **server timeouts**, and **companion self-termination** to ensure no background jobs persist beyond their lifecycle.
- Callers receive a structured result object indicating the **method used** and whether the signal was **delivered** or the process was already absent.

## Frequently Asked Questions

### What happens if the process already exited before termination is called?

If the process no longer exists, `terminateProcessTree` detects this via `looksLikeMissingProcessMessage` on Windows or `ESRCH` errors on Unix. The function returns `{ attempted: true, delivered: false }`, signaling that no signal was delivered because the target was already gone, but the operation is considered successfully handled.

### How does Windows termination differ from Linux/macOS implementation?

On Windows, the helper executes the external `taskkill` command with the `/T` flag to kill the entire process tree and `/F` for forceful termination. If `taskkill` is unavailable, it falls back to Node.js `process.kill()`. On Linux and macOS, it uses POSIX `kill()` with a negative PID to signal the entire process group, falling back to a single-process signal if the group no longer exists.

### What information does `terminateProcessTree` return?

The function always returns an object containing: `attempted` (boolean indicating an attempt was made), `delivered` (boolean indicating OS confirmation of termination), `method` (string identifying the mechanism used: `"taskkill"`, `"kill"`, `"process-group"`, or `"process"`), and optionally `result` containing the raw output from the command execution on Windows.

### Where is the termination logic defined and tested?

The core logic lives in `plugins/codex/scripts/lib/process.mjs`. Unit tests covering Windows paths, fallback scenarios, and Unix group-kill behavior are located in `tests/process.test.mjs`. Integration usage appears in `session-lifecycle-hook.mjs`, `app-server.mjs`, and `codex-companion.mjs`.