# Codex Plugin Job State File Structure: How On-Disk State Is Organized

> Explore the Codex Plugin job state file structure. Understand how workspace state is organized on disk with a three-tier filesystem layout including state json and log files.

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

---

**Codex Companion plugin stores workspace state in a three-tier filesystem layout: a global [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) containing job metadata summaries, individual JSON files per job for full payloads, and optional `.log` files for execution output.**

The `openai/codex-plugin-cc` repository persists runtime state to disk so that job history survives process restarts. Understanding this structure helps you debug, migrate, or integrate with the plugin's data layer. This article breaks down exactly how job state files are organized, where they live, and how to access them programmatically.

## State Directory Layout

The plugin partitions on-disk state across three file types under a workspace-specific root:

| File type | Relative path | Purpose |
|-----------|---------------|---------|
| **Global state** | `state/<slug-hash>/state.json` | Workspace-level metadata, config, and job index |
| **Job payload** | `state/<slug-hash>/jobs/<job-id>.json` | Complete job record with arguments, results, timestamps |
| **Job logs** | `state/<slug-hash>/jobs/<job-id>.log` | Streaming stdout/stderr capture (created on demand) |

This separation keeps [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) lightweight for frequent reads while isolating bulky job data and logs.

## Resolving State Paths

Path computation lives in **`state.mjs`**. The core helper is `resolveStateDir()` which builds the root directory:

```javascript
// From state.mjs lines 29-44
export function resolveStateDir(cwd) {
  const workspaceRoot = getWorkspaceRoot(cwd);
  const slug = slugifyPath(workspaceRoot);
  const hash = createHash("sha256")
    .update(workspaceRoot)
    .digest("hex")
    .slice(0, 16);
  const base = process.env.CLAUDE_PLUGIN_DATA || "/tmp/codex-companion";
  return path.join(base, `${slug}-${hash}`);
}

```

The function generates a deterministic 16-character hash from the workspace's canonical path. You can override the base directory via the `CLAUDE_PLUGIN_DATA` environment variable; otherwise it falls back to `/tmp/codex-companion` (see lines 10-13).

### Location Helpers

Four exported functions resolve specific file paths:

```javascript
resolveStateFile(cwd)      // → <state-dir>/state.json
resolveJobsDir(cwd)        // → <state-dir>/jobs
resolveJobFile(cwd, id)    // → <state-dir>/jobs/<id>.json
resolveJobLogFile(cwd, id) // → <state-dir>/jobs/<id>.log

```

Source references: `resolveStateFile` at lines 46-48, `resolveJobsDir` at lines 50-52, `resolveJobLogFile` at lines 83-86, and `resolveJobFile` at lines 88-90.

## state.json Structure

The global state file is a JSON object with three mandatory fields:

```json
{
  "version": 1,
  "config": { /* workspace configuration */ },
  "jobs": [
    { "id": "review-abc123", "status": "completed", "createdAt": "...", "updatedAt": "..." },
    { "id": "explain-def456", "status": "running", "createdAt": "...", "updatedAt": "..." }
  ]
}

```

The `jobs` array is a **lightweight index**—it contains enough metadata to list and filter jobs without opening individual files. Full job payloads live in separate JSON files, not inline here.

## Job State File Lifecycle

When `upsertJob()` saves new state, the plugin executes this sequence (lines 105-112):

1. Load existing state via `loadState()`.
2. Prune job list to `MAX_JOBS` (default 50).
3. Write updated [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json).
4. Delete orphaned `.json` and `.log` files for jobs no longer in the index.

This garbage collection prevents unbounded disk growth. Log files are removed automatically when their parent job is pruned.

## Working with Job State Files

### Reading a Job's Full Payload

```javascript
import { resolveJobFile, loadState } from "./state.mjs";
import fs from "fs";

// List jobs from the index
const { jobs } = await loadState(process.cwd());

// Access complete job data
const jobId = jobs[0].id;
const jobPath = resolveJobFile(process.cwd(), jobId);
const fullJob = JSON.parse(fs.readFileSync(jobPath, "utf8"));

console.log(fullJob.prompt);     // job-specific data
console.log(fullJob.result);     // execution results

```

### Streaming Job Logs

```javascript
import { resolveJobLogFile } from "./state.mjs";
import { createReadStream } from "fs";

const logPath = resolveJobLogFile(process.cwd(), jobId);
const stream = createReadStream(logPath, { encoding: "utf8" });

stream.on("data", chunk => process.stdout.write(chunk));

```

Log files are plain text with newline-delimited entries. They are created on first write and may not exist for jobs that produce no output.

## Key Implementation Files

| File | Role |
|------|------|
| `scripts/lib/state.mjs` | Core path resolution, `loadState()`, `saveState()`, `upsertJob()`, pruning logic |
| `scripts/lib/workspace.mjs` | `getWorkspaceRoot()` for canonical path computation |
| `tests/state.test.mjs` | Validates directory layout and file persistence |
| `tests/runtime.test.mjs` | End-to-end job lifecycle verification |

## Summary

- **Three file types**: [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) for global index, `<job-id>.json` for payloads, `<job-id>.log` for output.
- **Deterministic paths**: SHA-256 hash of workspace path plus slug, overridable via `CLAUDE_PLUGIN_DATA`.
- **API surface**: `resolveStateDir()`, `resolveJobFile()`, `resolveJobLogFile()` expose all locations.
- **Automatic cleanup**: Jobs beyond `MAX_JOBS` (50) prune their associated files on next save.

## Frequently Asked Questions

### What environment variable controls the state directory?

Set `CLAUDE_PLUGIN_DATA` to use a custom base path. Without it, the plugin defaults to `/tmp/codex-companion`.

### How many jobs are retained by default?

The plugin keeps **50 jobs** maximum. Older jobs and their logs are deleted automatically when the threshold is exceeded. This limit is defined as `MAX_JOBS` in `state.mjs`.

### Why split job data across multiple files instead of one large JSON?

Separating the global index ([`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json)) from full payloads reduces I/O for common operations like listing job history. The index stays small and fast to parse, while individual job files load only when needed.

### Can job log files be read while a job is running?

Yes. Log files are opened in append mode and written incrementally. External processes can tail or stream them via `resolveJobLogFile()` without interfering with active writes.