Codex Plugin Job State File Structure: How On-Disk State Is Organized
Codex Companion plugin stores workspace state in a three-tier filesystem layout: a global 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 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:
// 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:
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:
{
"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):
- Load existing state via
loadState(). - Prune job list to
MAX_JOBS(default 50). - Write updated
state.json. - Delete orphaned
.jsonand.logfiles 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
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
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.jsonfor global index,<job-id>.jsonfor payloads,<job-id>.logfor 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) 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.
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 →