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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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