openai / openai/codex-plugin-cc
`/codex:setup --enable-review-gate` reports success but writes the flag to a state root the Stop hook never reads — the gate silently fails open
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
/codex:setup --enable-review-gate reports success and returns "reviewGateEnabled": true,
but the flag can be written to a state root that the Stop hook never reads. The gate is
then silently inert: no error, no warning, and setup --json keeps affirming it is on.
The split-root mechanism itself is already described in #636 (broker/SessionEnd). This
issue is a different, user-facing consequence of it: an opt-in review gate that fails
open while reporting that it is armed. #676 covers the gate failing open on malformed
hook stdin; this is the same failure class reached through configuration instead.
Mechanism
scripts/lib/state.mjs:29-43 — the state root depends on CLAUDE_PLUGIN_DATA, with a
$TMPDIR fallback. The slug and hash are identical either way; only the root differs.
The two sides of the gate resolve that root in different environments:
| Side | Runs as | CLAUDE_PLUGIN_DATA |
State root used |
|---|---|---|---|
/codex:setup --enable-review-gate |
Bash tool | often unset | $TMPDIR/codex-companion/<slug>-<hash> |
stop-review-gate-hook.mjs:154 |
hook, spawned by the harness | set | $CLAUDE_PLUGIN_DATA/state/<slug>-<hash> |
The plugin is aware of this and tries to bridge it — session-lifecycle-hook.mjs:80:
function handleSessionStart(input) {
appendEnvVar(SESSION_ID_ENV, input.session_id);
appendEnvVar(TRANSCRIPT_PATH_ENV, input.transcript_path);
appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]); // <- propagates to the Bash tool
}
But appendEnvVar is a no-op whenever process.env.CLAUDE_ENV_FILE is unset, and the
whole propagation never happens at all if the SessionStart hook did not run. Two
reachable paths to that:
- Plugin installed mid-session (
/plugin install+/reload-plugins). The plugin's
SessionStarthook never ran for that session, so nothing was ever appended to
CLAUDE_ENV_FILE. This is exactly the flow a first-time user follows — install the
plugin, then immediately run/codex:setup, which is what the setup command itself
instructs. Every Bash invocation for the rest of that session resolves to$TMPDIR. SessionStartkilled by its own 5s timeout (#670) — same outcome in an ordinary
session, not just a mid-session install.
hooks/hooks.json gives Stop a 900s timeout but SessionStart only 5s, so path 2 is
not hypothetical.
Repro
Fresh session, plugin installed mid-session, on macOS, plugin 1.0.6, codex-cli 0.149.1:
$ env | grep -E 'CLAUDE_PLUGIN_DATA|CLAUDE_ENV_FILE|CODEX_COMPANION'
(nothing)
$ node .../scripts/codex-companion.mjs setup --json --enable-review-gate
"reviewGateEnabled": true,
"actionsTaken": ["Enabled the stop-time review gate for /Users/me/Projects/alfred-front."],
"nextSteps": []
Where the flag actually landed:
$ cat "$TMPDIR/codex-companion/alfred-front-790ce51531842df0/state.json"
{ "version": 1, "config": { "stopReviewGate": true }, "jobs": [] }
$ ls ~/.claude/plugins/data/codex-openai-codex/state/
(empty — the root the Stop hook reads has no state at all)
Re-running the same command with the variable set writes a second, independent copy:
$ CLAUDE_PLUGIN_DATA=~/.claude/plugins/data/codex-openai-codex \
node .../scripts/codex-companion.mjs setup --json --enable-review-gate
"actionsTaken": ["Enabled the stop-time review gate for /Users/me/Projects/alfred-front."]
$ cat ~/.claude/plugins/data/codex-openai-codex/state/alfred-front-790ce51531842df0/state.json
{ "version": 1, "config": { "stopReviewGate": true }, "jobs": [] }
Same repo, same slug, same hash, two state files, one of them ignored by the hook.
Impact
- The user explicitly opts into a review gate; the gate never fires. Failing open on a
quality gate is worse than failing closed, and here there is no signal at all. setup --jsonis affirmatively misleading:reviewGateEnabled: truereflects the state
file the command just wrote, not the one the hook will read.- The
$TMPDIRfallback is purged by macOS, so even a session where the propagation works
can lose the flag later without the user touching anything. --disable-review-gatehas the mirror-image problem: it can clear a flag in one root
while an armed one persists in the other.
Suggested fixes
Roughly in order of cost:
- Report the resolved path. Add
stateFile(and the resolved root) to thesetup --json
payload and to the human-readable output. A one-line change that makes every split-root
bug in this class self-diagnosing. - Warn when the variable is absent. If
CLAUDE_PLUGIN_DATAis unset while writing
config, emit anextStepswarning rather than silently using$TMPDIR— configuration
is not throwaway state, unlike job files. - Store durable config outside the volatile root.
stopReviewGateis small,
long-lived, per-workspace config;$TMPDIRis the wrong medium for it regardless of
which root wins. A stable location (e.g. under~/.codex/) keyed by workspace hash
removes the divergence for config even if job state stays where it is. - Reconcile on read. Have the
Stophook check the fallback root when the primary has
no state file, and migrate it — also fixes the SessionEnd miss in #636.
Happy to send a PR for (1) and (2) if that direction is useful.
🤖 Generated with Claude Code
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with scripts/lib/state.mjs:29-43, then trace setup, stop-review-gate-hook.mjs:154, and session-lifecycle-hook.mjs:80 to compare state-root resolution when CLAUDE_PLUGIN_DATA is unset. Reproduce the fresh-session case and inspect the setup --json output. Done means setup and the Stop hook use or clearly report the same durable configuration root, with the selected warning or reconciliation behavior covered by validation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- developer-experience, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100