Skip to content

permissions: V2 “Always allow” silently overrides configured “ask” across future project sessions #48456

Open

Description

@garnus

Description

In OpenCode V2, selecting Always allow creates a persistent, project-scoped permission in SQLite. That permission takes precedence over an explicit ask rule in configuration and applies to future sessions, including subagents using the same project.
This makes the effective permission policy impossible to determine from the global and project configuration files alone. A user can configure SSH to require approval, then discover that an approval saved days or weeks earlier allows new sessions to execute SSH without prompting.

Additional Context
V1 and V2 have materially different approval lifetimes
In V1 1.18.22, approvals were stored in an in-memory approved array. They were scoped to a running directory instance, rather than strictly to an individual conversation, but were not persisted to disk.
In the inspected V2 implementation:

  • saved approvals are loaded by project_id;
  • saved allow rules are appended after configured rules;
  • consequently, a saved allow takes precedence over configured ask;
  • Always allow writes the approval to SQLite.
    Configured deny is checked separately and remains enforced. This report concerns the loss of the expected approval boundary provided by ask.

Plugins

No response

OpenCode version

0.0.0-beta-19425

Steps to reproduce

The following scenario is supported by the observed session history and the V2 implementation. A fresh end-to-end reproduction was not performed.

  1. Configure SSH to require approval globally:
{
  "permissions": [
    {
      "action": "shell",
      "resource": "ssh *",
      "effect": "ask"
    }
  ]
}
  1. In a project session, approve an SSH permission request using Always allow.
  2. End that session.
  3. Start a new session in the same project, without adding an allow rule to either the global or project configuration.
  4. Have the agent perform an action that invokes SSH.
    The saved project permission allows SSH without a new approval prompt. Because the permission is stored in SQLite, its lifetime also extends beyond process restarts.
    Expected Behavior
    An explicit ask rule should remain a reliable approval boundary across new sessions.
    Temporary approval and persistent policy changes should be clearly separated:
  • Allow once approves one request.
  • Allow for this session approves matching requests within the current session.
  • Persistent project exceptions are explicitly configured in the project’s opencode.json.
    I deliberately use global configuration for defaults and project configuration for exceptions. I should not need to remember an old UI approval stored separately in a database to understand why my configured ask no longer prompts.
    Actual Behavior
    During an investigation of a server disk-usage alert, OpenCode made 15 SSH calls without requesting fresh approval:
  • 14 from a subagent;
  • one directly from the primary agent.
    The configuration contained shell / ssh * / ask.
    A saved permission record existed for the same project:
    action: shell resource: ssh * created: 2026-09-01
    The SSH calls occurred in a new session on 2026-09-11.
    The saved record and successful SSH execution were confirmed in local storage. The original interaction that created the saved approval was not recovered.

Screenshot and/or share link

No response

Operating System

macOS / Darwin 25.6.0, arm64

Terminal

ghostty/tmux (TERM=tmux-256color)

Activity

  1. github-actions commented on Sep 11, 2026

    @github-actions
    Contributor

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

  2. lingfish commented on Sep 20, 2026

    @lingfish

    I just came across this v1 to v2 change in behaviour too (v2.0.10).

    I actually like that it persists, but would think a better place would be in JSON. I don't see any way currently to remove the allowed permissions.

    Ideally, both choices would be good - persist to JSON, or just for current in-memory session.

  3. added a commit that references this issue on Sep 20, 2026
  4. aanbrn commented on Oct 15, 2026

    @aanbrn

    A concrete end-to-end reproduction for the mechanism described here (the report notes one was not performed) — OpenCode v2.0.29, macOS.

    Config (a project's opencode.json, V1 shape):

    { "permission": { "bash": { "*": "allow", "git push *": "ask" } } }

    opencode debug config normalizes it with the ask intact:

    {"action":"shell","resource":"*","effect":"allow"}
    {"action":"shell","resource":"git push *","effect":"ask"}
    

    The saved allow. A session on 2026-10-10 chose Always allow on a prompt, which persists a batch of shell allows for that project; git push * is one of them:

    $ sqlite3 -readonly ~/.local/share/opencode/opencode.db \
        "SELECT project_id, action, resource FROM permission WHERE action='shell' AND resource='git push *'"
    f27375c7117f8931d1deba0154d707075c27c811|shell|git push *
    

    Symptom. With that row present, a git push runs without an approval prompt:

    $ git push --dry-run origin HEAD
    To https://github.com/aanbrn/axon-showcase.git
     * [new branch]        HEAD -> record-saved-allow-override
    
    $ git push origin main
    Everything up-to-date
    

    (The second was a no-op — already in sync — so nothing was written; neither command raised a prompt.)

    Effect. evaluateInput builds const all = [...rules, ...(yield* savedRules())] and evaluate takes findLast(), so the saved allow is the last match and the configured ask never fires — for that project only; a project with no saved row still prompts as configured (saved rows are looked up by projectID). It is silent and durable: debug config shows the ask intact (the config reads correct), and the guard was still off five days later in a new session.

    Confirmed against packages/core/src/permission.ts at tag v2.0.29 (evaluate → rulesets.flat().findLast(…); evaluateInput → const all = [...rules, ...(yield* savedRules())]).

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions