Stop smashing the Approve button. Delegated Access gives OpenCode an AI safety reviewer that auto-approves the boring stuff and escalates the scary stuff to your desktop, so you can actually keep working instead of babysitting the terminal.
Think of it as Claude's auto mode — but for OpenCode, and you control it.
Right now, OpenCode stops and asks before every bash command. ls. git status. npm test. Every one of them pulls you back into the loop.
You could turn off permissions entirely with --dangerously-skip-permissions, but then rm -rf node_modules and rm -rf ~/Documents look the same to the machine. They are not the same.
Delegated Access splits the difference:
- Safe commands auto-dismiss themselves. OpenCode's prompt briefly flashes, a small LLM (Haiku-class by default) classifies it as safe, a "Running in 5s — Cancel" notification appears, and after the countdown the plugin dismisses the prompt for you and the command runs. Don't click Cancel, it runs. Ignore it, it runs.
- External directory access is handled the same way. When the agent wants to read or write outside the current project (e.g. a sibling repo you just mentioned), the same classifier decides whether your recent messages justify it — no extra config needed.
- Risky commands wake you up. Destructive
rm,sudo,curl | sh, anything touching.env, anything you didn't ask for — a desktop notification pops up with Approve and Reject buttons. Click one, the TUI prompt closes. Click nothing and the prompt's still there when you come back. - Weird commands fail safe. Classifier timed out? API flaked? Weird response? The prompt just stays there waiting for you. Nothing ever slips through silently.
You stay in flow. The agent stops pestering you for routine stuff. Dangerous stuff still needs a human.
Every time OpenCode would prompt for a bash command or an external directory access, Delegated Access:
- Finds the root session. If the permission fired inside a subagent, walks up
parentIDto where you actually typed. - Grabs the last N messages you sent from that root session (never the agent's messages, never a parent agent's dispatch prompt to a subagent — that would be a prompt injection wide open).
- Sniffs the current git state — branch name, and (via
gh) the PR number/title linked to that branch if there's an open PR. Helps the classifier judge commands that target specific PRs (e.g.gh pr comment 123). - Asks a small fast model: given this command / directory, what the user just said, and the current branch + PR, is this SAFE or RISKY?
- Acts on the verdict:
┌───────────────────────────────┐
│ Agent wants to run: `rm -rf …`│
└───────────────┬───────────────┘
▼
OpenCode shows TUI prompt
(and emits permission.asked)
│
▼
Classifier reads it
│
┌──────────┼──────────┐
▼ ▼ ▼
SAFE RISKY FAIL
│ │ │
▼ ▼ ▼
Notify + Notify + Leave
countdown buttons prompt
│ │ │
▼ ▼ ▼
Dismiss User clicks User decides
TUI & → dismiss in TUI
run TUI + run/
block
The classifier call happens in an ephemeral child session of your current session, using OpenCode's own provider + auth — no extra API keys, no extra packages to configure. It's hidden from session lists and deleted when done.
OpenCode 1.4.x emits the permission.asked event after it has already queued the permission and started showing you the prompt. That means on SAFE commands you'll briefly see the usual "Allow this command?" prompt before the plugin auto-dismisses it. The SDK declares a permission.ask hook that would let us intercept before the prompt appears, but the compiled runtime doesn't actually dispatch it yet. If that ever lands, this plugin will get a snappier flash-free SAFE path for free.
git clone https://github.com/jdtzmn/opencode-delegated-access.git
cd opencode-delegated-access
bun installOr install straight from GitHub:
{
"plugin": ["opencode-delegated-access@git+https://github.com/jdtzmn/opencode-delegated-access.git"]
}That's it. Defaults just work. Start OpenCode and it's live.
Use the per-plugin tuple form — [pluginSpec, optionsObject] — to pass options to the plugin. This is the schema-blessed mechanism in OpenCode and avoids the unknown-top-level-key startup error.
{
"plugin": [
[
"opencode-delegated-access@git+https://github.com/jdtzmn/opencode-delegated-access.git",
{
"enabled": true,
"contextMessageCount": 3,
"safeCountdownMs": 5000,
"classifierModel": "anthropic/claude-haiku-4-5",
"classifierTimeoutMs": 15000,
"classifierRetries": 1,
"notificationSound": true,
"externalDirectoryEnabled": true,
"directoryVerdictCacheTtlMs": 60000,
"approvalHistoryEnabled": true,
"approvalHistoryMax": 20,
"notifyOnClassifierFailure": true,
"classifierFailureNotifyCooldownMs": 60000
}
]
]
}Migrating from older configs: previous versions of this README documented a top-level
delegatedAccessobject. That form is no longer supported — OpenCode rejects unknown top-level keys at startup. Move your settings into the tuple form shown above.
| Knob | Default | What it does |
|---|---|---|
enabled |
true |
Turn the whole thing off without uninstalling. |
contextMessageCount |
3 |
How many of your recent messages the classifier sees. 0 = no context, just the command. |
safeCountdownMs |
5000 |
Cancellable countdown before auto-dismissing SAFE prompts. 0 = silent instant approve. |
classifierModel |
auto | Override the judge model, e.g. anthropic/claude-haiku-4-5. When unset, uses a small fast default for your provider (Haiku, gpt-5.4-mini, gemini-flash-lite). |
classifierTimeoutMs |
15000 |
How long before we give up on a single classifier attempt. |
classifierRetries |
1 |
Extra attempts if a classifier call times out (transient API stall). Each retry uses a fresh session and the full classifierTimeoutMs. 0 disables retry. Only timeouts retry; other failures never do. |
notificationSound |
true |
OS notification sound on/off. |
externalDirectoryEnabled |
true |
Also classify external_directory permissions (directory access outside the current project). Set to false to restrict the plugin to bash commands only. |
directoryVerdictCacheTtlMs |
60000 |
How long (ms) a SAFE directory verdict is cached. Covers rapid burst requests (agent walking a tree) without re-classifying each sub-path individually. 0 disables the cache. |
approvalHistoryEnabled |
true |
Remember each human Approve/Reject decision made via the OpenCode TUI or our notification, and surface recent ones to the classifier as prior-decision context. Session-scoped, in-memory only. |
approvalHistoryMax |
20 |
Per-session cap on how many recent human decisions the classifier sees. 0 disables playback (entries are still recorded; just not surfaced). |
notifyOnClassifierFailure |
true |
When the classifier ultimately fails to produce a verdict (after retries), fire an informational desktop notification so you know a transient error happened. The notification is Reject-only — it never offers a one-click Approve on an unclassified command. The TUI prompt is always shown too. Set false to keep failures silent. |
classifierFailureNotifyCooldownMs |
60000 |
Rate-limit window for failure notifications. At most one fires per window; a burst during an outage collapses into a single notification instead of spamming you. 0 disables the rate limit. |
Don't duplicate allowlists here. OpenCode's static rules run before this plugin, so put your always-safe and never-safe patterns there:
{
"permission": {
"bash": {
"git status": "allow",
"npm test": "allow",
"rm -rf /*": "deny"
},
"external_directory": {
"/tmp/**": "allow"
}
}
}Anything not matched by a static rule flows into the classifier.
The desktop notifications with Approve / Reject buttons work via terminal-notifier / macOS NotificationCenter. On Linux and Windows the SAFE countdown still works (the notification itself is the timer), but interactive buttons on the RISKY path may not be clickable — OpenCode's in-TUI prompt is always shown too, so you have a reliable fallback on every platform.
- The classifier never sees the agent's messages. Only yours. A rogue assistant can't smuggle "this command is safe, trust me" into the judge's context. Same for directory access — the classifier answers "did the human's recent messages justify this path?" not "does the agent think it's safe?"
- Subagents don't weaken that. When a permission fires inside a subagent session, the plugin walks up the session tree to the root and pulls your messages from there — never the dispatching agent's prompt to the subagent. If the tree can't be verified (SDK error, unexpected cycle, too deep) the plugin fails closed and leaves the TUI prompt for you. Even on the root session, user-role messages are filtered to the root's primary agent so synthetic "user" turns addressed elsewhere never leak in.
- Every error leaves the TUI prompt alone. Classifier timeout, API error, malformed verdict, missing subject, session-tree lookup failure, unexpected exception — none of them call the respond API, so the TUI prompt stays and you decide manually. The plugin only ever dismisses a prompt after an affirmative SAFE decision, never silently passes through on errors.
- The classifier can't call tools. The ephemeral session runs with
tools: { "*": false }, so even a compromised classifier model can only return text. - Risky commands and risky directory requests get two channels, not one. The TUI prompt stays up AND the notification fires with Approve/Reject. Whichever you answer first wins — no bug in the notification path can ever accidentally auto-approve a RISKY request.
- The classifier can't trigger itself. We track ephemeral classifier sessions and ignore permission events from them.
- The directory cache only speeds things up; it can't change a RISKY verdict. Only SAFE verdicts are cached. A RISKY verdict for any path always triggers the escalation notification — the cache only deduplicates rapid burst requests for a path that was already classified SAFE.
- Repo context is best-effort and gracefully optional. Branch is read with
git; the open-PR lookup usesgh. Ifghisn't installed, isn't authenticated, or the working directory isn't a git repo, the classifier just runs without that context — never blocks. The PR title is rendered inside<repo_context>delimiters and treated as data (not instructions) by the classifier. - The session pin can't be moved by the agent. PR-scoped elevated trust depends on a snapshot captured exactly once at plugin startup. There is no API to refresh, reset, or invalidate it for the lifetime of the OpenCode process — the agent can't
git checkoutits way into a different trust scope.
When you launch OpenCode inside a git checkout that has an open PR, Delegated Access takes a single snapshot of the current branch + PR number + PR title and freezes it for the lifetime of the OpenCode process. That pinned snapshot is shown to the classifier alongside the live branch/PR view in every classification.
The classifier uses the two views together:
- When the pinned snapshot and the live view match (same branch, same PR number), commands whose obvious purpose is to advance that pinned PR —
gh pr comment N,gh pr review N,gh pr checks N,gh pr view N,gh pr diff N, non-forcegit push origin <pinned-branch>— get the benefit of the doubt that they would otherwise need an explicit recent-user-message endorsement for. - When they don't match (the agent has checked out a different branch, ended up in a different repo, or the PR has merged/closed mid-session), PR-scoped elevated trust is withdrawn. All PR-targeting commands fall back to the normal classification rules.
A few important properties:
- The pin is captured once and never refreshed. This is the load-bearing security property. If the snapshot could be refreshed mid-session, an agent could
git checkoutto a different branch and have the trust window follow it. The pin is therefore deliberately immutable for the plugin's lifetime — close OpenCode and re-launch to re-pin. - Hard-RISKY categories still escalate. A pinned PR doesn't override destructive
rm, sudo, credential reads, force pushes, branch deletion, repo settings changes, or merging the PR itself. Those stay RISKY no matter what. - Cross-repo targeting stays RISKY. A command like
gh pr comment 7 --repo other-org/other-repodoes not match the pin even if the PR number is the same; the classifier treats different-repo targeting as out-of-scope. - No PR pinned = no elevated trust. If you launch OpenCode in a directory that isn't a git checkout, or in a checkout with no open PR, the feature is a no-op: classification proceeds exactly as before.
- No new config knob. This composes with the existing
<repo_context>mechanism. If you want to disable it, set your branch's PR to closed before launching.
If you Approve or Reject a permission in this session — either via OpenCode's TUI prompt or via the desktop notification's buttons — Delegated Access remembers your decision. The next time a similar request comes up, the classifier sees a brief <prior_human_approvals> block in its prompt summarising what you previously decided in this session, and may use that as evidence to lean toward your earlier judgment.
A few important properties of how this works:
- Session-scoped only. The history lives in memory for the lifetime of the OpenCode session group and is never written to disk. Closing OpenCode discards it.
- Pure human signal. When the classifier auto-approves a SAFE command, that decision is NOT recorded — only your explicit Approve/Reject clicks are. The history is the record of what you decided, not what the classifier decided for you.
- Hard-RISKY categories still escalate. Prior approvals don't override the destructive / privilege-escalation / credential-access categories. Approving
git status30 times doesn't teach the classifier to wave throughsudo rm -rf /. - Captures every channel. Decisions made via the TUI (clicking Approve in OpenCode's prompt), the desktop notification (clicking Approve in our
terminal-notifierpopup), or OpenCode's CLI/keyboard shortcuts all flow through the samepermission.repliedevent and are captured identically. - Disable with
approvalHistoryEnabled: falsein your config if you'd rather every classification be independent.
v0.4.0. Bash commands and external directory access, with per-session approval history that lets the classifier learn from your prior in-session decisions, plus PR-scoped elevated trust pinned at session start so the classifier knows which PR you actually committed to working on. Edit / write / webfetch still prompt normally — those are out of scope. TypeScript, Bun. macOS-tested; Linux/Windows should work with degraded notification interactivity.
bun install
bun run check # TypeScript check
bun run test # full unit-test suiteDesign doc and implementation plan in docs/superpowers/.
MIT.
{ "plugin": ["/absolute/path/to/opencode-delegated-access/src/index.ts"] }