OpenCode 2 Permissions: Migrate from V1 and Restore External Directory Prompts
Keep supported V1 configuration while reviewing OpenCode 2's changed defaults. Use one ordered V2 permissions baseline to restore external-directory prompts, ask before shell commands and edits, deny Git pushes, and protect environment-file reads. Then validate the effective rules in a disposable project.
Contents

Add an explicit external_directory rule with effect: "ask" to restore external-directory prompts after upgrading to OpenCode 2. As of 2026-10-31, the official migration guide documents a V2 default of allow, unlike V1’s ask. Keep supported V1 configuration until you are ready to convert its permission rules into the ordered V2 permissions array.
This guide provides one baseline for interactive coding and a validation checklist referenced to OpenCode v2.2.0. The expected results follow documentation and version-tagged source, not a live installation test.
Why does V2 allow external paths if unmatched checks use ask?
Built-in rules participate in matching, so an absent user rule does not mean an unmatched permission check.
The official V2 Defaults section starts every agent with a wildcard allow rule. That rule has action: "*", resource: "*", and effect: "allow". Later base rules ask before reading *.env and *.env.*, then allow *.env.example. Shipped agents can add restrictions.
Consequently, ordinary reads, edits, shell commands, and external-directory checks already have a matching base rule. The ask fallback applies only when the complete evaluated ruleset has no match. Do not treat omitted read or edit settings as an approval requirement.
External-directory approval and file-operation approval remain separate. OpenCode checks paths outside both the active Location and its non-root project worktree against external_directory, before checking read or edit. Here, Location means the active working location. An external file can therefore pass the boundary check and still encounter a read or edit restriction.
This sequencing comes from the directory-check mechanism, not the position of differently named actions in your array. Moving an external_directory rule above a read rule does not establish that sequence.
Can you keep V1 configuration, and which names change?
Yes. Native V2 configuration is optional; supported V1 configuration is normalized without requiring you to rewrite its source files. The migration instructions recommend verifying existing models, credentials, agents, permissions, and MCP servers before converting everything.
V1 groups permission effects by tool:
{
"permission": {
"bash": {
"git push *": "ask"
},
"edit": "allow"
},
"tools": {
"websearch": false
}
}
The documented V2 conversion expresses those explicit settings as ordered rules:
{
"permissions": [
{ "action": "shell", "resource": "git push *", "effect": "ask" },
{ "action": "edit", "resource": "*", "effect": "allow" },
{ "action": "websearch", "resource": "*", "effect": "deny" }
]
}
Each rule requires three strings: action, resource, and effect. allow proceeds without prompting, deny blocks the operation, and ask waits for a client decision.
This example converts the shown settings; it is not the recommended baseline below. It still allows edits and does not restore external-directory prompts.
The action mappings are bash → shell, task → subagent, and write/patch → edit. In V2, edit controls the target paths used by edit, write, and patch operations. subagent matches an agent ID; shell matches scanner-produced command strings. The action reference also identifies doom_loop and lsp as non-current V2 Core permission actions.
Review top-level settings, agent definitions, and scripts that generate configuration. Do not replace these words throughout unrelated files. Avoid leaving competing V1 and V2 permission definitions after an intentional conversion.
Use one conservative V2 permissions baseline
For interactive coding, ask before external access, shell commands, and edits. Allow routine Git inspection, deny matching push commands, and deny environment-file reads except example files.
Use project-root opencode.jsonc for repository-specific settings. Use ~/.config/opencode/opencode.jsonc for your user-wide configuration. OpenCode also accepts the .json variants; the configuration reference describes the supported locations.
Merge this array into your existing configuration. Preserve provider settings, agent definitions, and existing restrictions rather than replacing the whole file.
{
"$schema": "https://opencode.ai/config.json",
"permissions": [
{ "action": "external_directory", "resource": "*", "effect": "ask" },
{ "action": "shell", "resource": "*", "effect": "ask" },
{ "action": "shell", "resource": "git status *", "effect": "allow" },
{ "action": "shell", "resource": "git diff *", "effect": "allow" },
{ "action": "shell", "resource": "git push *", "effect": "deny" },
{ "action": "edit", "resource": "*", "effect": "ask" },
{ "action": "read", "resource": "*.env", "effect": "deny" },
{ "action": "read", "resource": "*.env.*", "effect": "deny" },
{ "action": "read", "resource": "*.env.example", "effect": "allow" }
]
}
The .env.example exception deliberately follows both read-deny rules. Only use that exception for files containing non-secret examples.
Unspecified actions still inherit their effective defaults. This baseline does not deny every MCP tool, prevent every possible push mechanism, or isolate the host filesystem. Its read restrictions also do not prohibit shell commands from reading the same files.
OpenCode’s shell warning explains that shell commands retain the host user’s authority. Directory inference is best effort. Review commands before approving them; use separate shell controls when file-operation rules are insufficient.
For a separate client, consult Claude Code permissions for secrets and Git. Do not reuse OpenCode configuration syntax there.
Put exceptions after the rules they override
Order matters among rules that match the same action and resource. A later matching rule replaces the earlier decision; a wildcard action can match several action names.
In the baseline, shell: "*" asks first. The later git status * and git diff * rules allow those commands. The git push * rule denies matching pushes.
A shell pattern ending in * also matches the command without arguments. Thus, git status * covers both git status and git status --short.
The matching reference defines whole-value wildcards: * matches zero or more characters, including /; ? matches one character. Paths are normalized to forward slashes, and Windows matching ignores case.
Multiple-resource operations require another step. First resolve each resource’s last matching rule. Any resulting deny blocks the operation; otherwise any ask requires approval. Compound shell commands can produce several resources, so one allowed Git command does not authorize the whole compound command.
Add a trusted external directory carefully
Allow a trusted directory boundary separately from its file operations. These optional rules are additions to the baseline, not a replacement configuration:
[
{ "action": "external_directory", "resource": "~/projects/reference/*", "effect": "allow" },
{ "action": "read", "resource": "~/projects/reference/*", "effect": "allow" },
{ "action": "edit", "resource": "~/projects/reference/*", "effect": "deny" }
]
Place the boundary allow after the broad external_directory ask. Place the read allow before the environment-file deny rules, so those restrictions still apply. Place the edit deny after the broad edit ask.
The directory rules support leading home references such as ~/ for these path actions. Shell resources remain raw command text and do not receive that configuration expansion.
The edit deny blocks operations governed by edit, including the write and patch tools. It does not block shell-based modifications to that directory. Keep separate shell restrictions and review any approval that could modify those files.
Migrate once, then validate with harmless files
Back up the files you actually use, request a proposed diff, and validate the accepted configuration in a disposable project. Begin with an installed V2 client and a working provider/model connection.
Review the proposed migration before applying it
Back up global and project configuration, including agent definitions under .opencode/agents/. The official agent locations also include user-wide definitions under ~/.config/opencode/agents/.
Handle plugins, server API integrations, and terminal settings separately. The migration guide identifies breaking changes in those areas.
Give OpenCode this review-first prompt:
Read the official OpenCode V1 migration and V2 permissions documentation.
Inspect my global, project, and agent permission configuration.
Do not modify any files yet.
Propose native V2 permissions rules.
Preserve unrelated settings and existing restrictions.
Map bash to shell, task to subagent, and write and patch to edit where applicable.
Identify unsupported fields and competing V1/V2 definitions.
Restore external_directory approval without weakening existing deny rules.
Use the baseline's shell and edit approval rules unless existing rules are stricter.
Put environment-file denies before the .env.example exception.
Show the complete proposed diff and explain every behavior change.
Wait for approval before applying it.
Check the merged result, not only the project file. Agent-specific permissions follow resolved top-level rules, so a later agent allow can override an ordinary top-level deny.
For organization-enforced restrictions, consult OpenCode policies. Policies can hard-deny checks after agent permissions and saved approvals. An ordinary permission array is not an administrator-enforced restriction.
After accepting the diff, reload the configuration with opencode reload. The CLI reference documents this command; inspect configuration warnings before continuing.
Run one validation pass
Use v2.2.0 as the reference version for these expected outcomes, with the built-in build agent. The table assumes the baseline is effective, with no conflicting agent rules, saved approvals, or additional policies.
Create harmless fixtures in Bash:
lab=$(mktemp -d)
mkdir -p "$lab/project" "$lab/outside"
git -C "$lab/project" init -q
printf 'ordinary fixture\n' > "$lab/project/README.md"
printf 'EXAMPLE_VALUE=local-test\n' > "$lab/project/.env"
printf 'EXAMPLE_VALUE=local-test\n' > "$lab/project/.env.example"
printf 'outside fixture\n' > "$lab/outside/fixture.txt"
cd "$lab/project"
Save the baseline as this project’s opencode.jsonc, then launch opencode. Use /agents to select build, as described in the terminal interface guide.
Ask the agent to attempt each operation separately through the specified tool. Do not substitute commands run manually in your terminal for agent permission checks.
| Agent operation | Expected permission result |
|---|---|
Read README.md with read | Allowed by the base read policy |
Read ../outside/fixture.txt with read | External-directory approval requested; ordinary read allowed after approval |
Read .env with read | Denied |
Read .env.example with read | Allowed |
Modify README.md with edit, write, or patch | Approval requested through edit |
Run git status, git status --short, or git diff through shell | Allowed |
Run git push through shell | Denied by the matching shell rule |
Run pwd through shell | Approval requested |
Before the push check, run git remote yourself and confirm it lists no names. Do not add a remote. A Git error about a missing remote is not a successful permission-denial test.
For approval tests, select Allow once rather than Allow always. Saved approvals persist for the project and can prevent later prompts; they do not override configured denies. Keep automatic approval disabled.
Inspect the actual tool request, permission decision, and file changes. A model’s promise not to push or read a file is not evidence that the permission engine blocked it.
Diagnose mismatches before loosening a rule
Check loaded configuration and the selected agent first. The CLI debugging commands include opencode debug config and opencode debug agents. Inspect their output locally; do not share credentials or sensitive configuration.
For a missing external prompt, confirm the target lies outside both the Location and its project worktree. Then check saved approvals and automatic approval. For an unexpected file-read result, confirm the agent used read, not shell.
For a blocked Git command, inspect its scanner-produced resource and any additional subcommands. For .env.example denial, check whether a later *.env.* rule or an additional policy overrides the intended exception.
Recheck your existing provider, model ID, authentication, and Base URL separately. BetterToken’s OpenCode integration instructions document a custom OpenAI-compatible connection using https://www.bettertoken.ai/v1. That connection does not replace OpenCode’s permission configuration.
For a wider migration decision, compare Cursor and OpenCode’s configuration and access routes. When evaluating a different terminal workflow, use the same disposable task with Aider or OpenCode for a Git project.
What does the v2.2.0 filesystem fix change?
The fix changes how external-directory checks resolve project files, not the recommendation to set an explicit approval rule.
The v2.2.0 release notes list the project-filesystem change. PR #59089 explains that remote and in-memory projects previously consulted the host machine’s disk. Its author states that ordinary local projects are unaffected.
For remote projects, repeat the external-directory check against that project’s actual filesystem. The V2 defaults documented on 2026-10-31 still require an explicit external_directory ask or deny rule when you need that boundary restriction.