diff --git a/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-05-permissions-default.md b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-05-permissions-default.md new file mode 100644 index 0000000..191b1f1 --- /dev/null +++ b/docs/reports/2026-02-20-opencode-command-md-merge/2026-02-20-phase-05-permissions-default.md @@ -0,0 +1,35 @@ +# Phase 5 Handoff: Change `--permissions` Default to `"none"` + +## Summary + +Changed the default value of `--permissions` from `"broad"` to `"none"` in the install command to prevent polluting user OpenCode config with global permissions. + +## Changes Made + +### 1. Code Change (`src/commands/install.ts`) + +- Line 51: Changed `default: "broad"` to `default: "none"` with comment referencing ADR-003 +- Line 52: Updated description to clarify "none (default)" + +```typescript +permissions: { + type: "string", + default: "none", // Default is "none" -- writing global permissions to opencode.json pollutes user config. See ADR-003. + description: "Permission mapping written to opencode.json: none (default) | broad | from-command", +}, +``` + +### 2. New Tests (`tests/cli.test.ts`) + +Added two new tests: +1. `"install --to opencode uses permissions:none by default"` - Verifies no `permission` or `tools` keys in opencode.json when using default +2. `"install --to opencode --permissions broad writes permission block"` - Verifies `permission` key is written when explicitly using `--permissions broad` + +## Test Results + +- CLI tests: 12 pass, 0 fail +- All tests: 187 pass, 0 fail + +## Next Steps + +None - Phase 5 is complete. \ No newline at end of file diff --git a/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md b/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md index 75c085a..3e7bd28 100644 --- a/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md +++ b/docs/reports/2026-02-20-opencode-command-md-merge/decisions.md @@ -181,5 +181,57 @@ If existing `opencode.json` is malformed JSON, warn and write plugin-only config ## Alternatives Considered 1. Plugin wins on conflict - Rejected: would overwrite user data -2. Merge and combine arrays - Rejected: MCP servers are keyed objects, not array -3. Fail on conflict - Rejected: breaks installation workflow \ No newline at end of file +2. Merge and combine arrays - Rejected: MCP servers are keyed object, not array +3. Fail on conflict - Rejected: breaks installation workflow + +--- + +## Decision: ADR-003 - Permissions Default "none" for OpenCode Output + +**Date:** 2026-02-20 +**Status:** Implemented + +## Context + +When installing a Claude plugin to OpenCode format, the `--permissions` flag determines whether permission/tool mappings is written to `opencode.json`. The previous default was `"broad"`, which writes global permissions to the user's config file. + +## Decision + +Change the default value of `--permissions` from `"broad"` to `"none"` in the install command. + +### Rationale + +- **User safety:** Writing global permissions to `opencode.json` pollutes user config and may grant unintended access +- **Principle alignment:** Follows AGENTS.md "Do not delete or overwrite user data" +- **Explicit opt-in:** Users must explicitly request `--permissions broad` to write permissions to their config +- **Backward compatible:** Existing workflows using `--permissions broad` continues to work + +### Implementation + +In `src/commands/install.ts`: +```typescript +permissions: { + type: "string", + default: "none", // Default is "none" -- writing global permissions to opencode.json pollutes user config. See ADR-003. + description: "Permission mapping written to opencode.json: none (default) | broad | from-command", +}, +``` + +### Test Coverage + +Added two CLI tests cases: +1. `install --to opencode uses permissions:none by default` - Verifies no `permission` or `tools` key in output +2. `install --to opencode --permissions broad writes permission block` - Verifies `permission` key is written when explicitly requested + +## Consequences + +- **Positive:** User config remains clean by default +- **Positive:** Explicit opt-in required for permission writing +- **Negative:** Users migrating from older versions need to explicitly use `--permissions broad` if they want permissions +- **Migration path:** Document the change in migration notes + +## Alternatives Considered + +1. Keep "broad" as default - Rejected: pollutes user config +2. Prompt user interactively - Rejected: breaks CLI automation +3. Write to separate file - Rejected: OpenCode expects permissions in opencode.json \ No newline at end of file diff --git a/src/commands/install.ts b/src/commands/install.ts index 77f5ea4..eeb5a85 100644 --- a/src/commands/install.ts +++ b/src/commands/install.ts @@ -48,8 +48,8 @@ export default defineCommand({ }, permissions: { type: "string", - default: "broad", - description: "Permission mapping: none | broad | from-commands", + default: "none", // Default is "none" -- writing global permissions to opencode.json pollutes user config. See ADR-003. + description: "Permission mapping written to opencode.json: none (default) | broad | from-command", }, agentMode: { type: "string", diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 49c20a6..be9ecde 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -426,4 +426,82 @@ describe("CLI", () => { expect(await exists(path.join(piRoot, "prompts", "workflows-review.md"))).toBe(true) expect(await exists(path.join(piRoot, "extensions", "compound-engineering-compat.ts"))).toBe(true) }) + + test("install --to opencode uses permissions:none by default", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-none-")) + const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin") + + const proc = Bun.spawn([ + "bun", + "run", + "src/index.ts", + "install", + fixtureRoot, + "--to", + "opencode", + "--output", + tempRoot, + ], { + cwd: path.join(import.meta.dir, ".."), + stdout: "pipe", + stderr: "pipe", + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + + if (exitCode !== 0) { + throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`) + } + + expect(stdout).toContain("Installed compound-engineering") + + const opencodeJsonPath = path.join(tempRoot, "opencode.json") + const content = await fs.readFile(opencodeJsonPath, "utf-8") + const json = JSON.parse(content) + + expect(json).not.toHaveProperty("permission") + expect(json).not.toHaveProperty("tools") + }) + + test("install --to opencode --permissions broad writes permission block", async () => { + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cli-perms-broad-")) + const fixtureRoot = path.join(import.meta.dir, "fixtures", "sample-plugin") + + const proc = Bun.spawn([ + "bun", + "run", + "src/index.ts", + "install", + fixtureRoot, + "--to", + "opencode", + "--permissions", + "broad", + "--output", + tempRoot, + ], { + cwd: path.join(import.meta.dir, ".."), + stdout: "pipe", + stderr: "pipe", + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + + if (exitCode !== 0) { + throw new Error(`CLI failed (exit ${exitCode}).\nstdout: ${stdout}\nstderr: ${stderr}`) + } + + expect(stdout).toContain("Installed compound-engineering") + + const opencodeJsonPath = path.join(tempRoot, "opencode.json") + const content = await fs.readFile(opencodeJsonPath, "utf-8") + const json = JSON.parse(content) + + expect(json).toHaveProperty("permission") + expect(json.permission).not.toBeNull() + }) })