How the Codex Plugin Handles Job State Persistence Across Restarts

The Codex plugin stores background job metadata in a JSON file inside a repository-specific state directory, enabling full recovery of job status, configuration, and output files after editor or system restarts.

The openai/codex-plugin-cc repository provides a Claude Code plugin that manages long-running Codex tasks in the background. To ensure job state persistence across restarts, the plugin implements a durable storage layer in plugins/codex/scripts/lib/state.mjs that serializes job records, configuration flags, and runtime logs to disk using a deterministic, repository-scoped directory structure.

State Directory Resolution and Isolation

The plugin derives a unique storage location for every workspace to prevent collisions between different repositories.

Environment Variable and Fallback Paths

The root state directory is determined by the CLAUDE_PLUGIN_DATA environment variable (referenced internally as PLUGIN_DATA_ENV). If this variable is unset, the plugin falls back to the system temporary directory via os.tmpdir(), appending codex-companion/state to the path.

Repository-Specific Hashing

To isolate state between different repositories, the plugin resolves the workspace's canonical path using fs.realpathSync.native and computes a short SHA-256 hash of that path. This produces unique directories such as:

/tmp/codex-companion/state/my-repo-7f3a9b2c8e1d2f3a

The functions resolveStateDir and resolveStateFile handle this path construction, ensuring that multiple copies of the same repository opened in different locations maintain separate state stores.

State File Structure and Versioning

The central state.json file (defined by the constant STATE_FILE_NAME) contains a versioned JSON object with the following structure:

{
  "version": 1,
  "config": { "stopReviewGate": false },
  "jobs": [ … ]
}

The STATE_VERSION constant is set to 1, enabling future schema migrations. Each entry in the jobs array is a job record containing fields such as id, createdAt, updatedAt, status, model, and logFile.

Saving, Pruning, and Loading Job State Across Restarts

The Upsert and Save Cycle

When a job is created or updated, the upsertJob function modifies the in-memory state and triggers saveState, which synchronously rewrites the entire state.json file to disk using fs.writeFileSync.

import { upsertJob, resolveJobLogFile } from "./lib/state.mjs";

const jobId = `review-${Date.now()}`;
await upsertJob(process.cwd(), {
  id: jobId,
  type: "review",
  status: "running",
  model: "gpt-4",
  logFile: resolveJobLogFile(process.cwd(), jobId)
});

Automatic Pruning of Old Jobs

Before writing, the pruneJobs function truncates the job list to the most recent MAX_JOBS (default 50). Older jobs are removed from the JSON array, and their associated files are deleted via removeJobFile and removeFileIfExists, keeping the on-disk footprint bounded.

Startup Recovery with loadState

On every command invocation, loadState reads state.json from the resolved state directory. If the file does not exist, it returns a defaultState object, ensuring that missing fields (such as newly added configuration flags) are populated with safe defaults.

import { listJobs } from "./lib/state.mjs";

// After a restart, list the recent jobs
const recentJobs = listJobs(process.cwd());
console.log(recentJobs.map(j => `${j.id}: ${j.status}`));

Job-Specific File Management

Beyond the central state file, each job receives two dedicated files in the jobs/ subdirectory, resolved by resolveJobsDir:

  • JSON payload file (<jobId>.json): Stores the raw Codex response, accessible via resolveJobFile.
  • Log file (<jobId>.log): Captures streaming output during execution, accessible via resolveJobLogFile.

These files survive process termination, allowing commands like /codex:status and /codex:result to retrieve historical data after a restart.

import { readJobFile, resolveJobFile } from "./lib/state.mjs";

// Retrieve the stored result of a finished job
const payload = readJobFile(resolveJobFile(process.cwd(), "job-abcdef-123"));
console.log(payload.output);

Configuration Persistence

User settings such as the review-gate flag are stored inside state.json under the config key. The setConfig and getConfig functions read and write these values, ensuring that configuration changes persist across editor sessions and system reboots.

Cross-Process Safety Considerations

All directory creation uses { recursive: true } to ensure path existence. File writes are performed synchronously (fs.writeFileSync) to prevent interleaved writes during sequential operations within the same CLI process. While the plugin is not designed for concurrent writes from multiple processes, this synchronous approach guarantees atomicity for single-process operations.

Summary

  • The plugin stores state in a directory determined by CLAUDE_PLUGIN_DATA or a fallback temporary path, hashed by repository location to ensure isolation.
  • Job metadata lives in state.json with schema versioning (STATE_VERSION = 1), while individual job outputs reside in separate .json and .log files.
  • The pruneJobs mechanism limits storage to 50 recent jobs (MAX_JOBS), deleting older artifacts automatically.
  • loadState and defaultState ensure graceful recovery and forward compatibility when loading persisted data.
  • Configuration settings are co-located with job data in state.json, surviving restarts via the same persistence layer.

Frequently Asked Questions

Where does the Codex plugin store job state data?

The plugin stores data in a subdirectory under CLAUDE_PLUGIN_DATA (or os.tmpdir()/codex-companion/state if unset). The exact directory name incorporates a SHA-256 hash of the repository's canonical path to ensure isolation between different workspace locations.

How many jobs are retained after a restart?

By default, the plugin retains the 50 most recent jobs (MAX_JOBS). Older jobs are pruned from state.json and their associated files are deleted during the save operation to prevent unbounded disk growth.

Can job state survive a system reboot?

Yes. Because all state is written to disk via synchronous file operations and stored in the file system (either in the configured data directory or the temp folder), job records, logs, and configuration settings persist through system reboots, editor restarts, and process crashes.

How does the plugin handle concurrent writes to state files?

The plugin uses synchronous write operations (fs.writeFileSync) to prevent corruption during sequential operations within a single process. However, it is not designed for concurrent access from multiple processes; simultaneous writes from different CLI instances could result in race conditions.

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 →