Repository navigation
Description
Description
A write: block in opencode.json or in agent frontmatter parses without error, loads into the ruleset, and is never consulted by anything.
write is not a declared permission key. packages/core/src/v1/config/permission.ts:18-34 declares:
read · edit · glob · grep · list · bash · task · external_directory
todowrite · question · webfetch · websearch · lsp · doom_loop · skill
The struct has a Schema.Record(Schema.String, Rule) rest element, so any unknown key parses cleanly and is carried into the ruleset by fromConfig. Nothing then asks under it.
The write tool asks under edit:
// packages/opencode/src/tool/write.ts:55-57
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(instance.worktree, filepath)],There is no permission: "write" anywhere in src.
The practical effect is that a config like this does not do what it reads as:
{
"permission": {
"edit": { "*": "allow" },
"write": { "*.env": "deny" }
}
}The write block is inert; writes are governed entirely by the edit rules. An operator who tightens write and leaves edit open has not tightened anything.
This also means unknown keys generally are silent no-ops — a typo like edt: or bahs: parses, loads, and does nothing, with no warning at any level.
Steps to reproduce
- Configure
"permission": { "edit": { "*": "allow" }, "write": { "*": "deny" } }. - Have the agent write a file.
- The write is allowed. The
writedeny never participates. - Nothing is logged about the unused key.
The evaluated log line at permission/index.ts:74 confirms it directly: writes appear as permission=edit, and permission=write never appears at all. An independent deployment checked 125,800 evaluated lines and found zero permission=write entries.
OpenCode version
dev @ 1882c33
Expected
One of:
- Reject unknown permission keys at parse time. Preferred — it makes this class of mistake loud, and it catches typos as a side effect. It is a breaking change for any config carrying an undeclared key, so it wants a release note.
- Make
write.tsask underpermission: "write"and document the key. Existingwrite:blocks become live as written, which is what their authors intended, but anyone relying oneditrules covering writes would see a behaviour change.
Either way write should appear in docs/permissions.mdx — as a supported key, or in the list of keys that do not exist.
Notes
Found while working on #30551 / #40149 (absolute permission patterns not matching outside the worktree). Filed separately because the fixes are unrelated: that one changes which path is matched, this one changes which key is consulted.
Two deployments were carrying write: blocks written in good faith that had never done anything.
Activity
Santoshkumarpuppala commentedon Sep 3, 2026
Confirmed on
dev—tool/write.ts:55asks withpermission: "edit", and the onlypermission: "write"anywhere is a test fixture. The diagnosis holds. Three things that bear on the fix rather than the finding.Option 1 would reject a documented feature. The config key is not a lookup key —
Permission.evaluatedoesWildcard.match(permission, rule.permission)(packages/opencode/src/permission/index.ts:32, and again at:77and:210), and every MCP or custom tool asks under its own name. That is documented:agents.mdx:453says permission keys "are matched as wildcard patterns against the underlying tool name… for example"mymcp_*": "deny"". So theSchema.Record(Schema.String, Rule)rest element atpackages/core/src/v1/config/permission.ts:35is load-bearing, and closing it at parse time would break every config gating an MCP tool."mymcp_search": "ask"has no glob metacharacter, so it is not distinguishable fromedt:by shape.That does not make the typo problem unreal —
edt:still silently does nothing. It means the remedy is a diagnostic for keys matching no registered tool name, not a parse-time allowlist.Option 2 has a second call site.
writeis remapped in two places: the ask attool/write.ts:55, andpermission/index.ts:205—const edits = ["edit", "write", "apply_patch"]— which feeds the path deciding which tools are hidden from the model. Changing onlywrite.tsmakes"write": {"*": "deny"}start prompting while tool-hiding stays keyed onedit. Half-live is worse than inert, because the config then appears to work.listis the same defect and survives the preferred fix. It is declared atpackages/core/src/v1/config/permission.ts:23, documented atagents.mdx:438as gating a tool calledlist, and still has a prompt renderer. There is nolisttool:packages/opencode/src/tool/contains apply_patch, edit, glob, grep, lsp, read, shell, skill, task, todo, webfetch, websearch, write and friends — nolist.ts. I control-tested that enumeration against glob, grep, lsp, skill and websearch, all of which resolve, so it finds rare positives. So"list": {"*": "deny"}is inert and declared, and a strict-key gate waves it through — which is the argument that the property making a key inert is reachability, not declaredness.One note on the 125,800-line check, which is good evidence here: the
evaluatedlog is emitted inside the ask path, whilePermission.disabled(:204-219) consumes the same ruleset and logs nothing. Forwriteboth remap toeditso the conclusion stands, but a key can be dead in one consumer and live in the other — which is why that method findswriteand notlist.Same shape from the other side, for whatever it is worth: in an MCP enforcement point I maintain, the integrity check pins an enumerated allowlist of six tool fields, so anything the spec adds later ships unpinned and silently uncovered. The schema vouches for coverage that reachability does not provide, which is
listexactly.opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up!
What you're seeing is how it's designed to work: the permissions docs say that
editcovers all file changes (edit,writeandpatch). So thewritetool asking undereditis expected, and there is no separatewritekey. Other keys are accepted on purpose, probably so rules can apply to permissions outside the core list, such as MCP or custom tool names.You're right that it's easy to get caught by this, though: a
write:block, or a typo likeedt:orbahs:, does nothing and gives no warning. I've filed this as a feature request. Options include a warning for keys that never match anything, a specific hint thatwriteis covered byedit, or a realwritepermission. Maintainers will consider it.For now, to restrict writes, put the rules under
edit:{ "permission": { "edit": { "*": "allow", "*.env": "deny" } } }Related: #30551 and #40149 (absolute permission patterns outside the worktree), which you already linked and which are a separate problem.
neriousy commentedon Nov 13, 2026
Closing, since this is intended in V2 and the config no longer ignores
write:rules. The write tool asks underediton purpose, and the docs sayeditcoversedit,writeandpatch(see the Permissions page and the V1 migration guide). Legacywrite,patchandapply_patchkeys inpermission,toolsand agent config are migrated toedit, sowrite: { "*.env": "deny" }is enforced as aneditdeny. Warnings for unknown permission keys are tracked in #46757.