Browse Source

fix(claude-code): mitigate coder-agent unscoped edit gap with deny-only permission guidance

Claude Code cannot express per-agent path-scoped denies and plugins
cannot ship permission rules, so the shipped coder-agent granted
Write/Edit without the five security globs. Ship explicit
disallowedTools: Bash, Task, a hard protected-paths prompt rule,
opt-in deny-only RECOMMENDED-PERMISSIONS.md (README-linked), and a
changelog security advisory. Bash remains omitted (fail-closed).

Task: canonical-refactor-02
darrenhinde 2 weeks ago
parent
commit
8e12c63fc4

+ 4 - 0
CHANGELOG.md

@@ -3,6 +3,10 @@
 All notable changes to this project will be documented in this file.
 
 ## [Unreleased]
+
+### Security
+- **Advisory: Claude Code `coder-agent` file-edit scoping** — Claude Code subagent frontmatter cannot express path-scoped permissions, so the shipped plugin's `coder-agent` (`plugins/claude-code/agents/coder-agent.md`) granted `Write`/`Edit` without the five security deny globs its OpenCode counterpart enforces (`**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`) — it could edit secret files the authored agent is explicitly denied. Claude Code plugins cannot ship permission rules, so full enforcement requires a one-time user opt-in. Mitigations shipped in this release: the agent now declares an explicit `disallowedTools: Bash, Task` (making the existing fail-closed shell posture explicit; `Bash` remains omitted from `tools:`), a hard protected-paths rule in the agent prompt (stop-and-report if a subtask targets those paths), and a new `plugins/claude-code/RECOMMENDED-PERMISSIONS.md` (linked from the plugin README) with copy-paste **deny-only** `settings.json` rules — users should add these to `.claude/settings.json`; never hoist per-agent allow rules to project scope, as that re-creates the escalation one layer up.
+
 ## [0.7.1] - 2026-01-30
 
 ### Changes

+ 19 - 1
plugins/claude-code/README.md

@@ -173,9 +173,11 @@ Fetch external library and framework documentation from Context7 API and other s
 ### coder-agent
 Execute coding subtasks with full context awareness, self-review, and quality validation.
 
-**Tools**: Read, Write, Edit, Glob, Grep  
+**Tools**: Read, Write, Edit, Glob, Grep (Bash and Task explicitly disallowed — fail-closed)  
 **Model**: sonnet
 
+> ⚠️ Claude Code cannot scope `Write`/`Edit` to paths per-agent. Apply the opt-in deny rules in [RECOMMENDED-PERMISSIONS.md](./RECOMMENDED-PERMISSIONS.md) to enforce the intended `.env`/`.key`/`.secret`/`node_modules`/`.git` protections.
+
 ### test-engineer
 Generate comprehensive tests using TDD principles with coverage analysis and validation.
 
@@ -318,6 +320,22 @@ Stage 4: Execute with loaded context → No nested discovery needed
 
 **Never skip approval** - it prevents wasted work and ensures alignment.
 
+## 🔒 Security: Recommended Permission Hardening
+
+Claude Code subagent frontmatter accepts tool **names only** — it cannot express the
+path-scoped denies (`**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`)
+that the OpenCode `coder-agent` is authored with, and plugins cannot ship permission
+rules. Without action on your part, agents granted `Write`/`Edit` can modify those paths.
+
+**Strongly recommended**: copy the opt-in, deny-only rules from
+[RECOMMENDED-PERMISSIONS.md](./RECOMMENDED-PERMISSIONS.md) into your project's
+`.claude/settings.json` (or your user `~/.claude/settings.json`). Deny rules always win
+over allows and over-apply only in the safe direction. Never hoist per-agent *allow*
+rules to project scope — that re-creates the escalation the denies prevent.
+
+`coder-agent` ships fail-closed on shell access: no `Bash` in `tools:` and an explicit
+`disallowedTools: Bash, Task`.
+
 ## 🔧 Configuration
 
 ### Model: opusplan

+ 91 - 0
plugins/claude-code/RECOMMENDED-PERMISSIONS.md

@@ -0,0 +1,91 @@
+# Recommended Permissions (Opt-In, Deny-Only)
+
+**Status: strongly recommended for every project that uses this plugin.**
+
+## Why this file exists
+
+The OpenCode `coder-agent` is authored with five path-scoped edit denies that protect
+secrets and repository internals:
+
+- `**/*.env*`
+- `**/*.key`
+- `**/*.secret`
+- `node_modules/**`
+- `.git/**`
+
+Claude Code cannot express these per-agent: subagent frontmatter `tools:` /
+`disallowedTools:` accept **tool names only** (no path patterns), and plugin-shipped
+`settings.json` files support only the `enabledPlugins` and `extraKnownMarketplaces`
+keys — a plugin **cannot ship permission rules** on your behalf. So the shipped
+`coder-agent` grants `Write`/`Edit` without those path guards enforced.
+
+The plugin mitigates this two ways, but neither is a hard guarantee:
+
+1. `coder-agent` omits `Bash` and explicitly declares `disallowedTools: Bash, Task`
+   (fail-closed — do not re-add these).
+2. `coder-agent`'s prompt contains a hard protected-paths rule — but prompt rules are
+   instructions, **not enforcement**.
+
+Real enforcement requires a one-time opt-in from you: copy the deny rules below into
+your project or user settings.
+
+## Where to put the rules
+
+- Project-wide (recommended, shareable): `.claude/settings.json`
+- Just for you, all projects: `~/.claude/settings.json`
+
+Permission precedence is **deny → ask → allow, first match wins; specificity does not
+reorder** — so these denies cannot be overridden by any allow rule, and they apply to
+every agent in the project, which only over-applies in the safe direction.
+
+## Recommended deny rules
+
+`Edit(...)` rules apply to **all built-in tools that edit files** (including `Write`),
+so these five rules restore the full authored protection:
+
+```json
+{
+  "permissions": {
+    "deny": [
+      "Edit(**/*.env*)",
+      "Edit(**/*.key)",
+      "Edit(**/*.secret)",
+      "Edit(node_modules/**)",
+      "Edit(.git/**)"
+    ]
+  }
+}
+```
+
+If your settings file already has a `permissions.deny` array, append these entries to it.
+
+## Optional extra hardening (also deny-only)
+
+Shell-safety denies that mirror the OpenCode `openagent` policy. Bash prefix matching is
+best-effort (it cannot catch every obfuscation), but a deny here still only removes
+capability:
+
+```json
+{
+  "permissions": {
+    "deny": [
+      "Bash(sudo:*)",
+      "Bash(rm -rf:*)"
+    ]
+  }
+}
+```
+
+## Never add allow rules from this plugin's docs
+
+Do **not** hoist per-agent allow rules (for example
+`Bash(bash .opencode/skills/task-management/router.sh complete:*)`) into project or
+user settings. Project-level allows apply to **every** agent — including agents authored
+with no shell access at all — which silently re-creates the exact privilege escalation
+these denies exist to prevent, one layer up. This document intentionally contains deny
+rules only.
+
+## Verifying
+
+After adding the rules, run `/permissions` in Claude Code and confirm the five `Edit(...)`
+denies are listed. Then ask any agent to modify a `.env` file — it must be denied.

+ 7 - 0
plugins/claude-code/agents/coder-agent.md

@@ -16,6 +16,7 @@ description: |
   <commentary>A concrete implementation task with a specific file — coder-agent executes it.</commentary>
   </example>
 tools: Read, Write, Edit, Glob, Grep
+disallowedTools: Bash, Task
 model: sonnet
 ---
 
@@ -25,6 +26,10 @@ model: sonnet
 
 ## Core Rules
 
+<rule id="protected_paths">
+  NEVER create, modify, or delete files matching these protected patterns: `**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`. These are security-protected paths (secrets, credentials, dependency and VCS internals). If a subtask appears to require touching one of them, STOP and report back to the orchestrator instead of proceeding. Claude Code cannot enforce per-agent path scoping — users should also apply the enforceable deny rules in `RECOMMENDED-PERMISSIONS.md` (plugin root).
+</rule>
+
 <rule id="context_preloaded">
   Context files are pre-loaded by the main agent. Read all context_files from subtask JSON before implementing.
 </rule>
@@ -43,6 +48,7 @@ model: sonnet
 <constraints>Limited bash access for task status updates only. Sequential execution. Self-review mandatory before handoff.</constraints>
 
 <tier level="1" desc="Critical Operations">
+  - @protected_paths: Never touch `**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`
   - @context_preloaded: Read all context_files before coding
   - @self_review_required: Self-Review Loop before signaling done
   - @task_order: Sequential, no skipping
@@ -133,6 +139,7 @@ Use `grep` on your deliverables to catch:
 - Hardcoded secrets, API keys, or credentials
 - Missing error handling: `async` functions without `try/catch` or `.catch()`
 - `any` types where specific types were required
+- Any deliverable touching a protected path (`**/*.env*`, `**/*.key`, `**/*.secret`, `node_modules/**`, `.git/**`) — forbidden, see @protected_paths
 
 #### Check 3: Acceptance Criteria Verification
 - Re-read the subtask's `acceptance_criteria` array