opencode-era record. Superseded where moot by the pi migration (ADR-0055). Retained as historical context.
Date: 2026-08-02
Accepted — partially superseded by ADR-0048 (project-plus-user union for
additional sensitive paths, invocation-scoped /setup trust, last-match-wins
permission ordering, glob.pattern/grep.include tool-argument coverage,
symlink canonicalization, and manifest-level validation of the security
field).
ADR-0053 partially supersedes only the get/validate-only trusted-subcommand
clause by adding exact argv-shaped present PROJECT USER_OR_DASH env.* trust.
Issue #288: no layer of the harness prevents an agent — or any of its
sub-agents — from reading credential files. OpenCode's only built-in secret
protection is a read-tool deny on *.env/*.env.*, and it does not extend
to the bash tool; upstream's platform protected-files list is empty on
Linux. The @debug investigation (human-approved, 6-phase loop) verified 304
silent-access cells and established the root causes:
- Permission rules match command strings, never paths. Read-only agents
allowlist pathless
cat*/head*/tail*/grep*/find*;build,design, andgeneralcarrybash "*": allow. - The shell-tool FILES-set prompt covers only a few commands (
cat,cp,rm, …).head/tail/grep/sed/awk/less/base64/dd/tar/curlnever prompt, even outside the workspace; wrappers (bash -c,env,command,exec,eval), redirections (cat < file), and symlinks dodge even thecatask.external_directorydefaults toask, and auto-approve mode converts every ask into an allow;--dangerously-skip-permissionsbypasses everything. - The safety plugin classifies bash for destructive commands only and early-returns for every non-bash tool (ADR-0023/0036).
- No prompt-level prohibition exists — the Hard Boundaries prohibit modifying files outside the project, not reading secrets.
The @architect review returned GO-WITH-CONDITIONS (ADR-required: 0047):
record the immutable deny floor, additive-only extension semantics, the four
enforcement layers, the /setup trusted exception, circuit-breaker behavior,
redaction, and the residual threat model.
We add a layered sensitive-path enforcement surface. The plugin layer is load-bearing; every other layer is defense-in-depth or instruction.
A new dependency-free module .opencode/plugins/sensitive-paths.ts holds the
immutable deny floor and all matching logic:
| Path class | Pattern |
|---|---|
| opencode auth store | ~/.local/share/opencode/ (covers auth.json + sibling mcp-auth.json) |
| basename floor | auth.json / mcp-auth.json anywhere on the filesystem |
| review config | ~/.opencodereview/ |
| intelephense license | ~/intelephense/licen?e.txt (both spellings) |
| user Prism manifest | ~/.config/opencode/ (prism-user-manifest class) |
| SSH | ~/.ssh/ |
| cloud credentials | ~/.aws/ |
| netrc | ~/.netrc |
| git credentials | ~/.git-credentials |
| TLS private keys | /etc/ssl/private/ |
| env class | any **/.env and **/.env.* anywhere on the filesystem |
.env.example is the only env-class file agents may read. The project
prism.jsonc (tracked, committed) and .opencodereview/rule.json (tracked)
remain readable.
Matching is prefix/basename based on normalized absolute paths; ~ is
resolved against the invoking user's home. Bash commands are checked
operand-wise: segments are split, shell wrappers unwrapped (bounded depth),
operands resolved (~, absolute, relative, symlink-aware via realpath where
available), redirection operators skipped while their file operands are
checked, and dynamically-unresolvable operands are conservatively blocked
when a fallback regex matches a sensitive class.
The deny floor is immutable in code. The Prism manifest may only add
paths via security.additional_sensitive_paths (project tier default:
[]; user tier overlays field-by-field per ADR-0043). Semantics:
- The plugin receives the resolved additions via
OPENCODE_SENSITIVE_PATHS(newline-joined, exported byprism_manifest.php env0alongside the other env pairs; no second JSONC parser — ADR-0043's single PHP manifest-reader boundary is preserved). - Additions are unioned with the floor; there is no disable flag, no removal list, and no exception list.
- Malformed additions (relative paths, control characters, non-array value) fail closed: the CLI exits non-zero and the plugin refuses to start.
- Plugin (load-bearing) —
pre-tool-use.tstool.execute.beforeinterceptsread,grep,glob, andlistby path argument, andbashvia the operand check. Matches throw a generic, redacted error; the call is denied. Throws feed the ADR-0042 circuit breaker as denial events (a sensitive-path block produces a tool error part, exactly like the existingrm -rfblocks), so a stuck agent still trips after the threshold of 3. - Permission rules (spelling-limited) — global
readdenies for the env family andauth.json/mcp-auth.jsonbasenames with.env.examplere-allowed last (last-match-wins); the same five patterns appended to the bash objects ofbuild/design/generaland every agent with bash reader allowances;chat'sread/glob/grep/listbecome deny-first/allow-last objects. This layer cannot express every spelling (~, absolute, relative, wrappers) — it is defense-in-depth only. - Validator contract —
validate-harness.shasserts (a) the plugin imports and calls the matcher, (b) AGENTS.md carries the Hard Boundary bullet, (c) every agent with reader allowances carries the deny set (including inline agents viainline-agent-permissions.js). A regression at any layer turns the harness red. - Prompt-level instruction — AGENTS.md Hard Boundaries gains a
credential-read prohibition naming prompt-injection refusal; a dedicated
credential-protectionskill documents the deny list, layers, bypass reporting, and the extension mechanism.
The narrow human-invoked /setup write exception (AGENTS.md Hard Boundaries,
ADR-0043) is preserved. The matcher exempts only the prism-user-manifest
class for commands whose resolved executable is one of the harness's own
setup scripts under .github/scripts/: migrate-setup.sh,
setup-write-user-config.sh, setup-write-project-config.sh,
setup-substitute.sh, setup-scaffold.sh, setup-rulesets.sh,
check-setup-secrets.sh, and prism_manifest.php. prism_manifest.php is
trusted only for the get and validate subcommands — never
env0/values0/decode, whose stdout can carry secrets. All other path
classes remain enforced even for setup scripts. Additionally, get and
values0 redact env.* values as [redacted], closing CLI-based
exfiltration of manifest secrets.
Sensitive-path block errors never include command text, resolved paths, or
credential content (ADR-0042 redaction invariant). The hook throws the
constant reason sensitive-path policy (ADR-0047); diagnostics log no path
data.
Sensitive-path denials count toward the ADR-0042 consecutive-denial breaker
(threshold 3, session.abort escalation). This extends ADR-0042's denial
classes from config-deny/safety-block/ask-reject to include
sensitive-path blocks.
OpenCode provides no sandbox; permissions and plugin hooks are the only enforcement surfaces. Uncloseable at the harness layer:
- Helper scripts or binaries whose operand is the script, not the credential
(e.g.
php leak.php), and any child process run with the user's OS privileges. - Environment-variable exfiltration (
echo $DEEPSEEK_API_KEY,printenv) andprism_manifest.php env0stdout capture by a hostile agent. - Interactive PTY sessions (
vi, barebash) and MCP/LSP server internals. - User-approved
external_directoryglobs persisted per-project (the plugin blocks regardless of permission state). - Upstream drift in the OpenCode evaluator, FILES set, or arity semantics (mitigated by the pinned SDK + version-parity tests).
- A new OpenCode process is required to activate the plugin and permission changes; existing sessions remain exposed until restart.
- ADR-0023 (bash interception scope) and ADR-0036 (fail-closed) are extended — not edited — to cover path-access interception; ADR-0042 is extended to count sensitive-path denials.
- Agents with reader allowances gain the deny set; the validator reports violations until the config catches up (the intended red gate).
.env.example, projectprism.jsonc, and.opencodereview/rule.jsonremain readable;/setupkeeps its narrow write path.- Users may extend the deny list via
security.additional_sensitive_paths— never reduce it.
Rejected: Prism configures OpenCode but does not provide an OS sandbox; adopting one would break the existing permission/hook model and require infrastructure outside the project boundary.
Rejected: @debug probe evidence — rules catch the ~ spelling but
absolute/relative/wrapper spellings bypass; a path-based rule set cannot be
expressed in the string-matching permission layer at all. The
operand-resolving plugin hook closed 2112/2112 probe cells.
Rejected: env0/values0/decode emit secret-bearing stdout; an agent
could invoke them to read the user manifest. Trust is scoped to
get/validate plus CLI-level env.* redaction.
adr/0023-safety-hook-for-bash-tool-interception.md(extended)adr/0036-safety-hook-fail-closed-block-rules.md(extended)adr/0042-consecutive-denial-circuit-breaker.md(extended)adr/0043-prism-jsonc-manifest-migration.md(/setupexception preserved)docs/plans/2026-08-02-sensitive-path-enforcement.md(implementation plan)AGENTS.md(Hard Boundaries, credential-read prohibition).opencode/skills/credential-protection/SKILL.md(instructional layer).opencode/plugins/sensitive-paths.ts,.opencode/plugins/pre-tool-use.ts