Skip to content

Instantly share code, notes, and snippets.

@altendky
Created March 8, 2026 03:35
Show Gist options
  • Select an option

  • Save altendky/60dba5fa32f0a7e8b40b021007d33dc2 to your computer and use it in GitHub Desktop.

Select an option

Save altendky/60dba5fa32f0a7e8b40b021007d33dc2 to your computer and use it in GitHub Desktop.

How opencode Path Permission Values Are Processed

The Wildcard-to-Regex Pipeline

All permission pattern matching flows through a single function: Wildcard.match().

The conversion steps are:

  1. Normalize backslashes\/ on both input and pattern (lines 5–6)
  2. Escape all regex metacharacters.+^${}()|[]\ are escaped so they match literally (line 8)
  3. Convert *.* — matches zero or more of any character, including / (line 9)
  4. Convert ?. — matches exactly one character (line 10)
  5. Trailing space+wildcard optimization — a trailing .* (from * in the pattern) becomes ( .*)?, making trailing arguments optional. This lets "ls *" match both "ls" and "ls -la" (lines 14–16)
  6. Anchor the regex — the result is wrapped as ^...$ with s (dotAll) flag; on Windows, the i flag is also added (line 19)

There is no raw-regex escape hatch. Because step 2 escapes all regex metacharacters before step 3 converts wildcards, there is no way to pass through raw regex syntax. The only special characters are * and ?.

Rule Evaluation: Last Match Wins

Rules are evaluated via .findLast() on the merged ruleset. Both the permission type and the pattern are wildcard-matched:

const match = merged.findLast(
  (rule) =>
    Wildcard.match(permission, rule.permission) &&
    Wildcard.match(pattern, rule.pattern),
)

User config is merged after defaults, so user rules always take priority.

How Rules Are Built from Config

PermissionNext.fromConfig() converts config entries into a ruleset:

  • A string value (e.g. "allow") becomes {pattern: "*", action: value}
  • An object value creates one rule per (pattern, action) pair

Before matching, expand() expands ~/ and $HOME/ prefixes to the actual home directory.

Default Permissions

Default rules are constructed per-agent. Notable defaults include:

  • "*": "allow" — allow everything by default
  • "read" > "*.env": "ask" and "*.env.*": "ask" — prompt for .env files
  • "external_directory" > "*": "ask" — prompt for paths outside the project

Pattern Semantics Vary by Tool

The value being matched against your permission pattern differs depending on which tool is requesting permission:

Tool Permission Pattern value Source
read "read" Absolute file path read.ts:47
edit "edit" Relative path from worktree edit.ts:57, edit.ts:88
write "edit" Relative path from worktree write.ts:36
apply_patch "edit" Relative paths from worktree apply_patch.ts:175–178
bash "bash" Command text bash.ts:159–161
external_directory "external_directory" parentDir/* glob bash.ts:150–153
glob "glob" The glob pattern glob.ts:24
grep "grep" The search pattern grep.ts:29
webfetch "webfetch" The URL webfetch.ts:29

Key Implication

edit/write/apply_patch convert absolute paths to relative paths via path.relative(Instance.worktree, filePath) before permission matching. So edit permission patterns should match paths relative to the worktree root (e.g. src/*.ts), not absolute paths.

read, on the other hand, passes the absolute file path. So read permission patterns need to match absolute paths (e.g. /home/user/project/*.env), or use * liberally to cover the prefix.

Where Exactly Edit Paths Become Relative

Summary

  • Only * (any characters) and ? (one character) are special in patterns
  • All regex metacharacters are escaped — no raw regex mode exists
  • ~/ and $HOME/ are expanded before matching
  • Last matching rule wins (user config overrides defaults)
  • edit patterns match relative paths; read patterns match absolute paths
  • Linked to commit 2b8acfa on anomalyco/opencode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment