Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix: fall back to tmpdir state when CLAUDE_PLUGIN_DATA state is empty
Setup commands run via Bash (no CLAUDE_PLUGIN_DATA) write state to tmpdir.
Stop hooks run via Claude Code (CLAUDE_PLUGIN_DATA set) read from the
persistent plugin data dir, missing the setup state entirely.

resolveStateDir() now checks for existing state at the tmpdir fallback
when CLAUDE_PLUGIN_DATA is set but the primary location has no state.json.
Also fixes the pre-existing test failure by properly isolating env vars.

Fixes #59
  • Loading branch information
tmchow committed Apr 3, 2026
commit 22d42f43ed1b7050d9cc5363c300b5fc0b3d6ee2
14 changes: 12 additions & 2 deletions plugins/codex/scripts/lib/state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,19 @@ export function resolveStateDir(cwd) {
const slugSource = path.basename(workspaceRoot) || "workspace";
const slug = slugSource.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0, 16);
const dirName = `${slug}-${hash}`;
const pluginDataDir = process.env[PLUGIN_DATA_ENV];
const stateRoot = pluginDataDir ? path.join(pluginDataDir, "state") : FALLBACK_STATE_ROOT_DIR;
return path.join(stateRoot, `${slug}-${hash}`);

if (pluginDataDir) {
const primaryDir = path.join(pluginDataDir, "state", dirName);
const fallbackDir = path.join(FALLBACK_STATE_ROOT_DIR, dirName);
if (!fs.existsSync(path.join(primaryDir, STATE_FILE_NAME)) && fs.existsSync(path.join(fallbackDir, STATE_FILE_NAME))) {
return fallbackDir;
Comment thread
tmchow marked this conversation as resolved.
Outdated
}
return primaryDir;
}

return path.join(FALLBACK_STATE_ROOT_DIR, dirName);
}

export function resolveStateFile(cwd) {
Expand Down
44 changes: 40 additions & 4 deletions tests/state.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,22 @@ import { resolveJobFile, resolveJobLogFile, resolveStateDir, resolveStateFile, s

test("resolveStateDir uses a temp-backed per-workspace directory", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
const previousPluginDataDir = process.env.CLAUDE_PLUGIN_DATA;
delete process.env.CLAUDE_PLUGIN_DATA;

try {
const stateDir = resolveStateDir(workspace);

assert.equal(stateDir.startsWith(os.tmpdir()), true);
assert.match(path.basename(stateDir), /.+-[a-f0-9]{16}$/);
assert.match(stateDir, new RegExp(`^${os.tmpdir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
assert.equal(stateDir.startsWith(os.tmpdir()), true);
assert.match(path.basename(stateDir), /.+-[a-f0-9]{16}$/);
assert.match(stateDir, new RegExp(`^${os.tmpdir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
} finally {
if (previousPluginDataDir == null) {
delete process.env.CLAUDE_PLUGIN_DATA;
} else {
process.env.CLAUDE_PLUGIN_DATA = previousPluginDataDir;
}
}
});

test("resolveStateDir uses CLAUDE_PLUGIN_DATA when it is provided", () => {
Expand All @@ -40,6 +51,31 @@ test("resolveStateDir uses CLAUDE_PLUGIN_DATA when it is provided", () => {
}
});

test("resolveStateDir falls back to tmpdir state when plugin state is missing", () => {
const workspace = makeTempDir();
const pluginDataDir = makeTempDir();
const previousPluginDataDir = process.env.CLAUDE_PLUGIN_DATA;
delete process.env.CLAUDE_PLUGIN_DATA;

const fallbackStateDir = resolveStateDir(workspace);
const fallbackStateFile = resolveStateFile(workspace);
fs.mkdirSync(fallbackStateDir, { recursive: true });
fs.writeFileSync(fallbackStateFile, `${JSON.stringify({ version: 1, config: { stopReviewGate: true }, jobs: [] })}\n`, "utf8");

process.env.CLAUDE_PLUGIN_DATA = pluginDataDir;

try {
const stateDir = resolveStateDir(workspace);
assert.equal(stateDir, fallbackStateDir);
} finally {
if (previousPluginDataDir == null) {
delete process.env.CLAUDE_PLUGIN_DATA;
} else {
process.env.CLAUDE_PLUGIN_DATA = previousPluginDataDir;
}
}
});

test("saveState prunes dropped job artifacts when indexed jobs exceed the cap", () => {
const workspace = makeTempDir();
const stateFile = resolveStateFile(workspace);
Expand Down