Permissions — portable security policy
Generic, reusable reference for an OpenCode opencode.jsonc permissions array. Replace <user> with the actual username.
For OpenCode permission semantics (rule shape, wildcards, last-match-wins, external_directory vs read/edit, shell resource handling), load the built-in opencode skill — do not duplicate it here.
1. Policy template (categories)
Generic template — copy from references/permissions.jsonc and substitute <user>:
| # |
Category |
What it does |
Key resources |
| 1 |
Broad tool allows |
Keep agent usable without prompts |
read/edit/shell/skill * → allow |
| 2 |
Username isolation |
Deny every /Users/* home, then re-allow only /Users/<user> (last-match-wins). Auto-denies typos and any other username. Covers both dir itself and /* children + shell */Users/<user>*. |
read/edit/external_directory /Users/* → deny, shell */Users/* → deny, then .../<user>[/\*] → allow |
| 3 |
Secrets & credentials |
Hard deny even if username allow would match (must sit after the allow) |
~/.ssh/*, ~/.gnupg/*, ~/.aws/*, ~/.config/gcloud/*, ~/.azure/*, ~/.kube/*, ~/.netrc |
| 4 |
Shell history |
Prevent reading prompt history |
~/.zsh_history, ~/.bash_history |
| 5 |
Private macOS data |
Block Keychains, Cookies, Mail, Messages, Safari, TCC, MobileSync |
~/Library/Keychains/*, Cookies/*, Mail/*, Messages/*, Safari/*, com.apple.TCC/*, MobileSync/* |
| 5a |
Private Linux data (optional, Linux) |
Same principle: keyrings, browser profiles, mail stores |
~/.local/share/keyrings/*, ~/.local/share/kwalletd/*, ~/.config/google-chrome/*, ~/.config/chromium/*, ~/.mozilla/firefox/*, ~/.thunderbird/*, ~/.password-store/* |
| 5b |
Private Windows data (optional, Windows) |
Credentials, Edge/Chrome/Firefox profiles, Outlook, DPAPI |
~/AppData/Roaming/Microsoft/Credentials/*, ~/AppData/Local/Microsoft/Credentials/*, ~/AppData/Roaming/Microsoft/Protect/*, ~/AppData/Local/Google/Chrome/*, ~/AppData/Local/Microsoft/Edge/*, ~/AppData/Roaming/Mozilla/Firefox/*, ~/AppData/Roaming/Thunderbird/* |
| 6 |
.env handling |
Allow example, ask for real envs (first-match in docs, but here ordered for last-match) |
~/dev/*.env* + *.env.example → allow, *.env + *.env.* → ask |
| 7 |
System config |
Ask before touching /etc |
external_directory /etc/*, /private/etc/* → ask |
| 8 |
Temp dirs |
Allow so /tmp never prompts (covers /tmp→/private/tmp symlink) |
external_directory /tmp/*, /private/tmp/* → allow |
| 9 |
External dev volume |
Treat like ~/dev |
external_directory /Volumes/Daten/dev/* → allow |
| 10 |
Tooling |
Homebrew |
external_directory /opt/homebrew/* → allow |
1a. Cross-platform username isolation
macOS uses /Users/*, Linux /home/*, Windows C:/Users/* (and other drive letters). All use forward slashes — OpenCode normalizes them and matches case-insensitive on Windows. Keep the same deny-then-allow pattern per platform:
// Linux (uncomment when porting)
// { "action": "read", "resource": "/home/*", "effect": "deny" },
// { "action": "edit", "resource": "/home/*", "effect": "deny" },
// { "action": "external_directory", "resource": "/home/*", "effect": "deny" },
// { "action": "shell", "resource": "*/home/*", "effect": "deny" },
// { "action": "read", "resource": "/home/<user>", "effect": "allow" },
// { "action": "read", "resource": "/home/<user>/*", "effect": "allow" },
// ... same for edit/external_directory + shell "*/home/<user>*"
// Windows (uncomment when porting — forward slashes, covers C: and D:)
// { "action": "read", "resource": "C:/Users/*", "effect": "deny" },
// { "action": "read", "resource": "D:/Users/*", "effect": "deny" },
// { "action": "shell", "resource": "*C:/Users/*", "effect": "deny" },
// ... then allow C:/Users/<user> + D:/Users/<user>
See references/permissions.jsonc §§2b/2c (username) and §§5a/5b (private data).
1b. Cross-platform shell history
Add once:
{ "action": "read", "resource": "~/.local/share/history/*", "effect": "deny" }, // cross-platform shells
{ "action": "read", "resource": "~/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/*", "effect": "deny" },
{ "action": "read", "resource": "~/.cache/*", "effect": "ask" } // optional: be cautious
2. Apply / update
# edit global config
code ~/.config/opencode/opencode.jsonc
# copy ordered block from references/permissions.jsonc into "permissions": [ ... ]
# trigger daemon reload (watches global dir):
touch ~/.config/opencode/opencode.jsonc
opencode2 service status
opencode2 api get /api/health
Validation after change:
node -e "const fs=require('fs');let t=fs.readFileSync(process.env.HOME+'/.config/opencode/opencode.jsonc','utf8');let o='',s=false,e=false;for(let i=0;i<t.length;){let c=t[i];if(s){o+=c;if(e)e=false;else if(c=='')e=true;else if(c=='\"')s=false;i++}else{if(c=='\"'){s=true;o+=c;i++}else if(c=='/'&&t[i+1]=='/'){while(i<t.length&&t[i]!='\n')i++}else if(c=='/'&&t[i+1]=='*'){i+=2;while(i<t.length&&!(t[i]=='*'&&t[i+1]=='/'))i++;i+=2}else{o+=c;i++}}} JSON.parse(o.replace(/,\s*([}\]])/g,'$1'));console.log('JSONC OK')"
# then test: substitute <user> with actual name first
read /Users/<user>/dev/... → allow
read /Users/<other>/... → deny (external_directory)
shell cat /Users/<user>/... → allow, cat /Users/<other>/... → deny
3. New machine / port
- Substitute
<user> in blocks 2/2b/2c with the real username (e.g. via sed -i '' 's/<user>/myname/g' references/permissions.jsonc before copying).
- Adjust block 9 if the external volume path differs (
/Volumes/Daten/dev), block 10 if Homebrew prefix differs (/opt/homebrew vs /usr/local vs C:/Program Files).
- Do not move block 2 after block 3 — secrets must stay after the username allow so they override it.
Reference files
references/permissions.jsonc — copy-paste ready ordered permissions array (10 active blocks + commented cross-platform §§2b/2c/5a/5b).
1---2name: permissions3description: TRIGGER when editing, reviewing, or bootstrapping OpenCode permissions in opencode.jsonc, setting up username isolation, secrets/macos privacy denies, .env handling, or external_directory rules. Reference for this machine's security policy and permission ordering (last-match-wins).4---56# Permissions — portable security policy78Generic, reusable reference for an OpenCode `opencode.jsonc` `permissions` array. Replace `<user>` with the actual username.910> For OpenCode permission semantics (rule shape, wildcards, `last-match-wins`, `external_directory` vs `read`/`edit`, `shell` resource handling), load the built-in `opencode` skill — do not duplicate it here.1112## 1. Policy template (categories)1314Generic template — copy from `references/permissions.jsonc` and substitute `<user>`:1516| # | Category | What it does | Key resources |17|---|---|---|---|18| 1 | **Broad tool allows** | Keep agent usable without prompts | `read/edit/shell/skill * → allow` |19| 2 | **Username isolation** | Deny every `/Users/*` home, then re-allow only `/Users/<user>` (last-match-wins). Auto-denies typos and any other username. Covers both dir itself and `/*` children + `shell */Users/<user>*`. | `read/edit/external_directory /Users/* → deny`, `shell */Users/* → deny`, then `.../<user>[/\*] → allow` |20| 3 | **Secrets & credentials** | Hard deny even if username allow would match (must sit *after* the allow) | `~/.ssh/*`, `~/.gnupg/*`, `~/.aws/*`, `~/.config/gcloud/*`, `~/.azure/*`, `~/.kube/*`, `~/.netrc` |21| 4 | **Shell history** | Prevent reading prompt history | `~/.zsh_history`, `~/.bash_history` |22| 5 | **Private macOS data** | Block Keychains, Cookies, Mail, Messages, Safari, TCC, MobileSync | `~/Library/Keychains/*`, `Cookies/*`, `Mail/*`, `Messages/*`, `Safari/*`, `com.apple.TCC/*`, `MobileSync/*` |23| 5a | **Private Linux data** *(optional, Linux)* | Same principle: keyrings, browser profiles, mail stores | `~/.local/share/keyrings/*`, `~/.local/share/kwalletd/*`, `~/.config/google-chrome/*`, `~/.config/chromium/*`, `~/.mozilla/firefox/*`, `~/.thunderbird/*`, `~/.password-store/*` |24| 5b | **Private Windows data** *(optional, Windows)* | Credentials, Edge/Chrome/Firefox profiles, Outlook, DPAPI | `~/AppData/Roaming/Microsoft/Credentials/*`, `~/AppData/Local/Microsoft/Credentials/*`, `~/AppData/Roaming/Microsoft/Protect/*`, `~/AppData/Local/Google/Chrome/*`, `~/AppData/Local/Microsoft/Edge/*`, `~/AppData/Roaming/Mozilla/Firefox/*`, `~/AppData/Roaming/Thunderbird/*` |25| 6 | **.env handling** | Allow example, ask for real envs (first-match in docs, but here ordered for last-match) | `~/dev/*.env*` + `*.env.example → allow`, `*.env` + `*.env.* → ask` |26| 7 | **System config** | Ask before touching `/etc` | `external_directory /etc/*`, `/private/etc/* → ask` |27| 8 | **Temp dirs** | Allow so `/tmp` never prompts (covers `/tmp→/private/tmp` symlink) | `external_directory /tmp/*`, `/private/tmp/* → allow` |28| 9 | **External dev volume** | Treat like `~/dev` | `external_directory /Volumes/Daten/dev/* → allow` |29| 10 | **Tooling** | Homebrew | `external_directory /opt/homebrew/* → allow` |3031## 1a. Cross-platform username isolation3233macOS uses `/Users/*`, Linux `/home/*`, Windows `C:/Users/*` (and other drive letters). All use forward slashes — OpenCode normalizes them and matches case-insensitive on Windows. Keep the same deny-then-allow pattern per platform:3435```jsonc36// Linux (uncomment when porting)37// { "action": "read", "resource": "/home/*", "effect": "deny" },38// { "action": "edit", "resource": "/home/*", "effect": "deny" },39// { "action": "external_directory", "resource": "/home/*", "effect": "deny" },40// { "action": "shell", "resource": "*/home/*", "effect": "deny" },41// { "action": "read", "resource": "/home/<user>", "effect": "allow" },42// { "action": "read", "resource": "/home/<user>/*", "effect": "allow" },43// ... same for edit/external_directory + shell "*/home/<user>*"4445// Windows (uncomment when porting — forward slashes, covers C: and D:)46 // { "action": "read", "resource": "C:/Users/*", "effect": "deny" },47 // { "action": "read", "resource": "D:/Users/*", "effect": "deny" },48 // { "action": "shell", "resource": "*C:/Users/*", "effect": "deny" },49 // ... then allow C:/Users/<user> + D:/Users/<user>50```5152See `references/permissions.jsonc` §§2b/2c (username) and §§5a/5b (private data).5354## 1b. Cross-platform shell history5556Add once:57```jsonc58{ "action": "read", "resource": "~/.local/share/history/*", "effect": "deny" }, // cross-platform shells59{ "action": "read", "resource": "~/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/*", "effect": "deny" },60{ "action": "read", "resource": "~/.cache/*", "effect": "ask" } // optional: be cautious61```6263## 2. Apply / update6465```bash66# edit global config67code ~/.config/opencode/opencode.jsonc68# copy ordered block from references/permissions.jsonc into "permissions": [ ... ]69# trigger daemon reload (watches global dir):70touch ~/.config/opencode/opencode.jsonc71opencode2 service status72opencode2 api get /api/health73```7475Validation after change:7677```bash78node -e "const fs=require('fs');let t=fs.readFileSync(process.env.HOME+'/.config/opencode/opencode.jsonc','utf8');let o='',s=false,e=false;for(let i=0;i<t.length;){let c=t[i];if(s){o+=c;if(e)e=false;else if(c=='')e=true;else if(c=='\"')s=false;i++}else{if(c=='\"'){s=true;o+=c;i++}else if(c=='/'&&t[i+1]=='/'){while(i<t.length&&t[i]!='\n')i++}else if(c=='/'&&t[i+1]=='*'){i+=2;while(i<t.length&&!(t[i]=='*'&&t[i+1]=='/'))i++;i+=2}else{o+=c;i++}}} JSON.parse(o.replace(/,\s*([}\]])/g,'$1'));console.log('JSONC OK')"79# then test: substitute <user> with actual name first80```8182* `read /Users/<user>/dev/...` → allow83* `read /Users/<other>/...` → deny (external_directory)84* `shell cat /Users/<user>/...` → allow, `cat /Users/<other>/...` → deny8586## 3. New machine / port8788- Substitute `<user>` in blocks 2/2b/2c with the real username (e.g. via `sed -i '' 's/<user>/myname/g' references/permissions.jsonc` before copying).89- Adjust block 9 if the external volume path differs (`/Volumes/Daten/dev`), block 10 if Homebrew prefix differs (`/opt/homebrew` vs `/usr/local` vs `C:/Program Files`).90- Do **not** move block 2 after block 3 — secrets must stay after the username allow so they override it.9192## Reference files9394- `references/permissions.jsonc` — copy-paste ready ordered `permissions` array (10 active blocks + commented cross-platform §§2b/2c/5a/5b).