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
{{ message }}
Repository navigation
permissions: external_directory ask is skipped for shell #56288
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.
Start opencode in a workspace (e.g. a temp directory).
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.
Have the agent write to the same path through the write tool -> the "Permission required / Access external directory" dialog appears.
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_directoryask rule and a simulated model:
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.
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 shellask rule is the way to keep a prompt in front of shell commands.
recording.mp4
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*asfsfrom"node:fs"import{Llm,OpenCodeDriver}from"opencode-drive"constOUT="/tmp/oc-56288-outside"exportdefaultOpenCodeDriver.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)=>{consttext=JSON.stringify(request)if(index===0)returnStream.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)returnStream.make(Llm.text("SHELL STEP DONE"))if(index===2)returnStream.make(Llm.toolCall({index: 0,id: "call_read",name: "read",input: {path: `${OUT}/shell.txt`}}),Llm.finish("tool-calls"),)consti=text.lastIndexOf("call_read")console.log("read tool result seen by the model:",text.slice(i,i+160))returnStream.make(Llm.text("WRITE STEP DONE"))})// 1. shell writes to an absolute path outside the projectyield*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 promptyield*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.
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.
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".
Activity
opencode-agent commentedon Oct 17, 2026
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_directoryaskrule and a simulated model:echo x > /tmp/oc-56288-outside/shell.txt; cp /etc/hostname …/copied.txt; touch …/touched.txtthroughshell. All three files are created, no prompt appears, and no permission request is created.readon/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 theexternal_directorycheck. YourSet-Contentcase 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
shellaskrule is the way to keep a prompt in front of shell commands.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.pulkit777exe commentedon Oct 17, 2026
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, andSet-Location.They were missing file paths passed to commands like
cp,touch, andSet-Content, as well as paths used in shell redirects like>and>>. Because of this, those paths were never passed toFileAccess.authorizeExternal.I've updated both Bash and PowerShell scanners to extract these paths from command arguments and redirections, and pass them to
FileAccess.authorizeExternalalong with the existing directory targets.I also added tests in
packages/core/test/shell-parse.test.tsto cover Bash redirects, file paths in command arguments, and PowerShell cmdlets and redirects.I'm opening a PR with these changes.
wortkarg commentedon Oct 29, 2026
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" },
...
],