Repository navigation
Stop hook starts node on every turn end even when the review gate is off — add a shell pre-gate #796
Description
Summary
hooks/hooks.json registers node scripts/stop-review-gate-hook.mjs on every Stop. With the review gate off (the default, stopReviewGate: false) the script reads state.json, finds nothing to do, and exits. That still costs a full node start on every turn end. A small sh pre-gate could exit before node starts in that case and keep current behavior otherwise.
Measurement
macOS (Darwin 24.6), Claude Code 2.1.288, plugin 1.0.6. Gate off, no jobs. hyperfine -N, 30 runs, real Stop payload on stdin:
| Command | Mean |
|---|---|
node scripts/stop-review-gate-hook.mjs (today) |
182.6 ms ± 9.6 ms |
sh pre-gate below (gate off, no live jobs) |
14.7 ms ± 0.6 ms |
Classic hooks on one event run in parallel, so this only shortens a turn when node is the slowest Stop hook. Every turn end still pays the CPU cost. #441 reports the same unconditional spawn on Windows, where it also flashes a console window each turn. A pre-gate addresses both without changing how the hook is registered.
Proposal
Point the Stop command at a POSIX sh wrapper. It hands off to node only when some workspace has the gate on or has a queued/running job. The live-job check keeps the "Codex task … is still running" note that main() prints even with the gate off.
#!/bin/sh
# Superset of "stop-review-gate-hook.mjs has work": any state.json with the gate on or a live job.
state_root="${CLAUDE_PLUGIN_DATA:+$CLAUDE_PLUGIN_DATA/state}"
grep -qsE '"stopReviewGate": *true|"status": *"(queued|running)"' \
${state_root:+"$state_root"/*/state.json} "${TMPDIR:-/tmp}/codex-companion"/*/state.json || exit 0
exec node "$CLAUDE_PLUGIN_ROOT/scripts/stop-review-gate-hook.mjs"Design notes:
- It must stay a superset. The script reads the same two state roots
resolveStateDir()can pick,$CLAUDE_PLUGIN_DATA/stateand theos.tmpdir()/codex-companionfallback, across every workspace slug. It does not recompute the workspace hash, so it cannot pick the wrong directory. That matters while/codex:setup --enable-review-gatereports success but writes the flag to a state root the Stop hook never reads — the gate silently fails open #684 / Review gate setup writes to temp dir, but Stop hook reads from persistent dir #59 / Stop-review gate is lost in every new git worktree (state keyed by rev-parse --show-toplevel) #764 leave the gate's write and read locations out of step. A gate that is on in any workspace sends every workspace tonode, which only costs the startup the pre-gate would otherwise skip. - The patterns depend on how state is written today.
saveState()writesJSON.stringify(state, null, 2), which produces"stopReviewGate": trueand"status": "running". If that serialization changes, the pattern has to change with it. A test pinning that serialization would catch the drift. - stdin passes straight through.
grepreads only the state files, soexec nodegets the hook payload untouched. - No change to the gate itself. With the gate on, the hook runs exactly as it does now.
Context
We distribute this plugin through an internal marketplace as a git-subdir source pinned to this repository, so we cannot patch the hook on our side. We are moving our own per-turn Stop hooks off process spawns for the same reason.
Activity