You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The write tool prompts for permission on /tmp/ paths even when the config has "*/tmp/*": "allow". The bash tool with "python3 *": "allow" writes to the same path without prompting, confirming the permission system works — just not for the write tool.
Issue any write tool call targeting /tmp/test.txt.
Observe prompt despite */tmp/*: allow in config.
Expected
write tool auto-allows writes to /tmp/ matching */tmp/*: allow.
Actual
Prompts every time. Manually approving the prompt succeeds.
Diagnosis
The root cause appears to be the same as #20045: the write tool passes a relative path (relative to Instance.worktree) to the permission matcher, while patterns in the config are written as absolute-style patterns (*/tmp/*). When Instance.worktree is / (non-git project) or when the resolved relative path doesn't anchor the way the globstar expects, the pattern never matches.
A recent commit (c2609cb) added a Global.Path.tmp exemption for agents, which suggests the team is already aware of this class of issue.
Environment
opencode version: 1.15.x (latest as of 2026-06-03)
macOS
Config loaded from ~/.config/opencode/opencode.jsonc
If your issue is specifically about the write tool being affected (while #20045 focuses on edit), it may be worth keeping open to track that gap — but please confirm whether #20045 or #25097 already covers your case before proceeding.
Your diagnosis in the issue body is correct, and I've opened a PR against it: #40149.
Adding the evidence I gathered, because this has been filed four other times (#20045, #22336, #22465, #25097) and closed unfixed each time. Every one of those reported it as ergonomics — an allow rule that doesn't fire, so you get prompted. There's a second direction nobody filed.
A deny rule doesn't fire either. Real Permission.evaluate on current dev:
config { edit: { "*": "allow", "/etc/**": "deny" } }
target : /etc/passwd
pattern the tool sends (path.relative) : ../../../../etc/passwd
VERDICT : allow ← matched rule "*"
same target sent as "/etc/passwd":
VERDICT : deny
The deny is loaded, expanded, and sitting in the ruleset. It loses to the catch-all because it never becomes a candidate.
The documented example doesn't work. This is docs/permissions.mdx verbatim, presented as the supported way to allow reads but block edits in an external directory:
The external_directory half works, the edit half doesn't, and they're written by the same operator in the same block with the same pattern.
No relative form works portably. Note the two relativized strings above: ../../../../etc/passwd and ../personal/notes.md. The ../ count depends on where the session was started, so the same rule text produces a different non-matching string per project. There's no relative spelling an operator could use instead.
The correct model already exists in three places in-tree, which is what makes this a bug rather than a design decision:
permission/index.ts:178-184 — expand() deliberately turns ~/... into an absolute pattern on every rule, then that pattern is compared against a relativized target.
tool/external-directory.ts:30-37 — passes an absolute glob to the same ctx.ask. That's precisely why external_directory rules work while edit rules don't.
project/instance-context.ts:18 — containsPath already solves inside-vs-outside including the non-git worktree === "/" case.
Scope correction on my own earlier reading, since it matters for anyone auditing their config: this affects only the edit/write/read/apply_patch keys. external_directory and list match absolute paths and are fine. If you have an absolute rule that seems to work, check which key it's declared under before concluding anything — I got that wrong initially and so did two other people looking at the same evidence.
Field data. An independent OpenCode deployment replayed the fix against its own permission logs: 125,800 evaluated lines → 19,411 distinct (pattern, rule, verdict) triples, re-evaluated with the platform's own Wildcard.match. Zero regressions. 181 verdicts flipped deny→allow, all of them scratch paths the operator had already written an allow rule for.
Across two deployments, every rule found genuinely inert was an allow — the friction everyone has reported. The deny-direction bypass above is demonstrated from the matcher rather than from a field incident, which I'd argue makes it easier to verify, not weaker: it reproduces in about a minute.
The PR takes the smallest fix that works — one helper, five call sites, evaluate() and last-match-wins untouched. #22336's broader proposal (specificity ranking, glob semantics) is a real discussion but a separate one, and mixing them is part of why the earlier attempts stalled.
Activity
github-actions commentedon Jun 3, 2026
This issue might be a duplicate of existing issues. Please check:
edit/writepermission checks pass relative paths to the matcher while patterns are written as absolute-style, causing*/tmp/*patterns to fail to match. Your issue appears to be the same bug applied to thewritetool specifically.*/tmp/*or**/private/tmp/**fail to match resolved absolute paths.If your issue is specifically about the
writetool being affected (while #20045 focuses onedit), it may be worth keeping open to track that gap — but please confirm whether #20045 or #25097 already covers your case before proceeding.iceteaSA commentedon Aug 2, 2026
Your diagnosis in the issue body is correct, and I've opened a PR against it: #40149.
Adding the evidence I gathered, because this has been filed four other times (#20045, #22336, #22465, #25097) and closed unfixed each time. Every one of those reported it as ergonomics — an allow rule that doesn't fire, so you get prompted. There's a second direction nobody filed.
A deny rule doesn't fire either. Real
Permission.evaluateon currentdev:The deny is loaded, expanded, and sitting in the ruleset. It loses to the catch-all because it never becomes a candidate.
The documented example doesn't work. This is
docs/permissions.mdxverbatim, presented as the supported way to allow reads but block edits in an external directory:The
external_directoryhalf works, theedithalf doesn't, and they're written by the same operator in the same block with the same pattern.No relative form works portably. Note the two relativized strings above:
../../../../etc/passwdand../personal/notes.md. The../count depends on where the session was started, so the same rule text produces a different non-matching string per project. There's no relative spelling an operator could use instead.The correct model already exists in three places in-tree, which is what makes this a bug rather than a design decision:
permission/index.ts:178-184—expand()deliberately turns~/...into an absolute pattern on every rule, then that pattern is compared against a relativized target.tool/external-directory.ts:30-37— passes an absolute glob to the samectx.ask. That's precisely whyexternal_directoryrules work whileeditrules don't.project/instance-context.ts:18—containsPathalready solves inside-vs-outside including the non-gitworktree === "/"case.Scope correction on my own earlier reading, since it matters for anyone auditing their config: this affects only the
edit/write/read/apply_patchkeys.external_directoryandlistmatch absolute paths and are fine. If you have an absolute rule that seems to work, check which key it's declared under before concluding anything — I got that wrong initially and so did two other people looking at the same evidence.Field data. An independent OpenCode deployment replayed the fix against its own permission logs: 125,800 evaluated lines → 19,411 distinct (pattern, rule, verdict) triples, re-evaluated with the platform's own
Wildcard.match. Zero regressions. 181 verdicts flipped deny→allow, all of them scratch paths the operator had already written an allow rule for.Across two deployments, every rule found genuinely inert was an allow — the friction everyone has reported. The deny-direction bypass above is demonstrated from the matcher rather than from a field incident, which I'd argue makes it easier to verify, not weaker: it reproduces in about a minute.
The PR takes the smallest fix that works — one helper, five call sites,
evaluate()and last-match-wins untouched. #22336's broader proposal (specificity ranking, glob semantics) is a real discussion but a separate one, and mixing them is part of why the earlier attempts stalled.github-actions commentedon Oct 2, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.