把开源终端 Agent opencode 铺给团队:策略层锁得住什么、锁不住什么
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
把 opencode 铺给一个团队,你能真正锁住的只有配置文件里表达得出来的那部分行为;而这份配置放在哪一层,比你在里面写了什么更要紧。 同一段 permission 规则,写进项目仓库里是一条约定,写进系统级的托管目录里才是一道硬约束——中间这段差别,就是”个人自用”和”团队铺开”的全部落差。
opencode 是一个跑在终端里的开源编码 Agent,采用 MIT 许可证(LICENSE,Copyright 2025 opencode)。它会在你的机器上执行 shell 命令、直接改写工作区里的文件、把代码内容当上下文发给模型服务商。这三件事个人自用时你自己扛,铺到几十个人身上就得有默认值、有兜底、有人为它负责。
本篇只谈 opencode 这一个项目的策略层机制,逐条对着仓库里的文档和源码讲。要看跨工具的团队治理框架(多工具并存怎么定规矩、怎么审计),去读AI 工具的团队治理;要看另一个终端 Agent 的团队实践细节,去读Claude Code 团队实践;要看权限模型本身的抽象设计(三态、最小权限、升降级路径),去读AI 权限模型。这三篇讲的是”该怎么想”,本篇讲的是”在 opencode 里怎么写、写完到底管不管用”。
一、团队铺开要管的是三条通道
先把要管的东西说清楚,不然容易只盯着一个 JSON 键。
第一条是本机执行面。opencode 会跑 shell、会改文件。误删误改是最直接的代价:一条 rm -rf 的参数写歪,一次批量重构把你没让它碰的目录也改了。个人自用时这类事故的爆炸半径是你自己的工作区,团队铺开时是每一台开发机。
第二条是数据外发面。代码内容作为上下文送给模型服务商,这是这类工具工作的前提,不是可选项。问题不在”发不发”,在”发给谁”。仓库里的企业文档把话说得很直:opencode 本身不存储你的代码或上下文数据,处理要么发生在本地,要么通过对你的 AI 服务商的直接 API 调用完成——所以只要你信任的服务商是被约束住的,这条通道就是可控的。
第三条是会话外发面,也就是分享功能。这是企业文档里唯一被点名的例外:一旦用户启用了 /share,会话内容和相关数据会被发送到 opencode.ai 上托管这些分享页的服务,并通过 CDN 的边缘网络分发、在靠近用户的边缘缓存。分享出去的会话对任何拿到链接的人都是公开可访问的。文档给试用阶段的建议是直接关掉它。
这三条通道对应的开关不在同一个地方,这是理解后面所有内容的前提:执行面归 permission,服务商归 experimental.policies,分享归顶层的 share 键。
二、permission:锁得住的是”工具在什么输入上能干什么”
permission 是 opencode 决定一个动作该自动执行、该弹窗问你、还是该直接拦掉的地方。每条规则解析成三个值之一:"allow"(不经批准直接跑)、"ask"(弹窗请求批准)、"deny"(拦截)。
权限按工具名索引,文档列出的键包括:read(读文件,匹配文件路径)、edit(所有文件修改,覆盖 edit、write、patch)、glob(匹配 glob 模式)、grep(匹配正则)、bash(跑 shell 命令,匹配的是解析后的命令,例如 git status --porcelain)、task(启动子 Agent,匹配子 Agent 类型)、skill(加载技能,匹配技能名)、lsp(目前不支持细粒度)、question(执行过程中向用户提问)、webfetch(匹配 URL)、websearch(匹配查询词),外加两个安全护栏:external_directory(工具碰到项目工作目录之外的路径时触发)和 doom_loop(同一个工具调用带着完全相同的输入重复三次时触发)。
默认值是偏宽松的:大部分权限默认 "allow";doom_loop 和 external_directory 默认 "ask";read 默认 "allow",但 .env 文件默认被拒绝。这一点值得单独强调——默认状态下,一个刚装完什么都没配的 opencode,是可以直接改你的文件、直接跑命令的。团队铺开时”不配置”本身就是一种配置,而且是最松的那种。
细粒度靠对象语法,按输入模式匹配,最后一条匹配上的规则胜出。文档里的示例是这样的:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "ask",
"git *": "allow",
"npm *": "allow",
"rm *": "deny",
"grep *": "allow"
},
"edit": {
"*": "deny",
"packages/web/src/content/docs/*.mdx": "allow"
}
}
}
通配符只有两个:* 匹配零到多个任意字符,? 匹配恰好一个字符,其余字符按字面匹配。模式开头可以用 ~ 或 $HOME 指代主目录。
external_directory 这条护栏专门管越界:任何接收路径作为输入的工具(read、edit、glob、grep,以及很多 bash 命令)碰到工作目录之外的路径时都会触发它。这里有个容易误解的点,文档专门写了一句——主目录展开只影响模式的写法,它不会让一个外部路径变成当前工作区的一部分,工作目录之外的路径仍然必须通过 external_directory 放行。而且被放行的目录会继承当前工作区的同一套默认值,read 默认是 allow,所以放行之后读也跟着开了,想只读不写就得再补一条 edit 的 deny。
弹窗那一刻的三个选项是 once(只批准这次)、always(在当前这个 opencode 会话的剩余时间里批准匹配建议模式的后续请求)、reject。always 会批准哪些模式由工具自己给出,例如 bash 的批准通常会把一个安全的命令前缀列进白名单。这个”仅限当前会话”的时效性很关键:它是给个人省事的,不是给团队定规矩的。
--auto 是另一个层次的开关。用 opencode --auto 启动,或者在 opencode run --auto 里用,会自动批准那些本来要问你的权限请求;显式的 "deny" 规则仍然被强制执行,auto 模式只改变那些原本会弹窗的请求。TUI 里也能从命令面板切换(Enable auto-approve permissions / Disable auto-approve permissions),auto 生效时提示符旁边会有一个灰色的 auto 指示。
权限还能按 Agent 覆写。Agent 权限与全局配置合并,Agent 规则优先。内置的两个主 Agent 里,Plan 就是靠这套机制做成受限形态的——文件编辑和 bash 默认都是 ask。Agent 也可以写成 Markdown 文件,在 frontmatter 里直接写 permission,让”只读评审”这类角色变成一个可分发的文件。
三、policies:锁的是”哪家服务商能用”,不是”工具能干什么”
experimental.policies 是另一条正交的线。文档把边界划得很清楚:权限控制会话期间工具能做什么,策略控制 opencode 是否可以使用某个资源,比如一个 LLM 服务商。这个特性目前标注为实验性。
每条策略语句三个字段:effect("allow" 或 "deny")、action(被控制的操作)、resource(资源 ID 或通配模式)。目前只支持一个 action:provider.use,资源是服务商 ID。被策略拒绝的服务商不会出现在模型选择里,也不能被使用——即便它凭据齐全、配置完全正确。
匹配同样支持 * 和 ?,同样是最后一条匹配的语句胜出,因此”只允许某一家”要写成先全拒后单放:
{
"$schema": "https://opencode.ai/config.json",
"experimental": {
"policies": [
{
"effect": "deny",
"action": "provider.use",
"resource": "*"
},
{
"effect": "allow",
"action": "provider.use",
"resource": "anthropic"
}
]
}
}
没有任何策略匹配上的服务商,默认是允许的。文档还建议用策略取代旧的 disabled_providers 和 enabled_providers 设置来控制服务商访问。
这里有一个和权限完全相反的优先级方向,是最值得记住的一条:策略可以同时写在全局配置和项目配置里,如果两处的策略匹配到同一个服务商,全局策略优先于项目策略——这样一个代码仓库就没法重新启用你在全局层面拒绝掉的服务商。源码里能看到这个反转是怎么实现的:packages/core/src/config.ts 在把各份配置里的策略语句喂给策略服务之前,先把配置顺序整个倒过来(注释写的是”规则使用相反的顺序,好让用户全局规则覆盖仓库规则,每份文件内部的语句顺序保持不变”);求值本身在 packages/core/src/policy.ts 里,是对语句数组取最后一条匹配项的效果值。
倒序 + 取最后匹配,两个动作合起来的效果就是:越靠近用户全局的那份配置,在策略上话语权越大。而普通的 permission 走的是常规合并方向——项目配置覆盖全局配置。同一个 JSON 文件里的两个键,优先级方向是反的,这件事不看文档和源码是猜不出来的。
四、规则放在哪一层,决定它是建议还是硬约束
opencode 的配置不是替换关系,是合并关系:多份配置组合到一起,后面的只在冲突的键上覆盖前面的,不冲突的设置全部保留。加载顺序(后面覆盖前面)文档列了八层:远端配置(来自 .well-known/opencode,组织默认值)、全局配置(~/.config/opencode/opencode.json)、自定义配置(OPENCODE_CONFIG 环境变量)、项目配置(项目里的 opencode.json)、.opencode 目录、内联配置(OPENCODE_CONFIG_CONTENT 环境变量)、托管配置文件、以及 macOS 托管偏好设置。
最后两层才是团队真正能倚重的东西。托管配置文件放在需要管理员权限才能写入的系统目录里:macOS 是 /Library/Application Support/opencode/,Linux 是 /etc/opencode/,Windows 是 %ProgramData%\opencode。macOS 上还能从 ai.opencode.managed 偏好域读取托管偏好,通过 MDM 下发 .mobileconfig 自动强制生效,plist 的键直接映射到 opencode.json 的字段。文档的原话是:所有托管偏好键都会出现在解析后的配置里,且不能被用户或项目配置覆盖。验证手段是 opencode debug config。
把上面这些落到一张表上:
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
permission 规则 | 工具能不能跑、要不要问、按输入模式细分 | packages/web/src/content/docs/permissions.mdx | 第一天定默认值,以及每次有人抱怨弹窗太多 |
external_directory 护栏 | 工具碰到工作目录之外的路径时拦一道 | packages/opencode/src/tool/external-directory.ts | 开发者同时开着多个仓库、或者引用主目录下的脚本时 |
experimental.policies | 某个 LLM 服务商能不能被用 | packages/web/src/content/docs/policies.mdx、packages/core/src/config/experimental.ts | 要求所有请求只走内部网关时 |
| 策略求值 | 通配匹配后取最后一条命中语句 | packages/core/src/policy.ts | 排查”为什么我 allow 了却还是不能用” |
| 配置合并与倒序 | 决定全局与项目谁覆盖谁 | packages/core/src/config.ts | 发现 permission 和 policies 优先级方向相反时 |
| 已保存的批准记录 | 按项目留存批准过的动作与资源 | packages/core/src/permission/saved.ts、packages/core/src/permission/sql.ts | 想搞清”批准过的东西到底记到哪儿”时 |
| 托管配置与优先级链 | 哪一层配置不可被用户覆盖 | packages/web/src/content/docs/config.mdx | 从”团队约定”升级到”强制策略”时 |
| 分享开关 | 会话内容会不会离开你的网络 | packages/web/src/content/docs/share.mdx | 合规评审提问的第一分钟 |
| 代理与自签证书 | 出网怎么走、CA 怎么信 | packages/web/src/content/docs/network.mdx | 装在内网机器上第一次连不上模型时 |
企业形态那份文档给的路径是:先在团队内部做试用,因为项目开源、默认不存储代码与上下文数据,开发者可以直接开始试;确认之后再联系官方讨论实施方案。落地形态的核心是一份组织级的集中配置,它可以对接你的 SSO 提供方、通过既有身份管理体系拿到访问内部 AI 网关的凭据,并且可以配置成只用内部网关、把其它所有 AI 服务商全部禁用。代码归属这条文档写得很短:你拥有 opencode 产出的所有代码,没有授权限制或所有权主张。分享页的自托管在文档里标注为路线图上的事项,没有给时间表。
内网环境还有两个绕不开的环境变量:代理走标准的 HTTPS_PROXY / HTTP_PROXY,自签 CA 用 NODE_EXTRA_CA_CERTS 指向证书文件。私有 npm 源方面,文档说明 opencode 通过 Bun 原生的 .npmrc 支持私有 registry,开发者必须在运行前先登录,否则包装不上。
五、边界与代价:这套设计明确不管的事
它不是沙箱。 权限规则匹配的是字符串模式,不是命令语义。"rm *": "deny" 拦的是解析出来长成那样的命令,不是”所有会删文件的行为”。一旦你把 bash 开成 allow,其它所有工具键上的 deny 就基本失去意义了——shell 能做的事是那些细粒度规则的超集。想要真正的隔离,得靠容器、靠独立的低权限账号、靠工作区隔离,那是 opencode 之外的事。这个取舍换来的是低摩擦:不需要额外的运行时,装上就能在真实工作区里干活。
策略层目前很窄。 experimental.policies 只有 provider.use 一个 action,文档明说未来可能会加更多。也就是说:你能锁”哪家服务商能用”,但锁不了”用哪个具体模型”、锁不了 MCP 服务器的取用、锁不了花销上限。带预算和额度的那套治理得在网关侧做,不要指望这一层。
外部工具是策略层没盖住的一条口子。 MCP 服务器写在配置的 mcp 键下,文档里说得很明白:加进来之后,这些工具会和内置工具一起自动提供给模型使用。文档给的注意事项只谈上下文开销(工具一多就占满上下文,某些服务器尤其能吃 token),没有讲信任问题——但对团队来说,信任才是重点:一个 MCP 服务器背后是一段别人写的代码,它拿到的调用意图来自模型,而权限文档那份工具键清单里也没有为它们单列条目。插件同理,插件文档里能看到 permission.asked、permission.replied 这类事件钩子,插件能观察到批准流程本身,就说明它跑在你这台机器的信任边界之内。铺开时的稳妥做法是:MCP 服务器地址与插件来源和红线规则放在同一层管理,别让任何人从项目仓库里随手引入一个没人看过的服务器。
配置在仓库里就等于可被修改。 项目里的 opencode.json 优先级高于全局配置,这对开发者体验是好事,对强制策略是坏事——任何有仓库写权限的人都能改它。真正不可篡改的只有系统托管目录和 MDM 那两层,代价是你得有终端管理能力。策略这一条因为优先级方向被反转过,是个例外:全局的拒绝,仓库改不回来。
这几份文档没有描述集中审计与团队侧报表。 讲的都是本机配置与资源开关。谁在什么时候批准了哪条危险命令、某段私有代码有没有被送出去过,这些取证需求要从别处解决——网关日志、终端管控、代码仓库审计。别把配置文件当合规系统用。
分享功能是一条主动的外发通道。 默认是手动模式,不会自动分享,但只要有人敲了 /share,会话就上公网了。/unshare 能停掉分享并删除相关数据,可这是事后动作。团队环境的稳妥做法是在不可被覆盖的层级把它设成 "disabled"。
六、上手与避坑清单
把配置放进仓库就以为锁住了。 会踩是因为项目配置在 permission 上确实覆盖全局,看起来”生效了”,但它同时也对每个开发者开放着编辑权——改一行就绕过去了,而且这种绕过不会留下任何提示。怎么避:把可协商的默认值放仓库,把不可协商的红线放系统托管目录或 MDM,下发后逐台跑一次 opencode debug config,确认红线出现在解析结果里。
规则顺序写反。 会踩是因为直觉上”通用规则兜底应该写最后”,但这里是最后一条匹配的规则胜出,把 "*" 放末尾会把上面所有细规则全冲掉。怎么避:catch-all 永远写第一行,具体规则往后排,写完拿两三条真实命令走一遍验证。
策略和权限的优先级方向反着来。 会踩是因为你在同一个 JSON 文件里写这两块,会默认它们遵守同一套覆盖顺序。怎么避:记住权限是项目盖全局、策略是全局盖项目,把服务商白名单写进用户全局配置而不是仓库,这样仓库改不动它。
模式漏了参数。 会踩是因为 "grep" 和 "grep *" 不是一回事:文档提示带参数的命令要用模式匹配,"grep *" 允许 grep pattern file.txt,光写 "grep" 会把它挡掉;git status 这类命令按默认行为是能工作的,但带上参数就需要显式写成 "git status *"。怎么避:所有 bash 规则一律带上尾部通配,改完真跑几条带参数的命令确认。
--auto 被当成省事开关。 会踩是因为弹窗确实烦,有人图快就一直带着 auto 跑。它会把所有本来要问的请求自动通过,此时唯一还拦得住的只剩显式 "deny"。怎么避:开 auto 之前先把 deny 清单补齐(至少覆盖删除、推送、凭据文件),并且只在隔离环境或明确低风险的批处理任务上用。
external_directory 一路点 always。 会踩是因为这条护栏默认就是 ask,多仓库并行时弹得频繁,很多人顺手点到底。怎么避:把真正需要的可信路径显式列进 external_directory 的允许列表,同时给这些路径单独补 edit 的 deny——文档里给的正是”放行读、挡住写”这个组合;另外别忘了主目录展开只是写法上的便利,不会让外部路径变成工作区的一部分。
以为 .env 默认被拒绝就万事大吉。 会踩是因为权限是按工具键判定的,read 上对 .env 的拒绝约束的是读文件这个工具,它不会自动约束 shell 那条路径。怎么避:凭据文件的防护要在 bash 规则里同步表达,同时把密钥从工作区里挪走——最稳的还是让机器上根本没有明文密钥。
内网装完连不上,或者 TUI 卡死。 会踩是因为 TUI 是和一个本地 HTTP 服务通信的,配了代理却没把本地地址排除,请求会绕回代理形成路由环。怎么避:设代理时必须同时设 NO_PROXY 并包含 localhost,127.0.0.1;公司用自签 CA 的话再配上 NODE_EXTRA_CA_CERTS。
收尾给一张自检表,铺开前把这七项逐条打勾:红线规则是否落在用户改不动的层级、catch-all 是否在每组规则的第一行、服务商白名单是否写在全局而非仓库、share 是否已在不可覆盖层关闭、--auto 的使用场景是否被书面限定、凭据文件在 read 和 bash 两条路径上是否都被挡住、以及有没有一台机器实际跑过 opencode debug config 验证过解析结果。
接下来该读哪些文件也很清楚:优先级链和托管设置看 packages/web/src/content/docs/config.mdx,Agent 级覆写和内置 Plan Agent 的默认值看 agents.mdx,内网代理与证书看 network.mdx。想弄明白某条规则为什么没生效,直接读 packages/core/src/policy.ts 和 packages/core/src/config.ts 这两个文件比翻文档快——前者是求值,后者是合并顺序,问题基本都出在这两处之一。
这类工具的治理,难的从来不是写出一份完美的规则文件,而是让规则待在开发者改不动、也不想改的位置上。做 Agent 权限设计时的通用取舍可以参考最小权限设计,团队里权限放太松之后怎么收回来可以参考权限太大的处理。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 开源项目 opencode 怎么把一个 Agent 内核挂上四五套界面 和 开源终端编码 Agent 项目 opencode 的安全边界:它明说不做沙箱,你该在外面补什么。