Skip to content

Bug report: openspec-plan agent cannot create/edit proposals in a git-less repo #14

Description

@jones77

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

  1. Create a directory that is not a git repo (no git init).
    mkdir /tmp/openspec-test && cd /tmp/openspec-test
    
  2. Start OpenCode there with the plugin installed.
  3. Select the openspec-plan agent and ask it to propose a new change, e.g. create openspec/changes/foo/proposal.md.
  4. 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)

  1. 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).
  2. 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.
  3. Bootstrap git: the plugin's bash allowlist could include git init so the agent can fix the precondition itself, then instruct a restart.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions