Skip to content

Permission glob matcher doesn't match deep absolute paths; only **/<basename>/** works #25097

Description

@bmeindl

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions