opencode-permission-reviewer

agent
Security Audit
Warn
Health Warn
  • License — License: Apache-2.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 6 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Policy-aware AI permission reviewer for OpenCode. Evaluates tool and command requests, then allows once, denies with feedback, or escalates to the user.

README.md

OpenCode Permission Reviewer

[!NOTE]
This is an unofficial community plugin for OpenCode. It is not affiliated
with or endorsed by Anomaly.

A tool-free AI reviewer for every ask permission. It reads the request,
your policy, and the session context, then allows once, denies with
feedback, or escalates to you
— so safe actions don't wait for a keystroke,
and genuinely risky ones still get blocked or surfaced.

OpenCode
Bun
npm
Downloads
License
Checks
Open issues

OpenCode pauses on every ask permission and waits for a keystroke — even
for safe, routine actions. This plugin adds a Codex-Guardian-style reviewer: a
dedicated, tool-free model session reads the pending request, bounded
transcript evidence, recovered user intent, and a tenant policy you control,
then allows once, denies with rationale, or escalates to you.
Critical risk is never allowed, and anything broken or uncertain
fails safe to manual review.

  • Preserves your policyallow continues, deny stays blocked; neither
    ever reaches the reviewer.
  • Isolated, tool-free reviewer session — the reviewer runs in a scratch
    directory outside your project (no AGENTS.md, project instructions, or
    project MCP servers) with every tool denied through a wildcard session
    permission rule, so it can neither call tools (MCP included) nor request
    permissions recursively.
  • Read-only enrichment — bounded, sanitized SSH / local-script / Git
    evidence for the reviewer; the filesystem is never modified.
  • Auditable — one JSONL record per review, with remote commands stored as
    SHA-256, not plaintext.
  • Optional TUI overlay — shows review state and gets out of the way of your
    native approval controls.

Policy adapted from OpenAI Codex Guardian
— the reviewer policy text in this project derives from Codex's auto-review
policy (Apache-2.0). The implementation is independent. See NOTICE.


Quickstart

Requirements

  • Bun ≥ 1.3.0 (CI runs 1.3.0 and 1.3.5)
  • OpenCode ≥ 1.18.11 and < 2 (tested with 1.18.25)
  • git on PATH (only used for read-only Git-state enrichment; missing git
    degrades gracefully)
  • A model provider configured in OpenCode, exposing a model that follows JSON
    schemas reliably (see Choosing the reviewer model)
  • A permission policy with at least one ask rule — if nothing is ask, the
    plugin never activates
    (everything is already allow/deny).

See Supported versions for the full matrix.

Install

The package is published to npm. Install it as a dependency, or clone and
build when you want to run from a checkout:

# From npm
bun add opencode-permission-reviewer   # or: npm install opencode-permission-reviewer

# From a checkout (development)
git clone https://github.com/Warc0s/opencode-permission-reviewer.git
cd opencode-permission-reviewer
bun install && bun run build

What ships in dist/:

  • Servermain / ./serverdist/index.js (bundled).
  • TUI overlay./tuidist/tui/tui.tsx (raw TSX, not a JS bundle).
    OpenCode's host compiles that entry with its own Solid/OpenTUI pipeline and
    rewrites solid-js / @opentui/* onto the host runtime. A prebundled
    dist/tui.js loads but never paints the overlay.
  • CLI./cli / bindist/explain.js.

The CLI can register the plugin for you (--tui also writes tui.json;
--npm emits an npm package name instead of a path; it never clobbers an
existing entry):

bunx opencode-permission-reviewer init --npm --tui --yes

Configure

Register the plugin in your opencode.json (project or
~/.config/opencode/opencode.json). Use an absolute path to a checkout, or the
npm package name after bun add / npm install:

// opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "/absolute/path/to/opencode-permission-reviewer",
      // or: "opencode-permission-reviewer"
      {
        "model": "openai/gpt-5.6-luna", // default reviewer; override with any provider/model
        "variant": "max",
        "timeoutMs": 120000,
      },
    ],
  ],
  "permission": {
    "bash": "ask", // at least one ask rule, or the plugin is a no-op
  },
}

For the optional TUI overlay, register the same plugin block in your
tui.json (~/.config/opencode/tui.json). Keep model, variant, and
timeoutMs identical in both files so the watchdog and server agree:

// tui.json
{
  "$schema": "https://opencode.ai/tui.json",
  "plugin": [
    [
      "/absolute/path/to/opencode-permission-reviewer",
      { "model": "openai/gpt-5.6-luna", "variant": "max", "timeoutMs": 120000 },
    ],
  ],
}

Restart OpenCode fully after install or rebuild. The host imports the plugin
once at startup; a live session keeps the previous code in memory and will not
show a rebuilt overlay.

Then ask the agent to run something safe, e.g. printf hello. An auto-approved
ask resolves itself with once and the tool runs normally — without
injecting rationale into the agent context. Denials still return a short reason
the agent can act on.

Cost note: every ask action now spawns one extra child-session model
call (up to timeoutMs). Your model spend scales with how much your policy
asks. Lower the reasoning variant or raise confidenceThreshold to taste.

Choosing the reviewer model

The reviewer is a normal OpenCode model invocation (every tool denied at the
session-permission level), so it can
be any model from any provider you have configured. Three options,
identical in both config files:

  • model — in provider/model form. Must match a configured provider and
    a model that provider exposes.
  • variant — reasoning effort the model supports (max, high, medium,
    low, none). Passed straight through to OpenCode.
  • outputFormat — how the reviewer returns its decision: json_schema
    (default; uses OpenCode's structured output, needs provider support) or
    text (ask the model to emit JSON in plain text and parse it locally). Use
    text for models that reject the json_schema format, e.g.
    opencode-go/deepseek-v4-flash.
  • timeoutMs — review timeout; must match across files.

The default reviewer is openai/gpt-5.6-luna (max reasoning) — a real
model that follows JSON schemas well. Override model to use any other
provider/model you have configured; whichever you pick should follow structured
output reliably, because weaker models just produce more escalations (safe, but
noisier). Higher reasoning variants give better safety judgments at higher
cost/latency.

Reviewer models without structured-output support

Some models (for example opencode-go/deepseek-v4-flash) do not support
OpenCode's json_schema structured-output format and fail with a format error
when it is requested. For those, set "outputFormat": "text" so the reviewer
asks the model to emit its decision as plain JSON and parses it locally. This
needs the flag set identically in both opencode.json and tui.json:

{
  "model": "opencode-go/deepseek-v4-flash",
  "variant": "high",
  "outputFormat": "text",
  "timeoutMs": 120000,
}

Text mode is safe but noisier: without host-side schema enforcement the
plugin re-prompts the reviewer once if the response is unparseable (mirroring
the auto-retry that json_schema mode gets from OpenCode), and a response that
is still invalid escalates to a human rather than being auto-approved. Parsing
is deliberately strict and fail-closed: the entire response must be exactly one
JSON object (optionally wrapped in a single Markdown code fence). Prose around
the object, multiple objects, multiple fences, or any other ambiguity escalates
to a human — the parser never guesses which candidate the model meant. Every
parsed decision still passes the same strict parseDecision validation and
enforceDecision invariants (critical risk is never approved, etc.), so text
mode cannot approve anything that structured mode would not.

One caveat applies to any output format: the deterministic gates check the
decision's consistency, not its semantic correctness. A reviewer model that
misclassifies an unsafe action as low risk can produce an unsafe allow in
either mode, so pick as strong a reviewer model as your budget allows.

All configuration options

Every option is optional. Numeric/string options are clamped to safe bounds.

Option Default Bounds / type Description
model openai/gpt-5.6-luna provider/model Reviewer model (override with any provider/model)
variant max non-empty string Reasoning variant passed to OpenCode
outputFormat json_schema json_schema / text How the reviewer returns its decision (text for models without structured output)
timeoutMs 120000 5000600000 Review timeout (match in both files)
confidenceThreshold 0.7 0.51 Minimum confidence to auto-act; below it escalates
maxContextChars 32000 4000200000 Total transcript evidence budget
maxPartChars 8000 50050000 Per-message-part budget
maxEnrichmentChars 24000 1000100000 SSH / script / Git enrichment budget
maxIntentChars 8000 100050000 User-intent history budget
transcriptMessages 12 1100 Recent messages shown to the reviewer
intentMessages 8 150 Genuine user intents kept
historyMessages 200 20500 Messages fetched to recover intent
retainReviewSessions false boolean Keep reviewer child sessions (debug only; see below)
audit true boolean Append one JSONL audit record per review
auditPath ~/.local/share/opencode/permission-reviewer-audit.jsonl path Audit file location
policy built-in default string Full local override of the tenant policy text
debug false boolean Verbose logs to stderr
enforcementMode observe observe / enforce enforce applies declarative policy routes; observe audits them only
escalationMode manual manual / deny How final escalations are disposed (manual = human; deny = fail-closed reject)
maxSessionDepth 8 132 Parent-session lineage walk depth
maxParentSessions 8 032 Max parent sessions resolved for actor context
actorProfiles {} name → profile map Trusted agent name → profile (read-only, validation, workspace, …)
riskPolicy built-in conservative matrix object Override allow cells per risk level and failure modes (onInvalidDecision, …)
repositoryTrust unknown trusted / untrusted / unknown Repository trust level used by the policy engine
policyRules [] array Declarative rules (most-restrictive wins); project rules combine with trusted ones
askDecisions true boolean Show the reviewer what the user answered in agent ask dialogs (scoped authorization evidence)

Config is layered: built-in defaults ← global
~/.config/opencode/permission-reviewer.jsonc ← project
.opencode/permission-reviewer.jsonc ← inline plugin options (later wins).
The project layer crosses a trust boundary: it can only tighten
security-sensitive fields, and its hardening survives even when a trusted layer
set the same field. The project layer cannot choose the reviewer model or
replace the policy text (both decide where code/context travels and what the
reviewer enforces), cannot redirect auditPath, grant actorProfiles, set
repositoryTrust: "trusted", downgrade a global enforcementMode: "enforce",
or relax a trusted escalationMode: "deny" / failure-mode deny knob /
confidenceThreshold / riskPolicy. Project values of the wrong type
(including null) are ignored, never normalized back to defaults.

A config file that exists but cannot be honored fails CLOSED on the trusted
side: a malformed or unreadable global config, or trusted policyRules
dropped by validation, marks the run degraded — reviews still run, but
automatic approval stays off (everything escalates) until the file is fixed,
and the degradation is reported on stderr. A malformed project file is
reported and ignored (the untrusted layer adds nothing anyway).

In declarative policyRules, a when condition with an unknown key (a typo),
a false flag, or an empty object drops the whole rule — a mistyped rule must
never degrade into a universal match. Catch-all rules are spelled explicitly:
omit when entirely, or use "when": { "always": true } (valid only alone).
When a catch-all comes from the trusted global config it simply matches
everything; project-sourced allow rules are still rejected outright.

Interactive vs autonomous

Mode Config Behavior
Interactive (default / 1.0) escalationMode: "manual" Uncertainty escalates to you; OpenCode's native approval UI takes over
Autonomous / fail-closed escalationMode: "deny" Every final escalation becomes a reject with rationale; no human prompt

For unattended agents, set fail-closed in global config (not in the repo):

// ~/.config/opencode/permission-reviewer.jsonc
{
  "escalationMode": "deny",
}

Optional fine-grained hardening under interactive mode (only their own cases):

{
  "riskPolicy": {
    "onInvalidDecision": "deny", // invalid structured output → reject
    "onReviewerFailure": "deny", // timeout / transport failure → reject
  },
}

escalationMode: "deny" hardens every escalate path globally. Restrictive
settings can only block more, never relax security.

audit defaults to true. Each completed review appends one JSON object to
the audit path with mode 0600 (schemaVersion: 2): outcome, decision source,
rationale, risk, authorization, confidence, per-phase latency, reviewer model,
optional reviewerOutcome / escalationDisposition (to distinguish an explicit
deny from fail-closed escalate→deny), and a bounded SSH summary. Remote commands
are stored as SHA-256, never in clear text. Set audit: false to disable.

What you'll see

✓ Review approved · bash · rm -rf /tmp/scratch-cache
Narrowly scoped temp cleanup; matches user intent.

While reviewing, the optional TUI overlay covers the native approval controls
with Reviewing this permission and No action needed, plus the action,
reviewer model, and elapsed time. Once resolved, the overlay becomes a compact
status strip below the session: one line for the result and a second for its
rationale, with long text truncated. The review keymap is released immediately
so you can resume typing while the result stays visible for 5 s. OpenCode still
hides its editor while a permission is pending. On a technical failure or
escalation, the overlay is removed and OpenCode's native approval controls
become available with a manual review required warning. A broken TUI
transport never changes the safety decision.

How it works

  1. OpenCode emits permission.asked for any ask-classified action.
  2. A deterministic emergency brake rejects unmistakable root destruction and
    direct credential export before any model call. It is wrapper-aware
    (sudo, doas, env, command, nice, nohup, …), so sudo rm -rf /,
    env VAR=x rm -rf /, /bin/rm -rf /, sh -c 'rm -rf /', ssh host rm -rf /,
    and busybox rm -rf / are all caught.
  3. The plugin builds bounded evidence: recent transcript, recovered user
    intent, and optional read-only enrichment for SSH commands, local
    interpreter scripts, and Git state. Intent attribution uses a single origin
    rule: synthetic/host-flagged parts are never human intent, and in a
    delegated (subagent) session no user-role message counts as human
    authorization — the initial briefing and every later task_id follow-up
    are agent-authored and surface only as labeled delegation context.
    Common credential formats are always redacted from this evidence
    (Bearer, AWS / GitHub / OpenAI / Anthropic / Slack / Google / Stripe /
    GitLab keys, JWTs, private keys, URL userinfo, cookies, and
    credential-bearing assignments) so a secret you once pasted into the
    session never travels to the reviewer's provider.
  4. An isolated, tool-free reviewer session runs the reviewer model with a
    strict JSON schema and returns { outcome, risk_level, user_authorization, rationale, confidence }. The session is created in a scratch directory
    outside your project so the host does not prepend repository instructions
    (AGENTS.md, project config instructions, project MCP context) to the
    reviewer's system prompt — only your trusted global instructions remain.
    Tool denial is a wildcard session permission rule, which takes precedence
    over agent-config allows and therefore also covers MCP tools and MCP
    resource tools. If the host refuses the isolated directory, the review is
    not run in the project directory: it escalates to the human as a reviewer
    failure instead, so isolation is never silently degraded.
  5. Decisions are enforced with invariants: critical risk is never approved,
    high risk with low/unknown authorization is escalated, medium risk
    with unknown authorization is escalated
    , low confidence is escalated,
    invalid output is escalated, errors and timeouts are escalated. Two
    deterministic blocks also apply regardless of model confidence: a degraded
    trusted config (see above) and evidence where a material part of the action
    itself was elided or truncated — neither can auto-approve. A single
    enforcement boundary then disposes every internal escalate according to
    escalationMode (manual → human; deny → reject with the original reason).
  6. Approved actions get once (never always) and execute silently — the
    tool output is not annotated, so approval rationale never contaminates the
    primary agent context (rationale still lands in audit, TUI, and debug logs).
    Denials return a short actionable rationale as tool feedback. A manual reply
    that arrives mid-review supersedes the automatic one (no double reply).

By default everything fails safe to manual review. With
escalationMode: "deny", uncertainty fails closed to a reject with reason
instead — suitable for non-interactive agents.

Evidence enrichment

The reviewer never sees the raw filesystem — only bounded, sanitized evidence.
Enrichment is deliberately conservative and never makes an approval decision
by itself
(one narrow deterministic exception exists for SSH, below).

  • SSH commands are parsed into destination, options, remote command,
    environment/mutation/secret/stdin signals, and bounded stdin content for the
    common cat script | ssh ... python - pattern. Sensitive paths,
    credential-like literal content, binary files, unresolved shell expressions,
    and symlinks escaping approved roots are excluded.
  • Local interpreter commands (Python, Node, Bun, shell, Ruby, Perl, and
    compound commands that first activate an environment) get the same bounded
    inspection when they name an explicit script. Inline code, modules, stdin
    programs, dynamic paths, and remote-only SSH arguments are not misidentified
    as local files.
  • Git operations (add, commit, checkout, restore, rm) get a
    read-only pre-command snapshot: current branch, files already staged before
    the command, unstaged/untracked files, planned targets, unresolved
    shell-expanded paths, and a bounded numstat for changes that would be
    discarded. Snapshots use fixed non-interactive Git queries with locking and
    hooks disabled, a two-second timeout, and bounded output. The repository is
    never modified.
    Repository-configured conversion filters (clean,
    smudge, process) and diff textconv drivers are enumerated before every
    snapshot and neutralized with config overrides (including dotted names); if
    the configuration cannot be fully verified — too many filters, or the config
    scan itself fails — the snapshot is withheld rather than risk executing
    repository-configured commands. Verification and inspection are still two
    distinct moments: a filter configured between them is a residual race the
    snapshot does not claim to eliminate.

Only regular text files inside the working directory, the worktree, or
/tmp/opencode can be included. Missing, blocked, and truncated executable
stdin is explicitly identified so the reviewer fails safe.

The only deterministic SSH preflight rejection is an executable stdin file
that still does not exist after a 100 ms recheck — the primary agent gets an
actionable instruction to create it and retry. Every other SSH case (sensitive,
binary, blocked, or truncated evidence) remains a reviewer decision.

Safety properties

  • Responds only to permission.asked.
  • Critical-risk actions cannot be approved, even if model output says
    allow.
  • High-risk actions with low or unknown authorization, and medium-risk
    actions with unknown authorization, are deterministically escalated
    — the
    model cannot auto-approve them by labeling a contradictory combination.
  • Invalid, low-confidence, or inconsistent output is escalated to the user.
  • Common credential formats are always redacted from the evidence before
    reaching the reviewer, so credentials never leak to the reviewer's provider.
  • Reviewer sessions cannot request permissions recursively; every tool is
    denied by a wildcard session permission rule that also covers MCP tools and
    takes precedence over agent-config allows.
  • The reviewer session runs outside the project directory, so repository
    instructions (AGENTS.md and project-config instructions) are not part of
    its system prompt; if the isolated directory cannot be established, the
    review escalates to the human as a reviewer failure rather than running
    with degraded isolation.
  • A narrow deterministic emergency brake rejects unmistakable root destruction
    (including privilege-prefixed and command-string forms such as
    sudo rm -rf /, sh -c 'rm -rf /', ssh host rm -rf /) and direct
    credential-file export before any model call.
  • A manual reply that arrives while a review is in flight supersedes it: the
    reviewer stops without replying or resurrecting a UI state.
  • Approvals are silent to the primary agent (no tool-result annotation); denials
    return the rationale as feedback. Rationale remains in audit/TUI/debug.
  • SSH commands and executable stdin receive bounded, untrusted action
    enrichment; enrichment never makes an approval decision on its own.
  • Long-session user intent is recovered separately from recent operational
    context; later explicit requests supersede conflicting older ones.
  • Synthetic compaction/control messages are excluded from authorization
    evidence.
  • Audit failures never affect or relax the safety decision.
  • UI status messages are versioned, request-scoped, bounded, and transported
    through OpenCode's own workspace TUI event channel.

Supported versions

Component Supported Notes
OpenCode >=1.18.11 <2 Declared in engines.opencode; verified with 1.18.25
@opencode-ai/plugin >=1.18.11 <2 Peer dependency for the server transport
Bun >=1.3.0 Declared in engines.bun; CI runs 1.3.0 and 1.3.5
TUI overlay OpenCode V1 Needs the host Solid/OpenTUI plugin pipeline (raw TSX entry)
OS macOS / Linux On Windows, SSH/Git enrichment degrade to fail-safe manual
  • TUI overlay ships as raw TSX (dist/tui/tui.tsx). The host compiles
    it against its embedded Solid/OpenTUI runtime. A prebundled TUI entry loads
    but never paints. The server half does not depend on the overlay.
  • Server half replies through an isolated transport chosen once at startup:
    public SDK reply with feedback message → public reply plus a separate
    feedback channel → authenticated raw HTTP
    (/permission/{requestID}/reply via input.client._client.post) → refuse
    startup
    . On OpenCode 1.18.x the message-bearing reply is only reachable via
    the raw transport, so the chain resolves there. That raw field is not part
    of OpenCode's public plugin API
    and can change without notice. If startup
    fails with "authenticated SDK transport is unavailable", file an issue
    rather than downgrading.
  • OpenCode v2-generation hosts are detected at startup and refused until
    their reply contract is verified.
  • Full enrichment assumes a Unix-like system (macOS/Linux). On Windows, SSH and
    Git enrichment degrade gracefully toward fail-safe manual review.
  • retainReviewSessions: keep it false in normal use. Set true only to
    debug the known json_schema structured-output serialization bug in OpenCode
    1.18.11 — it keeps the child session on disk so you can inspect the malformed
    response; it does not fix the bug.
  • Run opencode-permission-reviewer doctor to compare installed versions
    against the ranges above.

Troubleshooting

Symptom Likely cause Fix
Every ask escalates after a long wait Reviewer model not found / provider not configured Ensure the model's provider is set up in OpenCode and the model ID is valid in both config files
Plugin does nothing No ask rule in your permission policy Add e.g. "bash": "ask"
TUI overlay never appears Not in tui.json; mismatched timeoutMs; stale process; or host without Solid/OpenTUI pipeline Register the same block in tui.json with matching timeoutMs. Overlay is raw TSX (dist/tui/tui.tsx); a prebundled dist/tui.js does not render. Fully restart OpenCode after rebuilds
Startup error: "authenticated SDK transport…" OpenCode outside >=1.18.11 <2, or an SDK change that hides the raw transport Upgrade OpenCode and @opencode-ai/plugin into the supported range; report the version in an issue
Startup error: "Detected an OpenCode v2…" OpenCode v2-generation host Run an OpenCode 1.18.x host (v2 is not supported yet)
Reviews always time out timeoutMs too low for the model Raise timeoutMs (up to 600000)
GIT_STATE_ANALYSIS shows spawn git ENOENT git not on PATH Install git; Git enrichment degrades safely until then
Want a version check Host/SDK outside the supported range Run opencode --version and opencode-permission-reviewer doctor
Want to turn it off Remove the plugin entry from both opencode.json and tui.json

Enable "debug": true for verbose stderr logs while investigating. TUI load
errors ([tui.plugin] …) are printed on the TUI process console, not in
~/.local/share/opencode/log/opencode.log.

Development

bun install
bun run check          # format + lint + typecheck + tests + build (must pass before any push)
bun run test:stress    # stress suite only
bun run test:package   # npm pack ship-set smoke (raw TUI + server bundle)

bun run build bundles the server/CLI with tsup, then copies the slim TUI
source graph into dist/tui/ as raw TSX (scripts/copy-tui.ts). Do not add a
prebundled TUI entry — it will not render on the host.

The live end-to-end harness in tests/live-harness.ts runs against a real
OpenCode server + model and is not part of bun test; see
CONTRIBUTING.md.

Attribution

The reviewer policy and prompt text in src/policy.ts are adapted from
OpenAI Codex Guardian
(Apache-2.0). See NOTICE for full attribution and license details.

License

Apache License 2.0 © 2026 Warc0s

Reviews (0)

No results found