Skip to content

write tool permission */tmp/*: allow still prompts despite matching pattern #30551

Closed as not planned

Description

@ezadEzanee

Description

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.

Config (opencode.jsonc)

"permission": {
  "write": {
    "*/tmp/*": "allow",
    "*/.config/*": "allow",
    "*/dev/*": "allow",
    "*.env": "deny"
  },
  "edit": {
    "*/tmp/*": "allow",
    "*/.config/*": "allow",
    "*/dev/*": "allow",
    "*.env": "deny"
  },
  "external_directory": {
    "/tmp/*": "allow",
    "/private/tmp/*": "allow"
  }
}

Steps to reproduce

  1. Use a build agent (default permissions).
  2. Issue any write tool call targeting /tmp/test.txt.
  3. 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

Activity

  1. github-actions commented on Jun 3, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    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.

  2. iceteaSA commented on Aug 2, 2026

    @iceteaSA

    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:

    config: external_directory allow + edit deny on ~/projects/personal/**
    
    edit VERDICT (documented intent: deny) : ask
    external_directory VERDICT             : allow
    

    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.

  3. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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