Skip to content

permissions: deny rule for read tool is ignored, file contents returned anyway #55728

Open

Description

@DevExord

Summary

A deny permission rule targeting the built-in read tool for a specific file path is not enforced: the agent can still read the file and gets its full contents back. This silently breaks file-level access control configured in opencode.json.

Environment

  • opencode version: v2.0.20
  • OS: Darwin 25.4.0 (macOS, arm64)
  • Terminal: ghostty (TERM=xterm-256color, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: latest release
  • Active plugins: oh-my-openagent@latest

Reproduction

  1. Create an opencode.json in a project directory:
    {
      "$schema": "https://opencode.ai/config.json",
      "permissions": [
        {
          "action": "read",
          "resource": "./Workshop/Checklist.md",
          "effect": "deny"
        }
      ]
    }
  2. Start an opencode session in that directory.
  3. Ask the agent to read Workshop/Checklist.md.
  4. Observe the read tool call succeeding and returning the file contents.

Expected Behavior

The read tool call should be denied (rule takes effect, tool returns a permission-denied error or asks the user).

Actual Behavior

The read tool returned the full contents of Workshop/Checklist.md without any denial or prompt. The config was loaded (the schema is valid), but the rule had no effect.

Additional Context

  • The config matches the published schema (permissions → Permission.Ruleset, {action, resource, effect}), so this does not appear to be a config-format mistake.
  • Reproducible consistently (happened on multiple reads in the same session).
  • Unclear whether deny rules work for other tools (e.g. bash, edit) or whether the issue is specific to path-based resources for read.

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I could reproduce this on v2.0.20, and it still happens on the latest dev (81200db).

    What I did: I used your opencode.json with the rule "resource": "./Workshop/Checklist.md", then had the agent call read on Workshop/Checklist.md. The read succeeded and the file contents were returned to the model. The recording and screenshot below show this, and the script below reproduces it.

    The ./ prefix seems to be the cause. The read tool checks permissions against the path relative to the project (Workshop/Checklist.md), and the rule text is compared literally, so a rule starting with ./ never matches. With "resource": "Workshop/Checklist.md" (no ./), the same read was correctly denied.

    Until this is addressed, writing path rules without the leading ./ should work around it, for example:

    { "action": "read", "resource": "Workshop/Checklist.md", "effect": "deny" }

    Related: #53365 is an open PR for a similar mismatch between absolute rules and relative paths for read/edit. It doesn't cover the ./ case.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#55728: a project opencode.json `permissions` deny rule for
    // `read` on "./Workshop/Checklist.md" does not stop the read tool.
    // Set RULE to try another resource spelling, e.g. RULE=Workshop/Checklist.md (control).
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const resource = process.env.RULE ?? "./Workshop/Checklist.md"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        project: {
          git: true,
          files: {
            "opencode.json": JSON.stringify(
              {
                $schema: "https://opencode.ai/config.json",
                permissions: [{ action: "read", resource, effect: "deny" }],
              },
              null,
              2,
            ),
            "Workshop/Checklist.md": "# Checklist\n\nSECRET-CHECKLIST-CONTENT-42\n",
          },
        },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          // Turn 1: the simulated model calls `read`. Turn 2: it reports what the tool result contained.
          yield* llm.serve((request, index) => {
            if (index === 0)
              return Stream.make(
                Llm.toolCall({ index: 0, id: "call_read", name: "read", input: { path: "Workshop/Checklist.md" } }),
                Llm.finish("tool-calls"),
              )
            const body = JSON.stringify(request)
            const leaked = body.includes("SECRET-CHECKLIST-CONTENT-42")
            return Stream.make(
              Llm.text(`Done reading. Tool result leaked file contents: ${leaked ? "YES (SECRET-CHECKLIST-CONTENT-42)" : "NO"}`),
            )
          })
          yield* ui.submit(`Read Workshop/Checklist.md (rule resource: ${resource})`)
          yield* ui.waitFor("Done reading.", { timeout: 30000 })
          yield* Effect.sleep("1 second")
          yield* ui.screenshot("after-read")
          const frame = yield* ui.capture()
          yield* Effect.log(`RULE=${resource} screen-says-leaked=${JSON.stringify(frame).includes("YES (SECRET")}`)
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. samvallad33 commented on Oct 20, 2026

    @samvallad33

    Hi DevExord,

    Did that deny rule on Checklist.md ever block a single read, or did every call go straight through?

    I hit the same kind of problem where an agent kept running a command it had been allowed once. If that is what happened on your end, would you want to see something combining memory, authority, and security so every tool call gets checked against your rules before it runs?

    Happy to have you give it a try:
    npx @vestige/init
    It keeps your deny rules across sessions and checks each read against them first, so a protected file stays closed next time.

    Sam

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions