Guard
Permission modes and a shell safety floor for opencode: static read rules, credential guards, a classifier consulted only when nobody can be asked, loop detection and secret redaction.
0
412
近 7 天 21
36.1
生态多维模型
6 小时前
2026-10-21
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-guard@1.0.1"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-guard@1.0.1"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D opencode-guardOpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
Permission modes and a shell safety floor for opencode. It lets a coding agent work without asking about every command, while still refusing destructive commands, keeping it out of credential stores and scrubbing secrets from what it reads. A language-model classifier is used for exactly one question, and only when nobody is there to answer it instead: may this command run with nobody to ask?
Install
npm install -g opencode-guard # the oc-mode, oc-auto, oc-check and oc-reveal commands
// ~/.config/opencode/opencode.json
{ "plugin": ["opencode-guard"], "permission": { "edit": "ask", "bash": "ask", "webfetch": "ask" } }
Restart opencode. The permission block matters: opencode's default is to allow
almost everything, and the modes below decide how the guard answers the prompts
opencode raises. With nothing set to ask there is nothing to answer, and only the
floor and the classifier gate (which do not depend on it) are left.
To run from a checkout instead, npm install in it and symlink plugin.js to
~/.config/opencode/plugin/opencode-guard.js; imports resolve against the
symlink's target, so lib/ and node_modules/ are found in the checkout.
Quick start
With nothing configured you are in manual mode: opencode asks about everything it
normally asks about, and the floor runs under it. Try asking the agent to
cat ~/.ssh/id_ed25519 or git reset --hard; both are refused with a reason,
whatever your permission rules say.
See what the guard would do with a command, without running it:
oc-check --static 'git push --force origin main'
oc-check --static 'cd src && rg TODO | head -20'
To let the agent work without prompts, point the guard at a classifier and switch modes:
cat > ~/.config/opencode/classifier.json <<'EOF'
{ "url": "http://localhost:11434/v1/chat/completions", "model": "qwen2.5:7b-instruct", "private": true }
EOF
oc-mode auto
Any small instruction model that answers in one word will do; a model that thinks
out loud before answering needs its thinking turned off (see extraBody below), or
it spends its token budget before it says SAFE.
In auto, file edits and reads go through, shell commands on the static read list
run at once, and any other shell command runs only if the classifier answers SAFE.
Anything else is refused with a message the agent can act on. oc-mode manual
goes back.
Headless runs (opencode run, opencode serve, --auto) behave like auto
whatever the mode says, because nobody is there to answer a prompt.
Configuration
Every file is optional. The switches are re-read on every decision, so a change applies to running sessions without a restart.
| File | Set by | Purpose |
|---|---|---|
~/.config/opencode/mode |
oc-mode |
Global mode: manual, edits, auto. |
~/.local/share/opencode/modes/<sessionID> |
a TUI integration | Per-session mode, including god. Overrides the global one. |
~/.config/opencode/autoclass |
oc-auto |
Level: on, static, off. |
~/.config/opencode/reveal |
oc-reveal |
Deadline of the reveal window. |
~/.config/opencode/classifier.json |
you | The classifier (below). Re-read on every classification. |
~/.config/opencode-guard/config.json |
you | Site policy (below). Read once at startup. |
~/.local/share/opencode/autoclass.log |
the plugin | Decision log, truncated at 512 KB. |
~/.local/share/opencode/classifier-verdicts.json |
the plugin | Verdict cache shared by sessions, keyed by a hash of directory and command, six-hour TTL. |
Modes and levels
The table describes how the guard answers a permission opencode raises; "ask" means it leaves the prompt to you.
| Mode | File edits and reads | Shell commands | Other permissions |
|---|---|---|---|
manual (default) |
ask | ask | ask |
edits |
allow | ask | ask |
auto |
allow | static read: allow. Otherwise the classifier: SAFE allows, anything else is refused | allow |
god |
allow | allow, floor off | allow |
god exists only per session, only in an attended session, and is never inherited
by a subagent. The floor (below) runs in every other mode. Redaction runs in all
four.
On an OpenCode host that advertises native permissionAuthority: 2, god follows the
god escalation policy.
The god root's own request for any visible tool is allowed, including one that an
opencode.json deny rule names: the guard returns an exact, logged
override: "root" for that one request, and nothing is saved. Opencode's deny
rules therefore no longer hold for the god root itself. Any refusal of a
descendant of a god root on bash, read, glob, grep, edit or write becomes an exact
parent ask that the root model answers once or rejects with guard_pending /
guard_decide. That covers seat and role denies, session ceilings, named deny rules,
the guard's own floors, control-file writes and reveal-arm. Under an auto root, a
descendant's native deny is judged the way the root's own command would be (the
classifier, or a one-time human prompt for a named rule). Tree gates, the launch guard,
the read-intent gate and hidden tools are unchanged. Unattended runs, level off and
older hosts keep the behaviour described here, with no escalation and no override.
Known gap: refusals from pty_spawn, bg_run and other process tools without a
guaranteed native ask still throw for god descendants.
The level is the escape hatch: on is everything, static drops the classifier
(in auto, a command that is not a static read then gets a prompt instead of a
model call), and off turns the plugin off.
Site policy: ~/.config/opencode-guard/config.json
Also $OPENCODE_GUARD_CONFIG. See examples/config.example.json.
| Key | Meaning |
|---|---|
extraCredentialPatterns |
[{ "pattern": "<regex>", "root": "<path under $HOME>" }]. Paths refused like ~/.ssh/id_*. root lets the native-tool guard see a glob or grep over a directory that contains one. |
extraPromptableCredentialPatterns |
["<regex>"]. Commands that read a secret and must reach a person: a prompt when attended, a refusal when not. rbw, bw, op, pass and gopass reads are built in. |
credentialAdvice |
One sentence appended to every credential refusal, telling the agent how secrets are meant to reach a process at your site. |
trustedSshHosts |
["gpu-box", "*.lan"]. Hosts whose ssh host "<command>" payload is judged by the static read table like a local command. Empty by default; ["*"] trusts every host. |
loopGuard |
{ "enabled": true, "warnAt": 12, "handBackAt": 25 }. |
Extras can only add protection. A malformed file or pattern is ignored and the built-in floor stands.
The classifier: ~/.config/opencode/classifier.json
Any OpenAI-compatible /chat/completions endpoint works: llama.cpp, Ollama, vLLM,
LM Studio, or a hosted API. The request is one system prompt and one line naming
the working directory and the command; the answer must start with SAFE for the
command to run.
| Key | Meaning |
|---|---|
url |
Endpoint. Also $OPENCODE_GUARD_CLASSIFIER_URL. |
model |
Model id sent in the request. |
authToken |
Bearer token. Also $OPENCODE_GUARD_CLASSIFIER_TOKEN. No header is sent without one. |
authTokenFile |
A file whose first line is the bearer token, read on each classification, so the token itself stays out of this file. ~/ is expanded. authToken wins when both are set. |
private |
true if the endpoint keeps data on machines you control. Only then may a session under a privacy profile use it. |
votes |
1 to 5. Above 1, the classifier is asked that many times and SAFE needs a strict majority. |
timeoutMs |
Default 25000. |
extraBody |
Merged into the request body. A key set to null removes a default (temperature, top_p, seed, max_tokens: 8). For llama.cpp with a thinking model: { "chat_template_kwargs": { "enable_thinking": false } }. |
promptAddendum |
Site rules appended to the built-in prompt, taking precedence over it, e.g. an operation that is routine on your machines but on the built-in RISKY list. |
systemPrompt |
Replaces the built-in prompt entirely. |
broker |
false disables the optional broker lane. |
brokerDir |
Broker state directory when it is not ~/.local/share/opencode/model-routing. |
noThinkProviders |
Provider ids whose routed fleet-classifier calls receive llama.cpp's no-thinking option. Default: ["llamacpp"]. |
A hosted endpoint:
{ "url": "https://api.openai.com/v1/chat/completions", "model": "gpt-4.1-mini" }
with the key in OPENCODE_GUARD_CLASSIFIER_TOKEN. A small local instruction model
is usually the better choice: it is fast, free per call, and command text never
leaves the machine.
The optional broker lane
opencode-broker is a model router
for opencode. When its socket exists (~/.local/share/opencode/model-routing/broker.sock,
or brokerDir), the guard uses it in two ways. It reads the session's routing
profile, so that a privacy profile's command text never reaches a cloud classifier.
And it adds a second classifier lane: the guard creates one disposable, unpinned
fleet-classifier child (see the complete agent example). The broker's
ordinary chat.message route owns that child's single classifier lease, model and
reasoning variant; the guard owns its prompt, timeout and cleanup.
The direct endpoint is tried first when both exist; the broker lane is the fallback
when the direct one fails fast. "broker": false turns the lane off.
Without the broker, every session's profile is auto and the direct endpoint is
the only lane. Nothing else changes.
Commands
| Command | Does |
|---|---|
oc-mode [manual|edits|auto|next] |
Show or set the global mode. Refuses god. |
oc-auto [on|static|off|next] |
Show or set the level. |
oc-check [--static] '<command>' |
Walk the same tiers as the plugin and report what each mode would do. |
oc-reveal [<seconds>|off] |
Open or close the reveal window, 60 s by default, 600 s at most. |
All four refuse to change anything when run from inside opencode.
Design
One question
A safety layer for a coding agent is usually built as "ask a model whether this command is dangerous". That is the wrong question. The question the layer has to answer is narrower:
May this run with nobody to ask?
When a person is there, the right answer to an ambiguous command is a prompt. A
prompt costs one keypress. A model's opinion in the same situation is strictly
worse: slower, sometimes wrong, and when it is wrong it ends the task instead of
asking. So in manual and edits the classifier is never called. No model call,
no latency, nothing to be wrong about.
The classifier is reached in exactly two situations: auto mode, where the person
has said they do not want to be asked, and a headless session, where nobody can
be. There it decides alone and fails closed. A static read passes without a call.
A SAFE verdict passes. RISKY, an unreadable answer, a timeout or an unreachable
endpoint are all refusals. The expensive path is also the rare one: most commands
are settled by the static tiers, and most attended sessions make no classifier
calls at all.
Why auto refuses rather than prompts: on opencode 1.18 the permission.ask
plugin hook never fires, so a plugin cannot turn a verdict into a dialog. It can
throw, stand aside, or answer a permission that has already been raised. A prompt
for every gray-zone command is what manual mode already is. A refusal that says
what was refused and why, and tells the agent to try a narrower way or stop and
ask, is what auto is for.
The mode ladder
manual, edits, auto, god, in the shape of Claude Code's permission modes.
Two axes are kept apart on purpose. The mode says how much is waved through
without asking. The level says how hard the floor pushes back. Folding them into
one knob makes "turn the checks down a little" and "stop asking me" the same
action, and they are not.
god is a deliberate, per-session act. It is never the global default (oc-mode
refuses it, and a hand-written global god reads as auto), never inherited by a
subagent, and never honoured in a headless run, including a headless run that
resumes a session whose file says god. The point of having it is that "no
limits" is something a person switches on, rather than a reason to loosen the other
three modes until they are convenient.
A restrictive default is not automatically a safe one. A subagent used to fall
through to the global default, which was manual, the strictest mode. The result
was not extra safety: every permission the subagent raised landed on the operator
as a prompt, and its shell commands were never classified at all. A layer that asks
too often teaches people to stop reading what it asks.
The floor
Three checks run on every process-spawning tool call (bash, and pty_spawn,
pty_write and bg_run from pty and background-shell plugins) in every mode but
god:
- cc-safety-net static analysis. A deny verdict throws. So does an exception from the analyser: its API contract is that a throw means "do not run it".
- The launder floor. opencode's own
permission.bashglobs let read verbs run unprompted (echo *,fd *,git diff*), and a glob cannot look inside argv:echo x > ~/.bashrc,fd -HX rm -rf,git diff --output=fand a quotedrg '--pre' shall match a read allow. For every segment opencode would run without asking (its last-match-wins verdict is recomputed from the global config the way opencode reads it:config.json,opencode.jsonandopencode.jsonc, JSONC, over opencode's"*": "allow"default; a config that cannot be read fails closed and is reported), the floor parses the command with the same tree-sitter bash parser opencode uses and judges every command node -- inside loops, subshells, groups and substitutions too. It refuses a redirect into a file (anything but/dev/null, fd merges, or a literal path inside/tmp/opencode/) on a statement whose commands would all run unprompted, a read verb whose guard (below) says it writes or runs a program, and an allowed command it cannot verify as a read at all. Commands opencode would ask about are not judged: a person approved those. The parser is a dependency (npm install); if it cannot load, the plugin andoc-checksay so and the floor refuses any command with compound syntax. - The credential guard. Paths such as
~/.ssh/id_*,~/.aws/credentials,~/.kube/config,~/.docker/config.json,~/.gnupg/and/etc/shadow, plus the site's own, are refused outright. Password-manager reads are refused when nobody is present and left to a prompt when someone is.
The credential guard lives in the plugin rather than in opencode's permission
globs, and this is the detail most worth knowing. opencode resolves permission
rules last match wins, and an agent's own allow block is appended after the
global rules. A single cat *: allow in any agent definition silently overrides a
global deny on credential paths. The declarative approach is unsound under that
ordering, so the check has to live somewhere that cannot be appended past.
The static read table
About 140 inspection verbs (ls, rg, jq, git log, docker ps,
kubectl get, systemctl status, ...) settle without a model call. Many read by
default and write with one flag, so they carry per-verb guards: sed without -i,
find without -exec or -delete, curl without a body, upload, output file or
method flag (matched as prefixes, so -XPOST and -sSLo are caught), git branch
only listing (write flags in any cluster or abbreviation, or a name without a
list-mode option, are writes), git grep without -O, git diff/log/show without
--output or --ext-diff, fd without -x/-X in any cluster, rg without
--pre or --hostname-bin, uniq/xxd with one operand, xxd without -r,
pdftotext to stdout, tree without -o, yq without -i, bat/ag without
--pager, kubectl get of anything but a Secret. exiftool is not in the table
(-if runs Perl).
The command line is lexed with quotes honoured and every segment of a pipeline or
&& chain has to be a read on its own, so cd repo && rg foo settles while
rg -l x | xargs rm -f does not. A redirect that can write, or a command
substitution, disqualifies its segment. Wrappers (env, timeout, nice, and the
output compressors snip and rtk) are looked through. The payload of
ssh host "<command>" is judged by the same rules when the host is in
trustedSshHosts: what a command does does not change because it runs elsewhere,
but whether you trust the machine it runs on is yours to say.
The table exists for determinism as much as for speed. A small classifier asked the same read twice has been measured calling it SAFE once and RISKY once. A rule does not flip, costs nothing and can be tested.
The credential guard on native tools
read, glob and grep never produce a shell command, so their path arguments
are checked directly, after ~ expansion and symlink resolution of the deepest
existing prefix. A read of a protected path and a recursive grep over a tree
containing one are refused. A glob only lists names, so it is refused only when
its pattern could actually enumerate a credential file: glob ~ **/* reaches
~/.ssh/id_ed25519 and is refused, glob ~ **/src/cursor.py cannot and is not.
The pattern is compiled to an anchored expression and tested against stand-ins for
each protected path.
Redaction and the reveal window
The leak that actually happened was not a blocked command. It was an allowed one
whose output carried a live credential into a transcript: a management API's app
config with a database password, a backup job's query with a cloud storage key,
docker inspect printing an environment block. Transcripts are stored, backed up
and read by other agents.
So secret values are replaced in tool output in every mode, god included, since
redaction stops nothing from running. Matching is by key (password, token,
secret, a trailing _key, PEM blocks) across JSON, KEY=value and YAML shapes,
not by entropy: guessing that a string looks random mangles the hashes, ids and
base64 an agent legitimately needs. The output says how many values were redacted,
because an agent that silently receives a marker where a value should be assumes
the command failed and retries it.
Redaction needs a way out or it becomes the thing people switch off. oc-reveal 60
opens a window, for at most ten minutes, in which nothing is redacted and every
output that would have been is labelled as unredacted, so a transcript containing
a secret does not look like one that does not. The window is armed from a real
terminal only: the script refuses to run under opencode, and the plugin refuses any
tool call that names it or writes its file.
The guard's own switches
The mode, level, reveal deadline and classifier config are plain files. A native
write to ~/.config/opencode/autoclass in auto mode is an edit plus an
external-directory permission, and auto approves both. So a tool call that writes
one of those files, or a shell command that names one and is not a static read, is
refused, and the oc-* commands refuse to change anything when run under opencode.
Like the reveal guard, this stops a helpful model from unblocking itself. It is
not a sandbox.
Subagents inherit their ancestors' mode
A subagent has no mode file of its own. It runs under the stricter of two
inherited modes: its parent's (the parent's file, else the global mode) and that
of the nearest ancestor with a mode file (else the global mode), both capped below
god. A direct child gets its parent's mode exactly as before 1.7.0; a deeper
subagent can no longer run under a global mode looser than the one set on its
root. If the guard cannot read a subagent's ancestry it refuses the subagent's
tool calls rather than guess, and leaves its permission prompts to the person.
When a subagent in an inherited mode is refused a permission, the
refusal is answered rather than left as a prompt for the operator, and the parent's
task result gets a list of what was refused and why. The parent holds the mode the
child was refused under, so it decides: run it itself, re-dispatch a narrower
brief, or ask the user. A credential request from such a child always fails
closed; fetching a secret is the parent's call.
The dispatch gate
An agent can declare capability: read, write or exec in its frontmatter. A
task dispatch to a read agent must start its prompt with the line
INTENT: read; otherwise it is refused before a child session is created, with a
message naming the mismatch. Handing write work to an agent that cannot write fails
anyway, later, after the child was created and paid for, with an error that looks
like a permission problem instead of the dispatch mistake it is. Agents that
declare write, exec or nothing are untouched, so the gate cannot break a
dispatch that works today. It runs even in god: god lifts limits on what may run,
it is not licence for an incoherent tool call.
Agent team tree rules
Background tasks (task with background: true, behind opencode's
OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS flag) are the teammates of an agent
team. These rules keep a team a hub around the session that started it. They run
above the god bypass, like the dispatch gate, and a lookup the guard cannot
complete refuses the call with an error naming the rule.
- G1:
taskwithtask_idresumes only a child of the calling session. opencode itself resumes any session id, and starts a fresh child for an id that does not exist. - G2:
background: trueonly from a root session with a person at the TUI, and never together withtask_id. - G4: a teammate, and anything it dispatches, cannot call
workflow_run,peer_*,schedule_*,bg_watch,bg_unwatch, or abg_*tool withall: true.
A teammate is a child of a root whose creating task call had
background: true in its arguments; a foreground task promoted to the
background later is not one. G3 is the mode rule in the section above.
G4 fails closed on a session it cannot classify. A child of a root with no
creating task part in the root's history (for example a child that
opencode-agent-workflows creates through the SDK for a workflow step) is treated
as possibly in a team: it, and everything below it, is refused workflow_run,
peer_*, schedule_*, bg_watch, bg_unwatch, and any bg_* call with
all: true, and the refusal says retrying will not help. Its other tools are
unaffected. This is deliberate: a teammate's creating part can disappear from
the root's history (a revert removes it), and a missing part must never turn a
teammate into an outsider. Classification is decided by the session just below
the root on the caller's path, so a workflow child started from a foreground
subagent is classified by that subagent's (foreground) task part and keeps
these tools.
Only a creating part counts: a task part that carries task_id (a resume)
never classifies a child, so a foreground resume of a teammate cannot make it
look like an outsider. A "no creating part" answer is remembered for 30 seconds
so a retried call does not re-page up to 200 pages of history; a stale entry can
only refuse. A history page that arrives without paging headers counts as the
last page only when it is short, and a transient (5xx or transport) session read
is retried once before the lookup fails.
Level off skips G1, G2 and G4, like the other checks that run before a tool
call; the teammate index is still fed from events at every level.
Refusing tool calls after Esc
opencode 1.18 runs a tool call the model emitted without first checking whether the turn was cancelled; it looks at the abort signal only after the tool ran. Normally that window is milliseconds. With a starved event loop it was measured at minutes: subagents kept reading files after the operator pressed Esc. Before every tool call the guard reads the session status list, which holds only sessions that are busy or retrying, and refuses a call for a session that is not on it. A status read that fails allows, so this can never stall a live session.
The loop guard, and how it differs from doom_loop
opencode has a built-in doom_loop permission, ask by default. It fires when the
last three tool calls in one assistant message are the same tool with identical
input. It is fast and it catches repeated writes, but it misses the loop that
actually happens: an agent rotating through a few read-only probes (A, B, C, A, B,
C), across many messages, getting the same answers back. One such session was
measured at 306 repeats. And because doom_loop is a permission prompt, it only
helps when someone answers it: a headless run answers it without a person
(--auto approves it, a plain opencode run rejects it), and this plugin's auto
and god modes approve it like any other non-shell permission.
The loop guard counts sterile repeats: a read-only tool call whose input and output both match an earlier call in the same session, with no state-changing call in between. Repeating a read of unchanged state acquires no information; that is a fact about the call, not a guess about the model. The streak is global to the session, so a rotation counts. Any state-changing call (an edit, a write, a task, any tool not known to be read-only) clears the memory, so a long burst of edits to one file never reads as a loop, and a read whose output changes (a log being tailed) never counts. At 12 the tool result gets a warning; at 25 a subagent is handed back: its parent is sent the evidence, then the child is aborted. A root session is warned and never aborted. The thresholds come from recorded sessions, where the looping one reached 306 and four long legitimate ones peaked at 2 to 5; an earlier sliding-window design scored a legitimate editing burst worse than the real loop and was dropped.
The two are complementary. Leave doom_loop on.
Privacy profiles
With opencode-broker installed, a session can carry a privacy profile (local,
private, or one added later). Any profile other than auto and manual is
treated as one whose command text may not leave local infrastructure, including
names the guard has never seen: the check is on the shape of the name, not a list,
because a list would fail open the day the broker gains a profile. Such a session
is classified only by a direct endpoint marked private, or by a fleet-classifier
child whose broker route inherits that confinement from its owner and selects a local
target. With neither, the command is refused, not sent.
Quoted Python stdin
The agent-spawn check recognizes at = ... as a Python assignment rather than
an at scheduler command only inside a directly invoked, isolated Python 3
program supplied by a quoted stdin heredoc:
python3 -I -S -B - <<'PY'
record = {"at": 1}
at = record.get("at")
print(at)
PY
Both -I and -S are required so working-directory imports, PYTHONPATH and
site hooks cannot replace the standard library; -B is optional. A bundled
helper requires python3 and parses the complete body with ast.parse; it
never executes that body. Only a restricted subset of data processing and
read-only file access is accepted. Unsupported imports, calls, syntax or
bindings remain refused, as do unisolated invocations and wrappers. Missing
Python or a failed checker also refuses the exemption.
The checker limits input to 64 KiB, the complete AST to 4,000 nodes and depth 80, and its subprocess to two seconds. Existing raw-source launcher and local server API checks still apply. This exception is neither a general permission grant nor a Python/OS sandbox.
What it is not
A sandbox. The guard is built to stop a well-meaning agent from doing something harmful by mistake and from quietly unblocking itself. A process determined to defeat it on a machine where it can already run code will find a way: build a path the patterns do not match, or run an interpreter. The floor raises the cost of an accident; isolation is a different tool.
Integrations
All optional, all through documented contracts rather than code dependencies:
opencode-broker (profiles and a classifier lane), pty and background-shell plugins
(their process-spawning tools are judged like bash), the snip and rtk output
compressors (looked through as wrappers), and any TUI plugin that writes
per-session mode files.
Compatibility
opencode 1.x, tested on 1.18.22 and later. A port to opencode 2.x is planned. The
plugin runs inside opencode; the oc-* commands and the tests need Node 20 or
later. cc-safety-net 2.4 is used as a library through its checkCommand API, so
its own plugin-installer requirements do not apply.
Prior art
- cc-safety-net does the static
analysis in the first tier of the floor, and does it far better than a regex
table could: it parses the shell, follows
cd, heredocs and interpreter bodies, and knows whichgitandrmforms destroy work. opencode-guard uses it as a library and adds what it does not cover: modes, the classifier, native-tool and site credential guards, redaction and the session-level checks. - Claude Code's permission modes are the model for the mode ladder: ask by default, accept edits, an automatic mode, and an explicit bypass.
- opencode-permission-reviewer
reviews every
askpermission with a tool-free model session, reading the request, transcript evidence and a policy you write, and allows, denies with feedback or escalates. It is broader than this plugin's classifier and asks the model about attended sessions too; opencode-guard's premise is that an attended session should get a prompt, not a model. - opencode-safety-classifier
checks every
bashcommand with an on-device parser first and a safety classifier at an endpoint you control second, and denies on any failure. The shape is close to this plugin's shell path; the difference is where the classifier is consulted (always, there; only when nobody can be asked, here) and the rest of the layer around it.
License
MIT. See LICENSE.
同类生态推荐
Auto Permissions
opencode-auto-permissions
Automatic, context-aware permission review for OpenCode
@get-keel/opencode-plugin
@get-keel/opencode-plugin
Keel enforcement plugin for OpenCode — blocks rule violations at tool.execute.before, outside the agent's context window.
Guardrails Md
@bergetai/guardrails-md
GUARDRAILS.md gate for opencode, pi and Claude Code (hooks module): a System One decision model (TypeSafe Jev) judges every bash command before it runs — destructive / credentials / GUARDRAILS.md policy