openai / openai/codex-plugin-cc

Plugin state dir has no plugin-identity segment: sibling plugins share one jobs array, and pruneJobs deletes the other plugin's records

Open Beginner friendly
#609 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

Summary

scripts/lib/state.mjsresolveStateDir() returns:

path.join(stateRoot, `${slug}-${hash}`)   // stateRoot = $CLAUDE_PLUGIN_DATA/state

There is no component identifying which plugin owns that directory. Since CLAUDE_PLUGIN_DATA is exported into a shared, append-only session env file by every plugin's SessionStart hook (scripts/session-lifecycle-hook.mjs), last-writer-wins means this plugin frequently resolves to a sibling plugin's data directory. The two plugins then share one state.json and one jobs/ directory.

Observed with this plugin and xai-org/grok-build-plugin-cc installed side by side. Both compute the state path identically, so either can capture the other.

Why this is data loss, not untidiness

pruneJobs() trims the shared jobs array to MAX_JOBS (50) and deletes the job files it trims — including the other plugin's records and logs. Each plugin is silently garbage-collecting the other's run history.

Measured on one machine:

  • 19 of 50 surviving job records in grok-build's state/<repo>/jobs/ directory were produced by this plugin. Vendor was determined from record content (kindLabel, log banner), not from the path — no vendor/cli field exists on the records.
  • This plugin had no state directory for that repo at all. Every job it ran there since 2026-07-24 had landed in the sibling's directory.
  • The shared directory sat exactly at the 50-job cap with a hard history floor, i.e. records had already been deleted. Job ids are millisecond timestamps rather than counters, so removals leave no gap to detect — the deletion is silent and uncountable after the fact.

Second-order consequence: an artifact's location is part of its provenance. When plugin A's records can land in plugin B's directory, "which plugin produced this run?" is unanswerable from the path, and any audit keyed on directory ownership is unsound. We had to retract a published measurement for exactly this reason.

Reproduction

With both plugins installed, point the env var at the other plugin's data dir and resolve from each:

CLAUDE_PLUGIN_DATA=~/.claude/plugins/data/grok-build-xai-grok-build \
  node -e 'import("'"$HOME"'/.claude/plugins/cache/openai-codex/codex/1.0.6/scripts/lib/state.mjs")
    .then(m => console.log(m.resolveStateFile(process.env.HOME + "/some/repo")))'

Run the same for the grok-build plugin's state.mjs. Both print the identical path.

Suggested fix

A plugin-identity segment in the state path. This holds even when the env var resolves to the wrong plugin, so it does not depend on fixing env-var propagation:

 const PLUGIN_DATA_ENV = "CLAUDE_PLUGIN_DATA";
+const PLUGIN_ID = "codex";
 const FALLBACK_STATE_ROOT_DIR = path.join(os.tmpdir(), "codex-companion");
@@
-  return path.join(stateRoot, `${slug}-${hash}`);
+  return path.join(stateRoot, PLUGIN_ID, `${slug}-${hash}`);

After this, the two plugins land in …/state/codex/<slug>-<hash> and …/state/grok-build/<slug>-<hash> respectively — no shared jobs array, no cross-deletion, and vendor is readable from the path again. Existing job history under the old path is orphaned rather than migrated, which seemed the safer default; a one-time migration could be added if you prefer.

Alternatively, resolving the data dir from the plugin's own location (import.meta.url) instead of the shared env var fixes the root cause rather than the symptom, but is a larger change.

We have applied the one-line version locally and verified it: under the collision condition the paths now diverge, and running the same probe against the unpatched files reproduces the identical-path collision. Happy to open a PR if that is useful.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in scripts/lib/state.mjs at resolveStateDir() and inspect how resolveStateFile() uses the returned directory. Run the provided Node reproduction with two plugin data directories and confirm the paths currently collide. Done means each plugin resolves to a distinct identity-qualified state path and the collision probe prints different paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
tooling
Issue type
Bug
Difficulty
1/5
Estimated time
Under an hour
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.