Summary
The permission-glob matcher in opencode 1.14.30 doesn't match patterns of the form **/<segment>/<segment>/<dir>/** against absolute resolved paths like /private/tmp/<segment>/<segment>/<dir>/<file>. Only single-segment patterns like **/<basename>/** match reliably. Patterns starting with a literal / (e.g. /tmp/scope/**) also don't match.
This makes it impossible to express "allow exactly this absolute path subtree" precisely — you have to fall back to **/<basename>/** which over-matches if the basename appears elsewhere in the filesystem.
Environment
- opencode 1.14.30 (curl install, macOS 25.3.0)
- Permission engine in
OPENCODE_CONFIG_CONTENT JSON
Reproduce
mkdir -p /tmp/agent-out
rm -f /tmp/agent-out/x.txt
CFG_FAILS='{"permission":{"edit":{"*":"deny","**/private/tmp/agent-out/**":"allow"},"write":{"*":"deny","**/private/tmp/agent-out/**":"allow"},"read":"deny","bash":"deny","external_directory":"allow"},"tools":{"bash":false,"edit":true,"write":true,"web_fetch":true,"web_search":true}}'
echo "Use the write tool to create /tmp/agent-out/x.txt with content X. Reply DONE." | \
OPENCODE_CONFIG_CONTENT="$CFG_FAILS" opencode run --model <any-model> --dangerously-skip-permissions
ls /tmp/agent-out/x.txt # ❌ does not exist
Same setup but with **/agent-out/** instead of **/private/tmp/agent-out/**:
CFG_WORKS='{"permission":{"edit":{"*":"deny","**/agent-out/**":"allow"},"write":{"*":"deny","**/agent-out/**":"allow"},...}}'
# ✅ writes the file
Tested patterns (all targeting /private/tmp/agent-out/x.txt):
| Pattern |
Result |
/private/tmp/agent-out/** |
❌ blocked |
private/tmp/agent-out/** |
❌ blocked |
**/private/tmp/agent-out/** |
❌ blocked |
**/tmp/agent-out/** |
❌ blocked (mostly; sometimes flaky) |
**/agent-out/** |
✅ matches reliably |
**/agent-out/* |
✅ matches reliably |
The agent-side error message confirms the rule IS in opencode's evaluated rules list, just doesn't match:
"relevant rules [{...},{"permission":"edit","pattern":"*","action":"deny"},
{"permission":"edit","pattern":"**/private/tmp/agent-out/**","action":"allow"}]"
Expected
Per the docs (https://opencode.ai/docs/permissions/), ** should match any number of path segments and last-match-wins. So **/private/tmp/agent-out/** should match /private/tmp/agent-out/x.txt and the allow should override the catch-all deny.
Suggested investigation
- The matcher likely doesn't anchor
** correctly when the path starts with /. Maybe it's stripping the leading / from the input path but then the pattern's leading **/ requires non-empty segments, and private/tmp/... has zero segments before private from the matcher's POV.
- Or the matcher treats multiple
** differently from one — **/dir/** works, **/seg/dir/** doesn't.
- Test against Bun's built-in
Glob.match if that's what's being used; or wherever the rule resolution happens in src/permission/.
Workaround
Use **/<basename>/** in the config and enforce the actual path scope before spawning opencode (we validate against an ALLOWED_ROOTS list at the spawn boundary, then trust opencode for defense-in-depth).
Impact
For folks building MCP wrappers / sandboxes around opencode (e.g. our sub-agent-mcp), this means opencode's permission engine can't be the primary security gate for path scoping — you have to validate paths in your own code first. That's a reasonable defense-in-depth posture but probably not what the docs imply.
Summary
The permission-glob matcher in opencode 1.14.30 doesn't match patterns of the form
**/<segment>/<segment>/<dir>/**against absolute resolved paths like/private/tmp/<segment>/<segment>/<dir>/<file>. Only single-segment patterns like**/<basename>/**match reliably. Patterns starting with a literal/(e.g./tmp/scope/**) also don't match.This makes it impossible to express "allow exactly this absolute path subtree" precisely — you have to fall back to
**/<basename>/**which over-matches if the basename appears elsewhere in the filesystem.Environment
OPENCODE_CONFIG_CONTENTJSONReproduce
Same setup but with
**/agent-out/**instead of**/private/tmp/agent-out/**:Tested patterns (all targeting
/private/tmp/agent-out/x.txt):/private/tmp/agent-out/**private/tmp/agent-out/****/private/tmp/agent-out/****/tmp/agent-out/****/agent-out/****/agent-out/*The agent-side error message confirms the rule IS in opencode's evaluated rules list, just doesn't match:
Expected
Per the docs (https://opencode.ai/docs/permissions/),
**should match any number of path segments and last-match-wins. So**/private/tmp/agent-out/**should match/private/tmp/agent-out/x.txtand the allow should override the catch-all deny.Suggested investigation
**correctly when the path starts with/. Maybe it's stripping the leading/from the input path but then the pattern's leading**/requires non-empty segments, andprivate/tmp/...has zero segments beforeprivatefrom the matcher's POV.**differently from one —**/dir/**works,**/seg/dir/**doesn't.Glob.matchif that's what's being used; or wherever the rule resolution happens insrc/permission/.Workaround
Use
**/<basename>/**in the config and enforce the actual path scope before spawning opencode (we validate against anALLOWED_ROOTSlist at the spawn boundary, then trust opencode for defense-in-depth).Impact
For folks building MCP wrappers / sandboxes around opencode (e.g. our
sub-agent-mcp), this means opencode's permission engine can't be the primary security gate for path scoping — you have to validate paths in your own code first. That's a reasonable defense-in-depth posture but probably not what the docs imply.