How the Codex Plugin Persists Job State Across Claude Code Restarts
The Codex plugin uses a deterministic, file-based state store located in plugins/codex/scripts/lib/state.mjs to write atomic JSON records and append-only log files, enabling full recovery of job status, phase, and output history after Claude Code restarts.
The openai/codex-plugin-cc repository implements a robust persistence layer that survives process termination, container restarts, and user-initiated session resets. By maintaining durable JSON records and log files outside the Claude Code process, the plugin ensures that long-running jobs can be tracked and resumed without data loss.
How State Persistence Works
The persistence mechanism relies on two core modules: state.mjs for low-level file operations and tracked-jobs.mjs for higher-level job lifecycle management. Together, they create a deterministic directory structure, write atomic updates, and prune stale records to prevent unbounded growth.
The State Storage Architecture
Resolving the State Directory
The first step in persistence is establishing a stable storage location. The resolveStateDir() function in state.mjs (lines 29-44) builds a unique directory name derived from a hash of the workspace path. This directory is then stored under the environment variable CLAUDE_PLUGIN_DATA, falling back to a temporary location in /tmp if unset.
This hashing strategy guarantees that the same workspace always maps to the same directory, even across different Claude Code sessions or system reboots.
Loading and Initializing State
When the plugin initializes, loadState() (lines 58-77) reads the state.json file from the resolved directory. If the file is missing or corrupted, the function gracefully falls back to a default skeleton state, ensuring the plugin never crashes on malformed data.
// Located in plugins/codex/scripts/lib/state.mjs
const state = loadState(workspaceRoot);
// Returns the current state or a default structure if state.json is absent
Writing and Updating Job Records
Atomic Job File Creation
Each job receives its own dedicated JSON file (<jobId>.json) inside a jobs/ subdirectory. The writeJobFile() function (lines 66-71) serializes the job object and writes it atomically to prevent corruption during unexpected shutdowns. The file path is derived from resolveJobFile(), ensuring consistent naming conventions.
// Creating a persistent job record
import { generateJobId, upsertJob, writeJobFile } from "./state.mjs";
const jobId = generateJobId();
const job = {
id: jobId,
workspaceRoot: process.cwd(),
title: "Run unit tests",
status: "pending",
};
await upsertJob(process.cwd(), job); // Updates global state.json
await writeJobFile(process.cwd(), jobId, job); // Creates jobs/<jobId>.json
Global State Updates and Pruning
The upsertJob() function calls updateState() (lines 29-33 and 92-104) to synchronize the global state. This operation:
- Loads the full state from disk
- Merges the supplied patch (e.g., new
phase,status, orpid) - Prunes the job list to the most recent 50 entries
- Writes the updated
state.json - Cleans up stale job files to prevent disk bloat
This pruning mechanism ensures that the Codex plugin maintains constant memory and disk usage regardless of how many jobs are run over time.
Logging and Session Correlation
Beyond JSON state files, the plugin maintains human-readable audit trails. The createJobLogFile() function in tracked-jobs.mjs (lines 51-58) creates a plain-text <jobId>.log file in the same jobs/ directory. Subsequent calls to appendLogLine() and appendLogBlock() append output to this file, providing a replayable history that survives restarts.
Additionally, the plugin supports session correlation through the optional sessionId (read from CODEX_COMPANION_SESSION_ID). This value is stored with each job record (lines 60-68 in tracked-jobs.mjs), allowing the plugin to filter jobs belonging to the current Claude Code session when enumerating active or historical work.
Recovering Jobs After Restart
When a new Claude Code session starts, the plugin calls listJobs() (lines 49-52), which returns loadState().jobs. Because the state directory lives outside the process—in the OS temp folder or the user-provided CLAUDE_PLUGIN_DATA path—the data persists and can be reconstructed into live job objects.
// Retrieving all jobs after a restart
import { listJobs } from "./state.mjs";
const jobs = listJobs(process.cwd());
console.log("Recovered jobs:", jobs);
// Restores job objects with their original status, phase, and metadata
Code Examples
The following snippets demonstrate the full persistence lifecycle: creation, live updates, and recovery.
1. Create a new job record with associated log file:
import { generateJobId, upsertJob, writeJobFile, createJobLogFile } from "./state.mjs";
const cwd = process.cwd();
const jobId = generateJobId();
const job = {
id: jobId,
workspaceRoot: cwd,
title: "Run unit tests",
status: "pending",
};
upsertJob(cwd, job);
writeJobFile(cwd, jobId, job);
const logFile = createJobLogFile(cwd, jobId, job.title);
2. Update progress while the job runs:
import { createJobProgressUpdater } from "./tracked-jobs.mjs";
const progress = createJobProgressUpdater(cwd, jobId);
progress({ phase: "install", message: "Installing dependencies…" });
progress({ phase: "test", message: "Running tests…" });
// Automatically persists phase changes to state.json
3. Retrieve all jobs after a restart:
import { listJobs } from "./state.mjs";
const jobs = listJobs(cwd);
console.log("Recovered jobs:", jobs);
Summary
- Deterministic storage: The
resolveStateDir()function instate.mjscreates a hashed directory path based on the workspace root, ensuring consistent storage locations across Claude Code restarts. - Atomic writes: Job data is written to individual JSON files using
writeJobFile(), while global state updates occur throughupdateState()with automatic pruning to 50 entries. - Graceful degradation:
loadState()falls back to default skeleton state ifstate.jsonis missing or corrupted, preventing crashes on bad files. - Audit trails:
createJobLogFile()and append operations intracked-jobs.mjsprovide durable, human-readable logs that survive process termination. - Session awareness: The optional
sessionIdfromCODEX_COMPANION_SESSION_IDallows filtering jobs by Claude Code session.
Frequently Asked Questions
Where does the Codex plugin store job state files?
The plugin stores job state in a directory resolved by resolveStateDir() in plugins/codex/scripts/lib/state.mjs. This directory defaults to the path specified in the CLAUDE_PLUGIN_DATA environment variable, or falls back to a hashed subdirectory under /tmp. Inside this directory, you will find state.json (global state) and a jobs/ subdirectory containing individual <jobId>.json files and <jobId>.log files.
How does the plugin prevent data loss if Claude Code crashes?
The implementation uses atomic file writes and defensive error handling. Specifically, writeJobFile() writes job data atomically to prevent partial writes, while loadState() catches corrupted or missing files and returns a default state structure rather than throwing exceptions. This ensures that even if Claude Code terminates unexpectedly, the existing state files remain intact and readable on the next startup.
What is the maximum number of jobs the Codex plugin remembers?
The plugin maintains a sliding window of the 50 most recent jobs. The updateState() function (lines 92-104 in state.mjs) automatically prunes older entries when writing updates to state.json, and synchronously cleans up the associated stale job files from the jobs/ directory. This prevents unbounded disk usage while preserving recent work history.
How does the plugin distinguish between different Claude Code sessions?
The plugin reads the optional CODEX_COMPANION_SESSION_ID environment variable and stores it as a sessionId field in each job record (as implemented in tracked-jobs.mjs, lines 60-68). This identifier allows the plugin to filter and correlate jobs with specific Claude Code sessions, enabling session-specific job listings while still maintaining a complete historical record across all sessions.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →