Skip to content

permissions: external_directory ask is skipped for shell #56288

Open

Description

@LuchitoAlberto

Description

The external_directory rule is enforced for the file tools but not for shell.

With { "permissions": [ { "action": "external_directory", "resource": "*", "effect": "ask" } ] } in the global config:

Tool Action Prompt
write write to C:\Users\<user>\Desktop\out\a.txt yes — "Access external directory"
shell Set-Content "C:\Users\<user>\Desktop\out\a.txt" "x" -Encoding utf8 no

Same path, same session, back to back. The docs say shell checks "its external working directory and directories inferred by its scanner" before the shell rules, so a literal absolute path passed to a standard cmdlet should be the easiest case to infer.

This is not #54524. There the explicit rule works and only the built-in default does not. Here the rule is present, works for the file tools, and is skipped for shell. It also survives opencode reload and opencode service restart, and experimental.portable_shell_scanner: true makes no difference.

Impact: shell can read, write and delete files anywhere on disk without an external_directory prompt, so the rule is not a boundary on its own. Workaround: { "action": "shell", "resource": "*", "effect": "ask" }.

Related: #56247 (permission evaluations are no longer logged in v2) made this harder to diagnose, since no permission lines appear in the log for either case.

Steps to reproduce

  1. Global config: { "permissions": [ { "action": "external_directory", "resource": "*", "effect": "ask" } ] }
  2. Start opencode in a workspace (e.g. a temp directory).
  3. Have the agent write a file outside the workspace through shell: Set-Content "C:\Users\<user>\Desktop\out\a.txt" "x" -Encoding utf8 -> the file is created, no prompt.
  4. Have the agent write to the same path through the write tool -> the "Permission required / Access external directory" dialog appears.

Plugins

None

OpenCode version

2.0.32

Operating System

Windows 11 Home Single Language (NT 10.0.26300.0)

Terminal

Windows Terminal (PowerShell 5.1.26100.9711)

Activity

  1. opencode-agent commented on Oct 17, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I can reproduce this on 2.0.32, and it still happens on the latest dev (65932e8).

    There's no Windows or PowerShell in my sandbox, so I tested on Linux with bash, using your external_directory ask rule and a simulated model:

    1. The agent runs echo x > /tmp/oc-56288-outside/shell.txt; cp /etc/hostname …/copied.txt; touch …/touched.txt through shell. All three files are created, no prompt appears, and no permission request is created.
    2. The agent then calls read on /tmp/oc-56288-outside/shell.txt. The "Permission required / Access external directory" dialog appears right away.

    The recording and screenshot below show both steps, and the script below reproduces it.

    From the code, the shell scanner seems to infer directories only from commands that change the working directory (cd, pushd, Set-Location, …). Paths passed to other commands or cmdlets, and redirect targets, never reach the external_directory check. Your Set-Content case goes through the same code path, though I haven't run it on Windows.

    Related: #46398 (the same PowerShell symptom, filed with the older config syntax), and #49948 / #52083 (shell redirects bypassing permission checks). Until this is fixed, your workaround of a shell ask rule is the way to keep a prompt in front of shell commands.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#56288: with
    //   permissions: [{ action: "external_directory", resource: "*", effect: "ask" }]
    // the shell tool writes to an absolute path outside the project without any
    // external_directory check, while the read tool prompts for the same directory.
    import { Effect, Stream } from "effect"
    import * as fs from "node:fs"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const OUT = "/tmp/oc-56288-outside"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        config: {
          permissions: [{ action: "external_directory", resource: "*", effect: "ask" }],
        },
      },
      ({ ui, llm, opencode }) =>
        Effect.gen(function* () {
          fs.rmSync(OUT, { recursive: true, force: true })
          fs.mkdirSync(OUT, { recursive: true })
    
          yield* llm.serve((request: unknown, index: number) => {
            const text = JSON.stringify(request)
            if (index === 0)
              return Stream.make(
                Llm.toolCall({
                  index: 0,
                  id: "call_shell",
                  name: "shell",
                  input: { command: `echo x > ${OUT}/shell.txt; cp /etc/hostname ${OUT}/copied.txt; touch ${OUT}/touched.txt` },
                }),
                Llm.finish("tool-calls"),
              )
            if (index === 1) return Stream.make(Llm.text("SHELL STEP DONE"))
            if (index === 2)
              return Stream.make(
                Llm.toolCall({ index: 0, id: "call_read", name: "read", input: { path: `${OUT}/shell.txt` } }),
                Llm.finish("tool-calls"),
              )
            const i = text.lastIndexOf("call_read")
            console.log("read tool result seen by the model:", text.slice(i, i + 160))
            return Stream.make(Llm.text("WRITE STEP DONE"))
          })
    
          // 1. shell writes to an absolute path outside the project
          yield* ui.submit("Write files outside the project with shell")
          yield* ui.waitFor("SHELL STEP DONE", { timeout: 60_000 })
          yield* ui.screenshot("after-shell")
          console.log("files after shell:", fs.readdirSync(OUT))
          console.log("pending permission requests:", JSON.stringify((yield* opencode.permission.request.list()).data))
    
          // 2. read tool on the file shell just wrote -> external_directory prompt
          yield* ui.submit("Now read that file with the read tool")
          yield* ui.waitFor("Access external directory", { timeout: 60_000 })
          yield* Effect.sleep("1 second")
          yield* ui.screenshot("read-prompt")
          console.log("pending permission requests:", JSON.stringify((yield* opencode.permission.request.list()).data))
        }),
    )

    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. pulkit777exe commented on Oct 17, 2026

    @pulkit777exe

    I looked into the issue and found that both the tree-sitter and portable shell scanners were only picking up directory paths from commands like cd, pushd, and Set-Location.

    They were missing file paths passed to commands like cp, touch, and Set-Content, as well as paths used in shell redirects like > and >>. Because of this, those paths were never passed to FileAccess.authorizeExternal.

    I've updated both Bash and PowerShell scanners to extract these paths from command arguments and redirections, and pass them to FileAccess.authorizeExternal along with the existing directory targets.

    I also added tests in packages/core/test/shell-parse.test.ts to cover Bash redirects, file paths in command arguments, and PowerShell cmdlets and redirects.

    I'm opening a PR with these changes.

  3. wortkarg commented on Oct 29, 2026

    @wortkarg

    Not only external_directory, you can read e.g. .env files via shell commands like "Get-Content -Force .env", even if the read permission is set to "deny".

    "permissions": [
    { "action": "external_directory", "resource": "", "effect": "deny" },
    { "action": "read", "resource": "
    .env", "effect": "deny" },
    ...
    ],

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