# How the Codex Plugin Captures and Returns File Modifications and Command Execution Results

> Discover how the Codex plugin's notification-driven capture loop tracks file modifications and command results, returning structured state to callers. Learn more about openai/codex-plugin-cc.

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

---

The Codex plugin uses a **notification-driven capture loop** that listens to Codex "item/completed" notifications, filters them by turn, and accumulates file changes and command execution results into a structured state object returned to the caller.

This article explains the complete mechanism in the `openai/codex-plugin-cc` repository, from state initialization to the final returned payload. Understanding this flow helps developers build reliable integrations that track exactly what changes Codex makes during an automated coding session.

---

## Turn-Capture State Initialization

Every Codex turn begins by creating a dedicated state container. The function `createTurnCaptureState` in `plugins/codex/scripts/lib/codex.mjs` initializes an object with two arrays that will hold captured events:

```javascript
// Simplified structure from codex.mjs lines 31-38
{
  fileChanges: [],        // Accumulates fileChange notifications
  commandExecutions: [],  // Accumulates commandExecution notifications
  completion: Promise     // Resolves when the turn ends
}

```

This state persists for the entire duration of one turn or review operation. All subsequent notifications are filtered and stored here.

---

## Installing the Notification Handler

Before sending a request to Codex, the `captureTurn` function installs a temporary notification handler using `client.setNotificationHandler`. This handler intercepts every notification sent by the Codex app-server.

The handler assignment appears at `plugins/codex/scripts/lib/codex.mjs` lines 61-68:

```javascript
client.setNotificationHandler((notification) => {
  // Only process notifications belonging to this turn
  if (!belongsToTurn(notification, turnId)) return;
  
  recordItem(notification, state);
});

```

The handler remains active until `completeTurn` is called, ensuring no events are missed during the turn's execution.

---

## Filtering Notifications by Turn

Not all notifications belong to the current turn. The `belongsToTurn` function (lines 96-104) performs a strict membership check by comparing the notification's turn identifier against the active `turnId`:

```javascript
function belongsToTurn(notification, turnId) {
  return notification.params?.turnId === turnId;
}

```

This prevents cross-turn contamination when multiple turns execute concurrently or in rapid succession.

---

## Recording File Changes and Command Executions

The `recordItem` function (lines 80-88) processes qualifying notifications and persists them to the state object. It discriminates by notification type and lifecycle status:

| Notification Type | Lifecycle Requirement | Storage Target |
|-------------------|----------------------|--------------|
| `fileChange` | `"completed"` | `state.fileChanges` |
| `commandExecution` | `"completed"` | `state.commandExecutions` |

Only **completed** lifecycle events are captured. Intermediate states ("started", "in-progress") are ignored to ensure the final result reflects the definitive outcome.

```javascript
function recordItem(notification, state) {
  const { type, lifecycle, item } = notification.params;
  
  if (lifecycle !== "completed") return;
  
  if (type === "fileChange") {
    state.fileChanges.push(item);
  } else if (type === "commandExecution") {
    state.commandExecutions.push(item);
  }
}

```

Each `fileChange` item includes the patch list showing actual modifications. Each `commandExecution` item contains the command string, exit code, and captured stdout/stderr streams.

---

## Completing the Capture Loop

When Codex signals turn completion, `completeTurn` (lines 46-55) resolves the `state.completion` promise. This triggers the return of the fully populated `TurnCaptureState` containing all accumulated events.

The capture loop is synchronous with the turn lifecycle: it starts before the request, runs continuously during Codex execution, and terminates cleanly on completion.

---

## High-Level API: Returning Results to Callers

Two primary functions expose capture results to consuming code: `runAppServerTurn` for standard turns and `runAppServerReview` for review operations. Both invoke `captureTurn` internally and return a structured payload.

From `plugins/codex/scripts/lib/codex.mjs` lines 55-58, the return object includes:

```javascript
{
  fileChanges: state.fileChanges,           // Raw fileChange notifications
  touchedFiles: collectTouchedFiles(state), // Derived file paths list
  commandExecutions: state.commandExecutions // Raw commandExecution notifications
}

```

The `touchedFiles` array is computed via `collectTouchedFiles`, which extracts unique file paths from the change notifications for convenient access.

---

## Practical Usage Example

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

async function automateRefactor() {
  const result = await runAppServerTurn(
    process.cwd(),
    {
      prompt: "Refactor utils.js to use async/await and run npm test",
      model: "gpt-4o",
      onProgress: (msg) => console.log("Progress:", msg)
    }
  );

  // Inspect captured modifications
  console.log("Files modified:", result.touchedFiles);
  console.log("Detailed changes:", result.fileChanges);
  console.log("Test command results:", result.commandExecutions);
}

automateRefactor();

```

The `result` object provides complete observability: every file patch, every executed command with its exit status, and a clean list of affected paths.

---

## Summary

- **State creation**: `createTurnCaptureState` initializes per-turn storage for `fileChanges` and `commandExecutions` in `plugins/codex/scripts/lib/codex.mjs`
- **Notification handling**: `client.setNotificationHandler` installs a temporary handler that filters by turn ID via `belongsToTurn`
- **Selective recording**: `recordItem` captures only `completed` lifecycle notifications of type `fileChange` or `commandExecution`
- **Turn finalization**: `completeTurn` resolves the completion promise, returning the populated state
- **Public API**: `runAppServerTurn` and `runAppServerReview` return structured results including processed `touchedFiles` and raw notification arrays

---

## Frequently Asked Questions

### How does the plugin avoid capturing notifications from other turns?

The `belongsToTurn` function compares `notification.params.turnId` against the active turn identifier. Notifications with mismatched IDs are silently ignored, ensuring isolation between concurrent or sequential turn operations.

### Why does the plugin only capture "completed" lifecycle notifications?

Intermediate states like "started" or "in-progress" represent partial or speculative data. By filtering for `"completed"` lifecycle status, the plugin guarantees that captured items reflect final, committed results suitable for downstream processing.

### What information is included in a captured fileChange item?

Each `fileChange` item contains the complete patch list showing line-by-line modifications, file paths, and metadata from the Codex edit operation. The `touchedFiles` helper extracts just the unique file paths for convenience.

### Can I monitor progress while a turn is still executing?

Yes. Both `runAppServerTurn` and `runAppServerReview` accept an `onProgress` callback that receives real-time status updates. This operates independently of the capture loop, allowing UI updates while the final results accumulate in the state object.