Plugin: opencode-plugin-openspec v0.1.4 (injected agent openspec-plan, mode primary)
OpenCode: 1.18.5
Summary
If the OpenCode session is started in a directory that is not yet a git repository, the openspec-plan agent is denied on every edit call. It cannot create openspec/changes/<name>/proposal.md, cannot write spec files, cannot update AGENTS.md — the whole OpenSpec workflow is blocked.
The cause is an interaction between (a) the plugin's edit permission allowlist, which uses bare relative patterns (AGENTS.md, openspec/**, specs/**, project.md), and (b) OpenCode's edit tool, which checks permissions against path.relative(instance.worktree, filePath). In a git-less directory OpenCode binds the session to the global project whose
worktree is /, so every edit pattern comes out as Users/jones/src/github/clipclop/openspec/... — which never matches the plugin's relative patterns. The catch-all "*": "deny" then blocks all edits.
Steps to reproduce
- Create a directory that is not a git repo (no
git init).
mkdir /tmp/openspec-test && cd /tmp/openspec-test
- Start OpenCode there with the plugin installed.
- Select the
openspec-plan agent and ask it to propose a new change, e.g. create openspec/changes/foo/proposal.md.
- Every attempt to write/update a file is denied with a permission error.
Expected: the agent edits files under openspec/**, specs/**, and AGENTS.md without approval (per its own permission config).
Actual: all edit calls fail. Example from OpenCode's log:
message=evaluated permission=edit
pattern="Users/jones/src/github/clipclop/openspec/changes/build-clipclop/proposal.md"
action.permission=edit action.pattern=* action.action=deny
Root cause
OpenCode derives the project for a session via Project.fromDirectory
(packages/opencode/src/project/project.ts):
const worktree = data.id === ProjectV2.ID.make("global") && !data.vcs ? "/" : data.directory
A directory with no VCS resolves to the global project, whose worktree is the filesystem root /. This is stored per session in ~/.local/share/opencode/opencode.db:
session: id=ses_03f7c5804ffelscnVxgTWvB0DE project_id=global
project: id=global worktree=/
The edit tool (packages/opencode/src/tool/edit.ts) then asks permission with a path pattern relative to that worktree:
patterns: [path.relative(instance.worktree, filePath)]
With worktree === "/", editing
/Users/jones/src/github/clipclop/AGENTS.md produces the pattern
Users/jones/src/github/clipclop/AGENTS.md. The plugin's allowlist patterns
are bare relative paths:
edit: {
"*": "deny",
"project.md": "allow",
"AGENTS.md": "allow",
"openspec/**": "allow",
"specs/**": "allow"
}
Wildcard.match is plain glob (* → .*, / not special-cased), so Users/jones/.../AGENTS.md matches neither AGENTS.md nor openspec/**. Rules are last-match-wins (findLast), so the "*": "deny" catch-all is the only match — every edit is denied.
Note: once the session is bound to global, it stays that way for the session's lifetime. Running git init afterwards does not rebind it; only a fresh session in a now-git repo resolves correctly.
Why this is especially bad for this plugin
openspec-plan is a planning agent whose entire job is creating and updating OpenSpec artifacts. But because OpenSpec drives everything from openspec/changes/<name>/proposal.md (a file creation), a user is most likely to hit this on the first run in a brand-new project — precisely before they've had a chance to git init. There is no recovery path within the session: the agent cannot even bootstrap the repo with git init because its bash permission is also an allowlist (openspec, grep, ls, cat, find, echo, pwd, which, env, printenv, git status*, git log*, git diff*, git show*) that does not include git init.
Suggested fixes (any one)
- Use prefix-tolerant patterns in the agent permission. Since
Wildcard.match treats * as .* and does not anchor on /, patterns like **/AGENTS.md, **/openspec/**, **/specs/**, **/project.md match regardless of how the path is prefixed (worktree / or repo root).
- Detect and warn: if
ctx.project.id === "global" (git-less directory), skip injecting the restricted agent, or surface a message telling the user to git init and restart.
- Bootstrap git: the plugin's
bash allowlist could include git init so the agent can fix the precondition itself, then instruct a restart.
Plugin:
opencode-plugin-openspecv0.1.4 (injected agentopenspec-plan, modeprimary)OpenCode: 1.18.5
Summary
If the OpenCode session is started in a directory that is not yet a git repository, the
openspec-planagent is denied on everyeditcall. It cannot createopenspec/changes/<name>/proposal.md, cannot write spec files, cannot updateAGENTS.md— the whole OpenSpec workflow is blocked.The cause is an interaction between (a) the plugin's edit permission allowlist, which uses bare relative patterns (
AGENTS.md,openspec/**,specs/**,project.md), and (b) OpenCode's edit tool, which checks permissions againstpath.relative(instance.worktree, filePath). In a git-less directory OpenCode binds the session to theglobalproject whoseworktree is
/, so every edit pattern comes out asUsers/jones/src/github/clipclop/openspec/...— which never matches the plugin's relative patterns. The catch-all"*": "deny"then blocks all edits.Steps to reproduce
git init).openspec-planagent and ask it to propose a new change, e.g. createopenspec/changes/foo/proposal.md.Expected: the agent edits files under
openspec/**,specs/**, andAGENTS.mdwithout approval (per its own permission config).Actual: all
editcalls fail. Example from OpenCode's log:Root cause
OpenCode derives the project for a session via
Project.fromDirectory(
packages/opencode/src/project/project.ts):A directory with no VCS resolves to the
globalproject, whose worktree is the filesystem root/. This is stored per session in~/.local/share/opencode/opencode.db:The
edittool (packages/opencode/src/tool/edit.ts) then asks permission with a path pattern relative to that worktree:With
worktree === "/", editing/Users/jones/src/github/clipclop/AGENTS.mdproduces the patternUsers/jones/src/github/clipclop/AGENTS.md. The plugin's allowlist patternsare bare relative paths:
Wildcard.matchis plain glob (*→.*,/not special-cased), soUsers/jones/.../AGENTS.mdmatches neitherAGENTS.mdnoropenspec/**. Rules are last-match-wins (findLast), so the"*": "deny"catch-all is the only match — every edit is denied.Note: once the session is bound to
global, it stays that way for the session's lifetime. Runninggit initafterwards does not rebind it; only a fresh session in a now-git repo resolves correctly.Why this is especially bad for this plugin
openspec-planis a planning agent whose entire job is creating and updating OpenSpec artifacts. But because OpenSpec drives everything fromopenspec/changes/<name>/proposal.md(a file creation), a user is most likely to hit this on the first run in a brand-new project — precisely before they've had a chance togit init. There is no recovery path within the session: the agent cannot even bootstrap the repo withgit initbecause itsbashpermission is also an allowlist (openspec,grep,ls,cat,find,echo,pwd,which,env,printenv,git status*,git log*,git diff*,git show*) that does not includegit init.Suggested fixes (any one)
Wildcard.matchtreats*as.*and does not anchor on/, patterns like**/AGENTS.md,**/openspec/**,**/specs/**,**/project.mdmatch regardless of how the path is prefixed (worktree/or repo root).ctx.project.id === "global"(git-less directory), skip injecting the restricted agent, or surface a message telling the user togit initand restart.bashallowlist could includegit initso the agent can fix the precondition itself, then instruct a restart.