Strict Goal Mode for OpenCode: a primary goal agent, specialized review
subagents, slash commands, a goal-guard plugin that enforces review discipline
and blocks destructive shell commands, and a structured Goal-owned todo section
in the TUI sidebar.
One command. Needs Node 20.11+ and a working OpenCode. Works on macOS and Linux:
npm install -g opencode-goal-mode && opencode-goal-mode --globalThen restart OpenCode. That's the whole install — it copies the Goal agent,
review subagents, slash commands, and guard plugin into ~/.config/opencode, and
merge-safely registers the Goal todo sidebar in ~/.config/opencode/tui.json.
In the agent picker you'll see only the goal agent; reviewers are subagents
it drives automatically. The install is idempotent (re-run it to upgrade in
place), never touches files you've edited, and --uninstall removes exactly what
it added. Goal Mode inherits your existing OpenCode model/provider.
Other ways to install
# Global npm install, then run the installer separately
npm install -g opencode-goal-mode
opencode-goal-mode --global # alias of opencode-goal-mode-install
# Preview first, then install (no writes on --dry-run)
opencode-goal-mode --global --dry-run
# One-off install with npx (no global package needed; OpenCode still loads the TUI
# sidebar — it resolves that from its own plugin cache, not the global install)
npx opencode-goal-mode --global
# Into a single project (writes ./.opencode, including ./.opencode/tui.json)
npx opencode-goal-mode
# Clean removal of everything it installed (incl. its tui.json entry)
opencode-goal-mode --global --uninstall
# From source
git clone https://github.com/devinoldenburg/opencode-goal-mode
cd opencode-goal-mode && npm ci && npm run install:globalUse global install for normal daily use. Use project install only when you want
Goal Mode scoped to one repo and your OpenCode build reads project .opencode
config, including .opencode/tui.json. See Installer options.
↑ In goal mode, the Goal plugin takes over the sidebar todo section with a
structured, evidence-aware Goal todo list — a bold GOAL label, then the goal
title, gate progress, and per-acceptance/gate todo rows, each on its own line in
its own colour, with a first-display rainbow. Build and every other mode keep
OpenCode's native todo section — see TUI integration.
Quick start · Why it's different · Benchmarks · TUI integration · Configuration · Releasing · Architecture
# 1. Install (needs Node 20.11+ and OpenCode)
npm install -g opencode-goal-mode
opencode-goal-mode-install --global
# 2. Restart OpenCode, then verify it loaded — you should see ONLY `goal (primary)`,
# with every specialist as a (subagent):
opencode agent list | grep goal-
In OpenCode, start a goal:
/goal add rate limiting to the login endpoint and prove it worksThe
goalagent writes a contract, delegates research/review to subagents, and cannot answerGoal Completeduntil every required review gate passes — the guard rewrites a premature claim toGoal Not Completed. Try a destructive command mid-session (e.g.rm -rf build) and watch it get blocked. If your OpenCode build supports TUI plugins, Goal sessions also get the Goal-owned sidebar todo section (experimental — see TUI integration).
That's it. Everything below is detail.
See ARCHITECTURE.md for the design and research/ for the platform reference, comparison, and threat model.
Most "goal mode" / agentic setups are prompt-only: the model is asked to
review its work and to keep going until done. Goal Mode adds a guard plugin that
makes that discipline mechanical at the harness layer — the model cannot
declare Goal Completed until the required reviews actually passed, and it
is blocked from the benchmarked destructive-command bypasses that a regex guard
would miss.
Compared to Claude Code and OpenAI Codex (full analysis, with citations and honest caveats, in research/goal-mode-comparison.md):
- It is the only one of the three that mechanically blocks a premature
completion claim by default. Goal Mode intercepts the finished message and
rewrites
Goal Completed→Goal Not Completedunless every required reviewer gate has a fresh PASS and the claimedReview cycles: Nmatches the recorded counter. Claude Code can do this only via a user-authored Stop hook; Codex's code review is advisory. - An edit automatically invalidates prior approvals. A reviewer gate counts only when its PASS is newer (by a monotonic integer sequence) than the last edit — so any change forces the relevant reviews to re-run. The public Claude Code and Codex docs reviewed do not describe this stale-review invariant.
- Required specialist reviews are auto-selected and enforced (security, api, data, performance …) from the goal text, contract, and changed files — not left to the model's discretion.
- Destructive commands are blocked by a real shell tokenizer, not a regex. Claude Code's own docs call Bash argument-matching "fragile".
The headline number is measured on commands the analyzer was never fitted to:
704 real example commands from tldr-pages
(common/linux/osx), authored by hundreds of contributors who have never seen
this guard. Ground-truth labels come from a deliberately simple, analyzer-independent
rule (see build-external-corpus.mjs).
Reproduce with npm run bench or node benchmarks/external.mjs.
| On 704 real third-party commands | Legacy regex guard | Goal Mode analyzer |
|---|---|---|
| Destructive-command detection | 53.8% | 93.3% |
| False positives on safe commands | 0.2% | 0.2% |
Honest caveats, because the point of this rewrite was to stop overclaiming:
- The 7 remaining "misses" are all plain
rminvocations without-r/-f(single- or multi-target, a few with-i/-v/-d), which the guard intentionally permits: barermis extremely common, so the guard marks it dirty but lets the host's ownrm *permission decide, while still blocking the irreversible forms (rm -r/rm -f, wildcard/root,$(rm …),bash -c,/bin/rm, interpreters, etc.). Under a strict every-rm-is-destructive labeling those count against it. - The single counted false positive (
git filter-repo …) actually is a history-rewriting command, so the real-world false-positive rate is effectively zero.node benchmarks/external.mjs --jsonlists every miss and false positive so you can audit the disagreements yourself.
Two curated fixture sets also ship — and they are explicitly fixtures, not an unbiased benchmark. They define the patterns the analyzer must catch and guard against regressions, so they pass by construction; do not read the 100%/0% there as measured accuracy:
benchmarks/corpus.mjs— 71 destructive patterns (incl.$(…),bash -c,sudo -u,/bin/rm,git -C … reset --hard,curl | sh, interpreter deletes) and their safe look-alikes (git checkout -b,echo "rm -rf /").benchmarks/completion-corpus.mjs— 9 completion-claim policy cases (missing review-cycle line, stale review after edit, missing contextual gate, inactive session, custom marker).npm run bench:truthfulnessprints them.
The analysis costs ~1µs per command (hundreds of thousands of classifications per second) — negligible for a per-tool-call guard:
- Node.js 20.11 or newer.
- OpenCode configured to load local agents, commands, and plugins. The package is
tested against
@opencode-ai/plugin1.17.6 and declares compatibility with the 1.15+ plugin hook surface used here; newer OpenCode builds that change plugin or TUI slot APIs may need a package update. - A working OpenCode provider/model; Goal Mode does not configure API keys or choose a model for you.
- A primary
goalagent that owns implementation but delegates research, discovery, verification planning, and reviews to subagents.goalis the only user-selectable agent — every specialist (security, diff, verifier, …) is amode: subagentthat the Goal agent invokes via the task tool; the user never picks one directly, and the guard blocks any other agent from invoking them (see Goal-only subagents below). They surface with friendly names (e.g. "Security Reviewer", "API Reviewer") rather than raw ids. - Strict review gates for prompt compliance, diff review, verification, security, UX, operations, data, API, performance, tests, docs, quality, and final audit.
- Slash commands:
/goal,/goal-contract,/goal-review,/goal-evidence-map,/goal-status,/goal-repair,/goal-final. - The
goal-guardplugin:- Quote-aware shell analysis that blocks destructive and remote-exec
commands (including ones that evade naive regexes —
$(rm -rf …),bash -c "…",/bin/rm,git -C … reset --hard,curl | sh) without false-positiving harmless commands likegit checkout -b. - Completion enforcement: a premature
Goal Completedis rewritten toGoal Not Completedwith the exact missing review gates. - Contextual gating: the goal text and changed files determine which specialist reviewers are required.
- Goal-only subagents: the
goal-*specialist subagents are mechanically locked to Goal Mode. OpenCode resolves subagents globally, so the guard blocks any Build, Plan, or custom agent that tries to invoke agoal-*reviewer via the task tool — they run only under the Goal agent (toggle withrestrictSubagents). General-purpose subagents (explore/general/scout) are never restricted. - Reviewer Memory: blocking reviewer findings are carried across cycles, surfaced in status/system context, and marked resolved by fresh PASS verdicts.
- Disk persistence: review ledgers and Reviewer Memory survive OpenCode restarts.
- Custom tools:
goal_contract,goal_evidence,goal_evidence_map,goal_reviewer_memory,goal_status,goal_reset. - Live state injection into the system prompt so the model always knows what the guard requires.
- TUI toasts: a toast on each review verdict (PASS/FAIL), with the reviewer's friendly name, and a single "completion unlocked" toast the moment the last required gate clears.
- Quote-aware shell analysis that blocks destructive and remote-exec
commands (including ones that evade naive regexes —
- An experimental companion TUI plugin (
plugins/goal-sidebar.tsx) that, in Goal sessions only, takes over the sidebar todo area with a structured, evidence-aware Goal todo list (GOALlabel, goal title, gate progress, and todo rows — each on its own line in its own colour). It shows a first-display rainbow, then normal goal colours. See TUI integration. - A test suite validating the analyzer, plugin hooks, state store, install safety, and config compatibility.
Goal Mode is a plugin pair: the server-side goal-guard plugin owns
enforcement and writes its state to disk, and an experimental TUI plugin
(plugins/goal-sidebar.tsx) reads that same state to render a live todo section.
-
Goal-owned todo section. In a
goalsession with a goal set, the Goal plugin renders its own structured todo section into the sidebar'ssidebar_contentslot, stacked on separate lines, each in its own colour so it never reads as one run of text:- a bold
GOALlabel (yellow while running, red when done); - the short goal title (white);
- the gate count
passing/total gates(cyan), on its own line; - the lifecycle status (orange) on its own line —
in progress, orcompleted · N review cycles. No "changes pending" noise; pending work shows as a todo row instead; - structured todo rows derived from real guard state: one per acceptance criterion (✓ when fresh evidence covers it), a re-verify row when the tree changed, and one row per still-missing review gate by friendly name (e.g. "Pass Security Reviewer").
It opens with a first-display rainbow (
sidebarRainbowMs) so the takeover is visible, then settles to the lifecycle colours (running → yellow label; done → red). Because OpenCode renders the native todo list as that slot's fallback, on builds that rendersidebar_contentin replace/single-winner mode the Goal section replaces the native todo list while a goal is active; in append mode it sits alongside it. In every case:- no render — Build and every non-Goal mode (and a Goal session before a goal is set) render nothing here, so OpenCode's native todo section stays in the same position. The section is scoped to the session that owns the goal: a Build session in the same worktree never inherits another session's goal.
Toggle/recolour with
sidebarBanner,sidebarColor(running),sidebarDoneColor(done),sidebarMutedColor,sidebarRainbowMs, or theGOAL_GUARD_SIDEBAR_*env vars.How it loads — important. TUI plugins are not loaded from the
plugins/dir; OpenCode loads them fromtui.json. The Goal sidebar registers asidebar_contentslot that renders content only for the active session when that session is a Goal session; for any other session it renders nothing, so non-Goal modes keep their native todo section. With--global, the installer writes~/.config/opencode/tui.jsonfor you (merge-safe):{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-goal-mode"] }OpenCode installs the referenced package into its own plugin cache (
~/.cache/opencode/packages/) and provides the@opentui/solid+solid-jsruntime to it. It does not re-check that cache for newer versions, so the installer clears the cached copy on install/uninstall — that's why an upgrade needs only a restart to load the new sidebar. Restart OpenCode after install. The Goal todo section appears in a Goal session view (not the home screen and not Build mode), and because the Goal agent does its own todo tracking (nativetodowriteis disabled in Goal Mode), it replaces — rather than sits beside — the native todo list while a goal is active. The visual harness renders the component headlessly in visual test (npm run test:visual); the enforcement core is a separate server plugin and works regardless of the sidebar. - a bold
-
Toasts. Review verdicts and completion-unlock events surface as toasts (
toastOnReview), and blocked destructive commands / premature completions toast as before (toastOnBlock).
npm install -g opencode-goal-mode && opencode-goal-mode --global
npx opencode-goal-mode --global --dry-run
npx opencode-goal-mode --global
opencode-goal-mode-install --global --uninstall
node scripts/install.mjs --dry-run
node scripts/install.mjs --target /path/to/opencode-config
node scripts/install.mjs --global --force
node scripts/install.mjs --global --uninstallDefault target rules are simple: --global writes to ~/.config/opencode; no
flag writes to ./.opencode; --target writes to exactly the directory you pass.
In every target, the installer copies only agents/, commands/, plugins/,
writes .goal-mode-manifest.json, and merge-safely adds opencode-goal-mode to
tui.json in that same target. On upgrade it replaces files it owns but refuses
to clobber files you have locally modified unless --force is passed.
--uninstall removes only owned files and removes only its own tui.json entry.
The guard works with zero configuration. To tune it, add options in
opencode.json:
Or via environment variables (GOAL_GUARD_*):
| Option / env | Default | Effect |
|---|---|---|
blockDestructive / GOAL_GUARD_BLOCK_DESTRUCTIVE |
true |
Block destructive bash before execution. |
blockNetworkExec / GOAL_GUARD_BLOCK_NETWORK_EXEC |
true |
Block curl | sh-style remote execution. |
enforceCompletion / GOAL_GUARD_ENFORCE_COMPLETION |
true |
Rewrite premature Goal Completed. |
injectSystemState / GOAL_GUARD_INJECT_SYSTEM_STATE |
true |
Inject live state into the prompt. |
persist / GOAL_GUARD_PERSIST |
true |
Persist state under the XDG state dir. |
contextualGates / GOAL_GUARD_CONTEXTUAL_GATES |
true |
Require specialist gates by goal keywords. |
restrictSubagents / GOAL_GUARD_RESTRICT_SUBAGENTS |
true |
Block non-Goal agents from invoking the goal-* subagents via the task tool. |
maxSessions / GOAL_GUARD_MAX_SESSIONS |
200 |
Session cache size. |
sessionTtlMs / GOAL_GUARD_SESSION_TTL_MS |
86400000 |
Idle session TTL. |
toastOnBlock / GOAL_GUARD_TOAST_ON_BLOCK |
true |
Toast when something is blocked. |
toastOnReview / GOAL_GUARD_TOAST_ON_REVIEW |
true |
Toast on each review verdict and when completion unlocks. |
sidebarBanner / GOAL_GUARD_SIDEBAR_BANNER |
true |
Show the experimental Goal todo section in the TUI sidebar. |
sidebarColor / GOAL_GUARD_SIDEBAR_COLOR |
#FFD700 |
Normal colour of a running goal after the first-show rainbow. |
sidebarDoneColor / GOAL_GUARD_SIDEBAR_DONE_COLOR |
#FF5555 |
Colour of a done goal in the sidebar (red). |
sidebarMutedColor / GOAL_GUARD_SIDEBAR_MUTED_COLOR |
#808080 |
Reserved muted colour for no-goal projections. |
sidebarRainbowMs / GOAL_GUARD_SIDEBAR_RAINBOW_MS |
4500 |
First-display rainbow duration for the Goal todo section. |
The plugin registers six tools the model can call directly:
goal_contract— record the Goal Contract (requirements, non-goals, acceptance criteria). Activates enforcement and fixes the required gates.goal_evidence— record a verification command and result.goal_evidence_map— return the acceptance-criteria evidence map with reviewer status, gaps, and next actions.goal_reviewer_memory— return unresolved and recently resolved reviewer findings.goal_status— return the authoritative gate/dirty/completion status.goal_reset— clear the session's goal state (requiresconfirm: true).
Use /goal-evidence-map when you need a read-only matrix of each acceptance
criterion against recorded evidence, reviewer status, gaps, and the next
required action. The command is backed by the goal_evidence_map tool, so it
uses persisted Goal Guard state rather than relying on transcript memory.
npm test
npm run validate
npm run audit
npm run publish:checknpm run validate runs the test suite, the structural config validator, the
publish readiness check, and an npm pack --dry-run.
Agents do not pin a provider-specific model, so they inherit the model OpenCode
is configured to use. To give a particular agent a specific model, add a
model: (and optional variant:) line to that agent's frontmatter in your
installed copy.
The installer copies only agents/*.md, commands/*.md, and the plugins/
tree — never auth files, session files, tokens, or personal provider config.
The guard blocks destructive shell commands, marks real file mutations dirty,
keeps read-only inspection from dirtying the session, preserves goal state during
compaction and across restarts, and blocks premature Goal Completed responses
when review gates are missing or stale.
Releases are fully automated and version-synced: one pushed tag publishes to
npm and creates the matching GitHub Release. The pipeline lives in
.github/workflows/publish.yml (Node 24).
npm version patch # bumps package.json + package-lock.json and creates the vX.Y.Z tag
git push --follow-tags # pushes main + the tag → the Release workflow runsOn a vX.Y.Z tag push the workflow:
- installs and runs the full CI gate (
npm run ci— tests, audit, structural validation,npm pack --dry-run); - runs
npm run publish:check, which fails if the tag does not matchpackage.jsonor if that version already exists on npm; - publishes with
npm publish --access publicusing theNPM_TOKENrepository secret; - creates the GitHub Release for the tag with auto-generated notes.
So the git tag, the package.json version, the npm version, and the GitHub
Release version are always identical. A manual workflow_dispatch is available
and defaults to a safe npm publish --dry-run.
One-time setup: add a publish-scoped npm token as the NPM_TOKEN repository
secret (gh secret set NPM_TOKEN). Treat that token as sensitive — never commit
it.
Goal Completed is allowed only when:
- All acceptance criteria are mapped to evidence.
- Required verification passed or is credibly accounted for.
- No edit is newer than the latest required review cycle.
- Required reviewers return
Verdict: PASS. - The final answer includes an accurate
Review cycles: N.

{ "plugin": [ ["./plugins/goal-guard.js", { "blockDestructive": true, "contextualGates": true }] ] }