commit aac21e3bddc8ce0427ed4eb79a3862a293621771
parent 1e226f867b6547ca4c974c827286f7ec046e4239
Author: Anders Hedman <anders.hedman01@gmail.com>
Date: Tue, 22 Sep 2026 20:20:58 +0200
fix(opencode): harden secret-path denies
Read denies for cloud.conf, secret*, and terraform.tfstate* now match
nested and absolute paths; previously they only matched files at the
workspace root.
Drop the path-shaped glob and grep denies: those actions match the
query, not a path, so they could not protect files.
Scope the shell denies to the content-dumping readers that are
allowlisted (cat, head, tail, diff, find, git diff/show/log, grep, rg),
including quoted and path-prefixed argument forms. Command-agnostic
patterns were avoided because they also hard-block non-reading commands
that merely mention these names, such as commit messages; every other
command already falls back to ask.
Diffstat:
1 file changed, 27 insertions(+), 24 deletions(-)
diff --git a/.config/opencode/opencode.jsonc b/.config/opencode/opencode.jsonc
@@ -76,23 +76,13 @@
{ "action": "read", "resource": "*", "effect": "ask" },
{ "action": "read", "resource": "*.env", "effect": "deny" },
{ "action": "read", "resource": "*.envrc", "effect": "deny" },
- { "action": "read", "resource": "cloud.conf", "effect": "deny" },
- { "action": "read", "resource": "secret*", "effect": "deny" },
- { "action": "read", "resource": "terraform.tfstate*", "effect": "deny" },
+ { "action": "read", "resource": "*cloud.conf", "effect": "deny" },
+ { "action": "read", "resource": "*secret*", "effect": "deny" },
+ { "action": "read", "resource": "*terraform.tfstate*", "effect": "deny" },
+ // glob/grep resources are the query, not a path, so path-shaped denies do nothing here
{ "action": "glob", "resource": "*", "effect": "ask" },
- { "action": "glob", "resource": "*.env", "effect": "deny" },
- { "action": "glob", "resource": "*.envrc", "effect": "deny" },
- { "action": "glob", "resource": "cloud.conf", "effect": "deny" },
- { "action": "glob", "resource": "secret*", "effect": "deny" },
- { "action": "glob", "resource": "terraform.tfstate*", "effect": "deny" },
-
{ "action": "grep", "resource": "*", "effect": "ask" },
- { "action": "grep", "resource": "*.env", "effect": "deny" },
- { "action": "grep", "resource": "*.envrc", "effect": "deny" },
- { "action": "grep", "resource": "cloud.conf", "effect": "deny" },
- { "action": "grep", "resource": "secret*", "effect": "deny" },
- { "action": "grep", "resource": "terraform.tfstate*", "effect": "deny" },
{ "action": "subagent", "resource": "*", "effect": "allow" },
{ "action": "skill", "resource": "*", "effect": "allow" },
@@ -172,18 +162,31 @@
// environment $VAR
{ "action": "shell", "resource": "printenv *", "effect": "deny" },
{ "action": "shell", "resource": "env *", "effect": "deny" },
- // .env / .envrc via shell
- { "action": "shell", "resource": "cat* .env*", "effect": "deny" },
+ // .env / .envrc — content-dumping readers only; absorb * covers quoted and path-prefixed forms
+ { "action": "shell", "resource": "cat* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "head* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "tail* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "diff* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "find* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "git diff* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "git show* *.env*", "effect": "deny" },
+ { "action": "shell", "resource": "git log* *.env*", "effect": "deny" },
+ // grep/rg: absorb would also block "process.env" searches, so bare + path forms separately
{ "action": "shell", "resource": "grep* .env*", "effect": "deny" },
+ { "action": "shell", "resource": "grep* */.env*", "effect": "deny" },
{ "action": "shell", "resource": "rg* .env*", "effect": "deny" },
- { "action": "shell", "resource": "head* .env*", "effect": "deny" },
- { "action": "shell", "resource": "tail* .env*", "effect": "deny" },
- // terraform.tfstate
- { "action": "shell", "resource": "cat* terraform.tfstate*", "effect": "deny" },
- { "action": "shell", "resource": "grep* terraform.tfstate*", "effect": "deny" },
- { "action": "shell", "resource": "rg* terraform.tfstate*", "effect": "deny" },
- { "action": "shell", "resource": "head* terraform.tfstate*", "effect": "deny" },
- { "action": "shell", "resource": "tail* terraform.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "rg* */.env*", "effect": "deny" },
+ // terraform state — same readers; no dot-prefix, so one absorb pattern each
+ { "action": "shell", "resource": "cat* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "head* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "tail* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "diff* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "find* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "git diff* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "git show* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "git log* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "grep* *.tfstate*", "effect": "deny" },
+ { "action": "shell", "resource": "rg* *.tfstate*", "effect": "deny" },
// Sops
{ "action": "shell", "resource": "sops*", "effect": "deny" },
// destructive: never auto-run (after the broader asks above)