Motivation
The stop-time review gate (plugins/opencode/scripts/stop-review-gate-hook.mjs) runs a targeted review on every Claude Code stop event when enabled. For a long, chatty session this can fire dozens of times, burning OpenCode tokens (and real money if the user configured a paid provider) faster than they realize.
There's a warning in the README, but there's no mechanism. Users have to either leave the gate off entirely or accept unbounded spend.
Proposed UX
# Allow at most 5 stop-gate reviews per session
/opencode:setup --review-gate-max 5
# Require at least 10 minutes between stop-gate reviews
/opencode:setup --review-gate-cooldown 10
# Combine both
/opencode:setup --enable-review-gate --review-gate-max 5 --review-gate-cooldown 10
# Remove a limit
/opencode:setup --review-gate-max off
/opencode:setup --review-gate-cooldown off
When a limit is reached, the stop-gate review is skipped (session is allowed to end) and a one-line note is logged. /opencode:review still runs manually.
Implementation
1. handleSetup in opencode-companion.mjs — accept two new value options:
async function handleSetup(argv) {
const { options } = parseArgs(argv, {
valueOptions: ["review-gate-max", "review-gate-cooldown"],
booleanOptions: ["json", "enable-review-gate", "disable-review-gate"],
});
// ... existing setup body ...
if (options["review-gate-max"] != null) {
const max = options["review-gate-max"] === "off" ? null : Number(options["review-gate-max"]);
if (max !== null && (!Number.isInteger(max) || max < 1)) {
throw new Error(`--review-gate-max must be a positive integer or "off".`);
}
updateState(workspace, (state) => {
state.config = state.config || {};
state.config.reviewGateMaxPerSession = max;
});
}
if (options["review-gate-cooldown"] != null) {
const cooldown = options["review-gate-cooldown"] === "off" ? null : Number(options["review-gate-cooldown"]);
if (cooldown !== null && (!Number.isInteger(cooldown) || cooldown < 1)) {
throw new Error(`--review-gate-cooldown must be a positive integer (minutes) or "off".`);
}
updateState(workspace, (state) => {
state.config = state.config || {};
state.config.reviewGateCooldownMinutes = cooldown;
});
}
}
2. Persist per-session state in state.json:
{
"config": {
"reviewGate": true,
"reviewGateMaxPerSession": 5,
"reviewGateCooldownMinutes": 10
},
"reviewGateUsage": {
"<sessionId>": { "count": 3, "lastRunAt": "2026-04-12T14:03:22Z" }
}
}
3. Gate check in stop-review-gate-hook.mjs (add before the existing stdin read):
const sessionId = getClaudeSessionId();
const now = new Date();
if (sessionId && state.config) {
const usage = state.reviewGateUsage?.[sessionId] ?? { count: 0, lastRunAt: null };
const max = state.config.reviewGateMaxPerSession;
if (Number.isFinite(max) && usage.count >= max) {
console.log(`ALLOW: Review gate session cap (${max}) reached.`);
return;
}
const cooldownMin = state.config.reviewGateCooldownMinutes;
if (Number.isFinite(cooldownMin) && usage.lastRunAt) {
const elapsedMs = now - new Date(usage.lastRunAt);
if (elapsedMs < cooldownMin * 60 * 1000) {
const remaining = Math.ceil((cooldownMin * 60 * 1000 - elapsedMs) / 1000);
console.log(`ALLOW: Review gate cooldown (${remaining}s remaining).`);
return;
}
}
}
// ...proceed with stdin read + OpenCode call...
// After a successful review, bump usage:
updateState(workspace, (s) => {
s.reviewGateUsage = s.reviewGateUsage || {};
const entry = s.reviewGateUsage[sessionId] ?? { count: 0, lastRunAt: null };
entry.count += 1;
entry.lastRunAt = now.toISOString();
s.reviewGateUsage[sessionId] = entry;
});
4. Surface in /opencode:setup output so users can see their current limits and usage. In renderSetup:
Review gate: enabled (limit: 5/session, cooldown: 10 min)
Used this session: 3/5, last run 4 minutes ago
5. Garbage-collect old session entries. On setup calls, drop entries older than 7 days so reviewGateUsage doesn't grow unbounded across many sessions.
Test plan
- Set
reviewGateMaxPerSession: 2, simulate 3 stop events with the same session ID. Assert the third returns ALLOW: Review gate session cap (2) reached. without calling OpenCode.
- Set
reviewGateCooldownMinutes: 10, run one stop event, wait simulated 5 min, run second. Assert second is skipped with cooldown message.
- Unset both limits — behavior matches current unbounded path.
- Different session IDs have independent counters.
/opencode:setup with no flags surfaces current counts and limits.
Upstream reference
openai/codex-plugin-cc#20 (open).
Port of openai/codex-plugin-cc#20 (open)
Motivation
The stop-time review gate (
plugins/opencode/scripts/stop-review-gate-hook.mjs) runs a targeted review on every Claude Code stop event when enabled. For a long, chatty session this can fire dozens of times, burning OpenCode tokens (and real money if the user configured a paid provider) faster than they realize.There's a warning in the README, but there's no mechanism. Users have to either leave the gate off entirely or accept unbounded spend.
Proposed UX
When a limit is reached, the stop-gate review is skipped (session is allowed to end) and a one-line note is logged.
/opencode:reviewstill runs manually.Implementation
1.
handleSetupinopencode-companion.mjs— accept two new value options:2. Persist per-session state in
state.json:{ "config": { "reviewGate": true, "reviewGateMaxPerSession": 5, "reviewGateCooldownMinutes": 10 }, "reviewGateUsage": { "<sessionId>": { "count": 3, "lastRunAt": "2026-04-12T14:03:22Z" } } }3. Gate check in
stop-review-gate-hook.mjs(add before the existing stdin read):4. Surface in
/opencode:setupoutput so users can see their current limits and usage. InrenderSetup:5. Garbage-collect old session entries. On setup calls, drop entries older than 7 days so
reviewGateUsagedoesn't grow unbounded across many sessions.Test plan
reviewGateMaxPerSession: 2, simulate 3 stop events with the same session ID. Assert the third returnsALLOW: Review gate session cap (2) reached.without calling OpenCode.reviewGateCooldownMinutes: 10, run one stop event, wait simulated 5 min, run second. Assert second is skipped with cooldown message./opencode:setupwith no flags surfaces current counts and limits.Upstream reference
openai/codex-plugin-cc#20 (open).