这家馆子跟别处不一样:前厅和后厨,是两间锁死的屋子,中间只留一道传菜口。
客人落座、报菜名,话并不直接钻进厨师的耳朵——跑堂的把菜名写成一张单子,顺着传菜口的滑轨送进去。后厨这边,厨师从不靠脑子记"刚才那桌点了什么";他手边压着一本流水账,每做完一步,就回头把整本账从头翻一遍,看看下一步该干嘛。菜谱、火候、上到第几道,全写在账上,不在他心里。
要紧的是:厨师做菜,从头到尾不出后厨那扇门。真遇上得由客人拍板的时候——比如这块牛排要几分熟、这道菜里搁了客人忌口的料——他也不亲自跑到前厅去问。他把那道半成品菜别在传菜口的夹子上,当当摇一记铃,然后就地站住等。这道菜就卡在那儿,厨师的手停着,可后厨一刻没关火,别的锅照炒。
铃一响,前厅哪个跑堂听见都能来应:大堂的、外卖窗口的、甚至电话那头的老板,谁先把"就要三分"或者"这个换一道"顺着滑轨传回来,厨师才把夹子上那道菜取下,接着做。
你留意没有:这间馆子真正做菜的地方,是那间谁都进不去、却谁都能隔着传菜口递话的后厨。前厅?不过是众多"来递话的窗口"里的一个。
把这套布局翻成工程话,就是 OpenCode 最大的一处招式:它把整个 harness 做成了一台 HTTP 服务器——循环、工具、连"等你点头"的那次审批,全都活在服务端;你眼前的 TUI,只是众多客户端里的一个。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
OpenCode — the harness as an HTTP server: a server-side loop and a pull-based, network-resolved permission gate
Other Coding Agents · 他山之石
中文速览 · Quick read
Citations pinned at opencode@77429f5 (cloned 2026-07-08).
OpenCode bills itself as "The open source AI coding agent" from sst — an MIT-licensed TypeScript monorepo that runs on Bun (opencode/README.md:10). Its ~30 packages split UI from engine, and the split is the whole personality of the thing. The entrypoint is a yargs CLI (opencode/packages/opencode/src/index.ts:45-47) whose subcommands include serve (opencode/packages/opencode/src/index.ts:93): the harness itself is an HTTP server, and the TUI, desktop, and web apps are all clients that attach to it. Where Claude Code is one process that owns its own terminal, OpenCode is a server that owns the agent and lends its screen to whoever connects.
Internally, everything is an Effect service wired as layers — the prompt engine is declared Context.Service("@opencode/SessionPrompt") (opencode/packages/opencode/src/session/prompt.ts:111). Model I/O goes through the Vercel AI SDK's streamText (opencode/packages/opencode/src/session/llm.ts:280), and a code comment spells out the division of labor cleanly: "AI SDK owns provider execution and tool dispatch" (opencode/packages/opencode/src/session/llm.ts:276-277). OpenCode writes the policy — what tools exist, when to compact, who may approve — and rents the mechanics of streaming and tool-call matching from the SDK.
A user prompt is first persisted as a message row; then prompt() calls loop(), which runs the turn under a per-session run-state lock — loop hands the actual work to SessionRunState.ensureRunning, which keys a Runner per sessionID so a second concurrent call on the same session just awaits the in-flight run's outcome instead of starting a parallel one (opencode/packages/opencode/src/session/prompt.ts:1052-1071, opencode/packages/opencode/src/session/prompt.ts:1343-1347). The engine is a literal while (true) (opencode/packages/opencode/src/session/prompt.ts:1088), and its defining move is that it keeps no conversation in memory. Every single iteration re-reads the session's message history from the database, compaction-filtered, before deciding anything (opencode/packages/opencode/src/session/prompt.ts:1092-1094). The loop's state is the SQL store; control flow is derived from the data, not carried in local variables.
The exit test reads that freshly-loaded history: stop when the newest assistant message finished with a reason other than tool-calls, carries no unresolved tool parts, and is newer than the last user message (opencode/packages/opencode/src/session/prompt.ts:1111-1130). Otherwise the loop takes one "step": it writes a fresh assistant message row (opencode/packages/opencode/src/session/prompt.ts:1186-1201), assembles the tool set and system inputs, and hands an LLM stream to a processor handle (opencode/packages/opencode/src/session/prompt.ts:1272-1286). That SessionProcessor drains the AI-SDK event stream — text-delta, tool-call, tool-result, step-finish — into persisted message parts (opencode/packages/opencode/src/session/processor.ts:627-646) and returns one of three verdicts, "compact" | "stop" | "continue" (opencode/packages/opencode/src/session/processor.ts:30). The loop maps those to break, enqueue a compaction turn, or iterate again (opencode/packages/opencode/src/session/prompt.ts:1319-1329). Crucially, tool execution happens inside the stream — the AI SDK calls each tool's execute itself — so by the time the loop re-enters, results are already rows in the DB waiting to be re-read.
A tool is a Tool.Def: an id, a description (usually loaded from a sibling .txt), parameters as an Effect Schema, and execute(args, ctx) returning {title, metadata, output} (opencode/packages/opencode/src/tool/tool.ts:55-65). Tool.define wraps every tool with two universal behaviors: argument validation whose failure becomes a typed, model-facing error — "Please rewrite the input so it satisfies the expected schema" is fed back as the tool result (opencode/packages/opencode/src/tool/tool.ts:24-34) — and automatic output truncation with the overflow spilled to a file (opencode/packages/opencode/src/tool/tool.ts:130-145). The registry assembles the builtins (shell, read, glob, grep, edit, write, task, webfetch, todo, skill, patch…) (opencode/packages/opencode/src/tool/registry.ts:224-247), and SessionTools.resolve converts each Def into a Vercel AI SDK tool({description, inputSchema, execute}), wrapping the real call in tool.execute.before/tool.execute.after plugin hooks (opencode/packages/opencode/src/session/tools.ts:92-134). Dispatch is then entirely the SDK's.
The permission gate is where OpenCode diverges most sharply from Claude Code. Rules are a flat list {permission, pattern, action: allow|ask|deny} and evaluation is last-matching-rule-wins, defaulting to ask (opencode/packages/opencode/src/permission/index.ts:28-38). But the check is pull-based and lives inside each tool: instead of one central pre-execution gate, every tool calls ctx.ask() at the exact moment it knows what it is about to touch, carrying evidence. The edit tool asks with the concrete unified diff in its metadata (opencode/packages/opencode/src/tool/edit.ts:145-153); the shell tool parses the command into a syntax tree, walks every subcommand, and asks with per-command patterns (opencode/packages/opencode/src/tool/shell.ts:378-414). And because the harness is a server, Permission.ask cannot pop a prompt. When any pattern evaluates to ask, it parks the request in a pending map, publishes an Asked event, and blocks the tool's Effect fiber on a Deferred (opencode/packages/opencode/src/permission/index.ts:67-107). The reply arrives over HTTP from whatever client is attached — POST /permission/:requestID/reply calls svc.reply (opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38), which resolves the Deferred; a reject fails it (optionally carrying user feedback), while always appends allow-rules and auto-approves any pending request the new rules now cover, cascading across the session (opencode/packages/opencode/src/permission/index.ts:109-167).
Token accounting is folded from provider-reported usage on every step-finish event (opencode/packages/opencode/src/session/processor.ts:435-484). isOverflow compares the running total against usable — the model's input limit minus a reserved compaction buffer of 20k tokens (opencode/packages/opencode/src/session/overflow.ts:8-20, opencode/packages/opencode/src/session/overflow.ts:22-34). On overflow the processor cuts the stream short and returns "compact", and here is the twist: compaction does not summarize inline. compaction.create writes a synthetic user message carrying a compaction part into the session (opencode/packages/opencode/src/session/compaction.ts:513-536). The next loop iteration finds that queued task and runs it as an ordinary LLM turn — executed by a hidden built-in compaction agent with every tool denied ("*": "deny") (opencode/packages/opencode/src/agent/agent.ts:219-233). Compaction is just another turn the same loop dispatches, run by a locked-down internal agent.
After compaction, history-rewriting is a read-time filter: filterCompacted walks the messages, drops everything a completed summary covers, and retains only [compaction-user, summary, retained tail…] (opencode/packages/opencode/src/session/message-v2.ts:521-572) — and the loop applies it on every iteration (recall opencode/packages/opencode/src/session/prompt.ts:1092-1094). Two cheaper layers run alongside the summarizer: prune erases old completed tool outputs once they pile up, keeping the last turns (opencode/packages/opencode/src/session/compaction.ts:243-287), and every tool's output is already truncated at the source with the full text spilled to a file (opencode/packages/opencode/src/tool/tool.ts:130-145). There is no CLAUDE.md-style auto-loaded memory organ; the analogous instruction files enter as system-prompt inputs assembled per step (opencode/packages/opencode/src/session/prompt.ts:1257-1263).
Same organs, rehoused in a client/server body. Each row maps OpenCode's mechanism to the Claude Code page that dissects the equivalent:
- The loop → /stories/loop/1/. Same "call model → run tools → loop until no tool calls" cycle, but Claude Code's in-memory message array becomes a SQL store re-read every iteration (opencode/packages/opencode/src/session/prompt.ts:1088), and the tool-execution half is delegated to the AI SDK inside one stream (opencode/packages/opencode/src/session/processor.ts:627-646).
- Tool anatomy → /stories/tools/1/. Same name + description-file + schema + execute shape (opencode/packages/opencode/src/tool/tool.ts:55-65), with a typed "rewrite your input" protocol on validation failure (opencode/packages/opencode/src/tool/tool.ts:24-34).
- The permission gate → /stories/tools/2/. Allow/ask/deny with an "always allow" memory, but inverted: the check happens inside the tool (which volunteers its own diff/command as evidence), and approval is an async HTTP round-trip through a server-side
Deferred(opencode/packages/opencode/src/permission/index.ts:67-107) — where Claude Code's resolver is an in-process confirm queue by default, racing an optional remote/bridge channel alongside it (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92). - Compaction → /stories/context/1/. Threshold-triggered, model-written summary — implemented as a queued compaction turn run by a tool-denied internal agent (opencode/packages/opencode/src/agent/agent.ts:219-233), plus reversible output pruning Claude Code's page can contrast.
- Memory → /stories/context/2/. No dedicated auto-load organ; instruction files are system-prompt inputs (opencode/packages/opencode/src/session/prompt.ts:1257-1263) — "no such organ" is the finding.
- Sub-agents → /stories/multiagent/1/. The
tasktool spawns permission-scoped child agents (opencode/packages/opencode/src/tool/task.ts:43-62), but children are first-class persisted sessions with aparentID(opencode/packages/opencode/src/tool/task.ts:142-158), and the recursion guard is a default-denytaskpermission rule, not a code check (opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27). Background tasks (theagent-background-taskspage, not yet built) report back by injecting synthetic user messages. - Skills → /stories/caps/1/. A
skilltool permission-gates loading aSKILL.mdand returns its content (opencode/packages/opencode/src/tool/skill.ts:12-33). - Hooks → /stories/caps/2/. Different organ in the same slot: no shell-command hooks — the extension point is an in-process JS/TS plugin system whose
tool.execute.before/afterfunctions fire viaplugin.trigger(opencode/packages/opencode/src/plugin/index.ts:280-293). - Plan mode → the
cap-plan-modepage (not yet built). Plan mode is nothing but a ruleset: the built-inplanagent denieseditexcept for plan files (opencode/packages/opencode/src/agent/agent.ts:156-181).
Three ideas OpenCode implements that are worth lifting:
-
The doom-loop breaker. The processor watches the last 3 parts of the current assistant message; if all three are tool calls with the same tool and byte-identical input, it forces a
doom_looppermission ask — turning a silent infinite retry into a human checkpoint (opencode/packages/opencode/src/session/processor.ts:29, opencode/packages/opencode/src/session/processor.ts:353-380). Its default action isask(opencode/packages/opencode/src/agent/agent.ts:119-136). -
AST-scanned bash permissions with an arity dictionary. Rather than pattern-matching the raw command string, the shell tool parses the command into a tree, extracts every subcommand, and asks per subcommand (opencode/packages/opencode/src/tool/shell.ts:378-414); the generalized "always allow" pattern comes from an arity table that knows
git checkout mainshould widen togit checkout *whiletouch foowidens totouch *(opencode/packages/opencode/src/permission/arity.ts:1-9). This is the principled version of the scratch cluster'sCHAININGmetacharacter guard. -
Harness-as-server: the approval gate is an HTTP resource. Because the whole agent is a
serve-able HTTP API (opencode/packages/opencode/src/index.ts:93), a pending permission is a server-sideDeferredany client can list and reply to over REST (opencode/packages/opencode/src/permission/index.ts:67-107, opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38). That single decision is what makes multi-frontend and remote attachment fall out for free.
(Bonus, seen in the same source: the edit tool appends LSP diagnostics straight into its own output — "LSP errors detected in this file, please fix:" — so the model fixes type errors in the same turn (opencode/packages/opencode/src/tool/edit.ts:196-201).)
classDiagram
class Clients {
<<HTTP clients>>
+TUI
+desktop
+web
+send prompt over HTTP
+hold open the event stream
+reply to a parked permission
}
class Server {
<<opencode serve the harness>>
+HTTP API
+Effect service layers
+AI SDK owns provider and dispatch
}
class TurnLoop {
<<organ loop>>
+while true
+re-read history from DB each pass
+verdict stop compact continue
}
class ToolDispatch {
<<organ tools>>
+Tool.Def id schema execute
+registry filters per model
+AI SDK invokes execute mid-stream
}
class PermissionGate {
<<organ the gate>>
+tool calls ctx.ask with evidence
+last matching rule wins
+park on a Deferred publish Asked
}
class Context {
<<organ context>>
+isOverflow vs usable minus buffer
+compaction is a queued turn
+filterCompacted at read time
}
class SubAgents {
<<organ sub-agents>>
+task tool spawns a child session
+child permission derived not inherited
+default deny task is the recursion guard
}
Clients --> Server : POST message then GET event then POST reply
Server *-- TurnLoop : runs server-side
TurnLoop --> ToolDispatch : build tool set each step
ToolDispatch --> PermissionGate : ask before any side effect
PermissionGate ..> Clients : Asked event then await HTTP reply
TurnLoop --> Context : compact turn on overflow
ToolDispatch --> SubAgents : task tool
SubAgents ..> TurnLoop : child runs the same loop 读法:OpenCode 把 harness 做成一台 HTTP 服务器
(serve 子命令,opencode/packages/opencode/src/index.ts:93)。
客户端(TUI / desktop / web)只做三件事:POST /session/:sessionID/message 发起一轮、
挂住 GET /event 收事件、必要时 POST /permission/:requestID/reply 回话。
真正的五个器官全在服务端:
循环(while (true),每圈从 DB 重读历史,prompt.ts:1088、prompt.ts:1092-1094)、
工具(Tool.Def + registry,派发外包给 AI SDK,tool.ts:55-65、session/tools.ts:92-134)、
权限门(工具就地 ctx.ask,请求停在服务端 Deferred 上等回话,permission/index.ts:67-107)、
上下文(溢出触发一次排队的压缩轮,读时再用 filterCompacted 改写,overflow.ts:22-34、message-v2.ts:521-572)、
子智能体(task 工具开一个带 parentID 的子会话,tool/task.ts:142-158)。
虚线 PermissionGate ..> Clients 就是那记铃:审批是一次网络往返,而不是进程内的阻塞提示。
同一轮请求 —— 「编辑 config.ts」 —— 一步步走过两套骨架。 Claude Code 是单进程:循环、工具、连审批都在一个进程里。 OpenCode 把 harness 做成一台 HTTP 服务器:UI 只是客户端,循环与审批全在服务端,靠往返通信连起来。 点 ▸ 下一步,看那记"审批"在两边走的是不是同一条路。
GET /eventDeferred 停在这儿,fiber 卡住
看那两条审批:Claude Code 把待批的调用 push 进同一进程内的一个确认队列(interactiveHandler.ts:92),
默认由本地终端应答;但同一次 ask 也可能并行去问远端的 bridge/channel 回调(interactiveHandler.ts:244、316),
谁先 claim() 拿到令牌谁定案(interactiveHandler.ts:70)——即便那趟真走了网络,这台状态机也没离开这一个进程。
OpenCode 里,工具就地 ctx.ask,请求被别在服务端一个 Deferred 上、发一个 Asked 事件,
再由客户端 POST /permission/:requestID/reply 把它解开(permission/index.ts:67-107、109-167)。
正因为循环本身是一台独立于任何客户端的服务器,这次审批才必须是一次网络往返——也正因如此,TUI、桌面端、网页端随便哪个都能应这记铃。
The lay of the land
Citations pinned at opencode@77429f5 (cloned 2026-07-08).
OpenCode bills itself as "The open source AI coding agent" from sst — an MIT-licensed TypeScript monorepo that runs on Bun (opencode/README.md:10). Its ~30 packages split UI from engine, and the split is the whole personality of the thing. The entrypoint is a yargs CLI (opencode/packages/opencode/src/index.ts:45-47) whose subcommands include serve (opencode/packages/opencode/src/index.ts:93): the harness itself is an HTTP server, and the TUI, desktop, and web apps are all clients that attach to it. Where Claude Code is one process that owns its own terminal, OpenCode is a server that owns the agent and lends its screen to whoever connects.
Internally, everything is an Effect service wired as layers — the prompt engine is declared Context.Service("@opencode/SessionPrompt") (opencode/packages/opencode/src/session/prompt.ts:111). Model I/O goes through the Vercel AI SDK's streamText (opencode/packages/opencode/src/session/llm.ts:280), and a code comment spells out the division of labor cleanly: "AI SDK owns provider execution and tool dispatch" (opencode/packages/opencode/src/session/llm.ts:276-277). OpenCode writes the policy — what tools exist, when to compact, who may approve — and rents the mechanics of streaming and tool-call matching from the SDK.
The loop
A user prompt is first persisted as a message row; then prompt() calls loop(), which runs the turn under a per-session run-state lock — loop hands the actual work to SessionRunState.ensureRunning, which keys a Runner per sessionID so a second concurrent call on the same session just awaits the in-flight run's outcome instead of starting a parallel one (opencode/packages/opencode/src/session/prompt.ts:1052-1071, opencode/packages/opencode/src/session/prompt.ts:1343-1347). The engine is a literal while (true) (opencode/packages/opencode/src/session/prompt.ts:1088), and its defining move is that it keeps no conversation in memory. Every single iteration re-reads the session's message history from the database, compaction-filtered, before deciding anything (opencode/packages/opencode/src/session/prompt.ts:1092-1094). The loop's state is the SQL store; control flow is derived from the data, not carried in local variables.
The exit test reads that freshly-loaded history: stop when the newest assistant message finished with a reason other than tool-calls, carries no unresolved tool parts, and is newer than the last user message (opencode/packages/opencode/src/session/prompt.ts:1111-1130). Otherwise the loop takes one "step": it writes a fresh assistant message row (opencode/packages/opencode/src/session/prompt.ts:1186-1201), assembles the tool set and system inputs, and hands an LLM stream to a processor handle (opencode/packages/opencode/src/session/prompt.ts:1272-1286). That SessionProcessor drains the AI-SDK event stream — text-delta, tool-call, tool-result, step-finish — into persisted message parts (opencode/packages/opencode/src/session/processor.ts:627-646) and returns one of three verdicts, "compact" | "stop" | "continue" (opencode/packages/opencode/src/session/processor.ts:30). The loop maps those to break, enqueue a compaction turn, or iterate again (opencode/packages/opencode/src/session/prompt.ts:1319-1329). Crucially, tool execution happens inside the stream — the AI SDK calls each tool's execute itself — so by the time the loop re-enters, results are already rows in the DB waiting to be re-read.
Tools and the gate
A tool is a Tool.Def: an id, a description (usually loaded from a sibling .txt), parameters as an Effect Schema, and execute(args, ctx) returning {title, metadata, output} (opencode/packages/opencode/src/tool/tool.ts:55-65). Tool.define wraps every tool with two universal behaviors: argument validation whose failure becomes a typed, model-facing error — "Please rewrite the input so it satisfies the expected schema" is fed back as the tool result (opencode/packages/opencode/src/tool/tool.ts:24-34) — and automatic output truncation with the overflow spilled to a file (opencode/packages/opencode/src/tool/tool.ts:130-145). The registry assembles the builtins (shell, read, glob, grep, edit, write, task, webfetch, todo, skill, patch…) (opencode/packages/opencode/src/tool/registry.ts:224-247), and SessionTools.resolve converts each Def into a Vercel AI SDK tool({description, inputSchema, execute}), wrapping the real call in tool.execute.before/tool.execute.after plugin hooks (opencode/packages/opencode/src/session/tools.ts:92-134). Dispatch is then entirely the SDK's.
The permission gate is where OpenCode diverges most sharply from Claude Code. Rules are a flat list {permission, pattern, action: allow|ask|deny} and evaluation is last-matching-rule-wins, defaulting to ask (opencode/packages/opencode/src/permission/index.ts:28-38). But the check is pull-based and lives inside each tool: instead of one central pre-execution gate, every tool calls ctx.ask() at the exact moment it knows what it is about to touch, carrying evidence. The edit tool asks with the concrete unified diff in its metadata (opencode/packages/opencode/src/tool/edit.ts:145-153); the shell tool parses the command into a syntax tree, walks every subcommand, and asks with per-command patterns (opencode/packages/opencode/src/tool/shell.ts:378-414). And because the harness is a server, Permission.ask cannot pop a prompt. When any pattern evaluates to ask, it parks the request in a pending map, publishes an Asked event, and blocks the tool's Effect fiber on a Deferred (opencode/packages/opencode/src/permission/index.ts:67-107). The reply arrives over HTTP from whatever client is attached — POST /permission/:requestID/reply calls svc.reply (opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38), which resolves the Deferred; a reject fails it (optionally carrying user feedback), while always appends allow-rules and auto-approves any pending request the new rules now cover, cascading across the session (opencode/packages/opencode/src/permission/index.ts:109-167).
Context and memory
Token accounting is folded from provider-reported usage on every step-finish event (opencode/packages/opencode/src/session/processor.ts:435-484). isOverflow compares the running total against usable — the model's input limit minus a reserved compaction buffer of 20k tokens (opencode/packages/opencode/src/session/overflow.ts:8-20, opencode/packages/opencode/src/session/overflow.ts:22-34). On overflow the processor cuts the stream short and returns "compact", and here is the twist: compaction does not summarize inline. compaction.create writes a synthetic user message carrying a compaction part into the session (opencode/packages/opencode/src/session/compaction.ts:513-536). The next loop iteration finds that queued task and runs it as an ordinary LLM turn — executed by a hidden built-in compaction agent with every tool denied ("*": "deny") (opencode/packages/opencode/src/agent/agent.ts:219-233). Compaction is just another turn the same loop dispatches, run by a locked-down internal agent.
After compaction, history-rewriting is a read-time filter: filterCompacted walks the messages, drops everything a completed summary covers, and retains only [compaction-user, summary, retained tail…] (opencode/packages/opencode/src/session/message-v2.ts:521-572) — and the loop applies it on every iteration (recall opencode/packages/opencode/src/session/prompt.ts:1092-1094). Two cheaper layers run alongside the summarizer: prune erases old completed tool outputs once they pile up, keeping the last turns (opencode/packages/opencode/src/session/compaction.ts:243-287), and every tool's output is already truncated at the source with the full text spilled to a file (opencode/packages/opencode/src/tool/tool.ts:130-145). There is no CLAUDE.md-style auto-loaded memory organ; the analogous instruction files enter as system-prompt inputs assembled per step (opencode/packages/opencode/src/session/prompt.ts:1257-1263).
对照 Claude Code
Same organs, rehoused in a client/server body. Each row maps OpenCode's mechanism to the Claude Code page that dissects the equivalent:
- The loop → /stories/loop/1/. Same "call model → run tools → loop until no tool calls" cycle, but Claude Code's in-memory message array becomes a SQL store re-read every iteration (opencode/packages/opencode/src/session/prompt.ts:1088), and the tool-execution half is delegated to the AI SDK inside one stream (opencode/packages/opencode/src/session/processor.ts:627-646).
- Tool anatomy → /stories/tools/1/. Same name + description-file + schema + execute shape (opencode/packages/opencode/src/tool/tool.ts:55-65), with a typed "rewrite your input" protocol on validation failure (opencode/packages/opencode/src/tool/tool.ts:24-34).
- The permission gate → /stories/tools/2/. Allow/ask/deny with an "always allow" memory, but inverted: the check happens inside the tool (which volunteers its own diff/command as evidence), and approval is an async HTTP round-trip through a server-side
Deferred(opencode/packages/opencode/src/permission/index.ts:67-107) — where Claude Code's resolver is an in-process confirm queue by default, racing an optional remote/bridge channel alongside it (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92). - Compaction → /stories/context/1/. Threshold-triggered, model-written summary — implemented as a queued compaction turn run by a tool-denied internal agent (opencode/packages/opencode/src/agent/agent.ts:219-233), plus reversible output pruning Claude Code's page can contrast.
- Memory → /stories/context/2/. No dedicated auto-load organ; instruction files are system-prompt inputs (opencode/packages/opencode/src/session/prompt.ts:1257-1263) — "no such organ" is the finding.
- Sub-agents → /stories/multiagent/1/. The
tasktool spawns permission-scoped child agents (opencode/packages/opencode/src/tool/task.ts:43-62), but children are first-class persisted sessions with aparentID(opencode/packages/opencode/src/tool/task.ts:142-158), and the recursion guard is a default-denytaskpermission rule, not a code check (opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27). Background tasks (theagent-background-taskspage, not yet built) report back by injecting synthetic user messages. - Skills → /stories/caps/1/. A
skilltool permission-gates loading aSKILL.mdand returns its content (opencode/packages/opencode/src/tool/skill.ts:12-33). - Hooks → /stories/caps/2/. Different organ in the same slot: no shell-command hooks — the extension point is an in-process JS/TS plugin system whose
tool.execute.before/afterfunctions fire viaplugin.trigger(opencode/packages/opencode/src/plugin/index.ts:280-293). - Plan mode → the
cap-plan-modepage (not yet built). Plan mode is nothing but a ruleset: the built-inplanagent denieseditexcept for plan files (opencode/packages/opencode/src/agent/agent.ts:156-181).
What to steal
Three ideas OpenCode implements that are worth lifting:
-
The doom-loop breaker. The processor watches the last 3 parts of the current assistant message; if all three are tool calls with the same tool and byte-identical input, it forces a
doom_looppermission ask — turning a silent infinite retry into a human checkpoint (opencode/packages/opencode/src/session/processor.ts:29, opencode/packages/opencode/src/session/processor.ts:353-380). Its default action isask(opencode/packages/opencode/src/agent/agent.ts:119-136). -
AST-scanned bash permissions with an arity dictionary. Rather than pattern-matching the raw command string, the shell tool parses the command into a tree, extracts every subcommand, and asks per subcommand (opencode/packages/opencode/src/tool/shell.ts:378-414); the generalized "always allow" pattern comes from an arity table that knows
git checkout mainshould widen togit checkout *whiletouch foowidens totouch *(opencode/packages/opencode/src/permission/arity.ts:1-9). This is the principled version of the scratch cluster'sCHAININGmetacharacter guard. -
Harness-as-server: the approval gate is an HTTP resource. Because the whole agent is a
serve-able HTTP API (opencode/packages/opencode/src/index.ts:93), a pending permission is a server-sideDeferredany client can list and reply to over REST (opencode/packages/opencode/src/permission/index.ts:67-107, opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38). That single decision is what makes multi-frontend and remote attachment fall out for free.
(Bonus, seen in the same source: the edit tool appends LSP diagnostics straight into its own output — "LSP errors detected in this file, please fix:" — so the model fixes type errors in the same turn (opencode/packages/opencode/src/tool/edit.ts:196-201).)
来源 · Source citations
- [1]
claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92 - [2]
opencode/README.md:10 - [3]
opencode/packages/opencode/src/index.ts:45-47 - [4]
opencode/packages/opencode/src/index.ts:93 - [5]
opencode/packages/opencode/src/session/prompt.ts:111 - [6]
opencode/packages/opencode/src/session/llm.ts:276-277 - [7]
opencode/packages/opencode/src/session/llm.ts:280 - [8]
opencode/packages/opencode/src/session/prompt.ts:1052-1071 - [9]
opencode/packages/opencode/src/session/prompt.ts:1088 - [10]
opencode/packages/opencode/src/session/prompt.ts:1092-1094 - [11]
opencode/packages/opencode/src/session/prompt.ts:1111-1130 - [12]
opencode/packages/opencode/src/session/prompt.ts:1186-1201 - [13]
opencode/packages/opencode/src/session/prompt.ts:1272-1286 - [14]
opencode/packages/opencode/src/session/prompt.ts:1319-1329 - [15]
opencode/packages/opencode/src/session/prompt.ts:1343-1347 - [16]
opencode/packages/opencode/src/session/processor.ts:30 - [17]
opencode/packages/opencode/src/session/processor.ts:627-646 - [18]
opencode/packages/opencode/src/tool/tool.ts:24-34 - [19]
opencode/packages/opencode/src/tool/tool.ts:55-65 - [20]
opencode/packages/opencode/src/tool/tool.ts:130-145 - [21]
opencode/packages/opencode/src/tool/registry.ts:224-247 - [22]
opencode/packages/opencode/src/session/tools.ts:92-134 - [23]
opencode/packages/opencode/src/permission/index.ts:28-38 - [24]
opencode/packages/opencode/src/permission/index.ts:67-107 - [25]
opencode/packages/opencode/src/permission/index.ts:109-167 - [26]
opencode/packages/opencode/src/tool/edit.ts:145-153 - [27]
opencode/packages/opencode/src/tool/shell.ts:378-414 - [28]
opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38 - [29]
opencode/packages/opencode/src/session/overflow.ts:8-20 - [30]
opencode/packages/opencode/src/session/overflow.ts:22-34 - [31]
opencode/packages/opencode/src/session/processor.ts:435-484 - [32]
opencode/packages/opencode/src/session/compaction.ts:513-536 - [33]
opencode/packages/opencode/src/agent/agent.ts:219-233 - [34]
opencode/packages/opencode/src/session/message-v2.ts:521-572 - [35]
opencode/packages/opencode/src/session/compaction.ts:243-287 - [36]
opencode/packages/opencode/src/session/prompt.ts:1257-1263 - [37]
opencode/packages/opencode/src/agent/agent.ts:38 - [38]
opencode/packages/opencode/src/agent/agent.ts:119-136 - [39]
opencode/packages/opencode/src/agent/agent.ts:156-181 - [40]
opencode/packages/opencode/src/tool/task.ts:43-62 - [41]
opencode/packages/opencode/src/tool/task.ts:104-114 - [42]
opencode/packages/opencode/src/tool/task.ts:142-158 - [43]
opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27 - [44]
opencode/packages/opencode/src/tool/skill.ts:12-33 - [45]
opencode/packages/opencode/src/plugin/index.ts:280-293 - [46]
opencode/packages/opencode/src/session/processor.ts:29 - [47]
opencode/packages/opencode/src/session/processor.ts:353-380 - [48]
opencode/packages/opencode/src/permission/arity.ts:1-9 - [49]
opencode/packages/opencode/src/tool/edit.ts:196-201