Permission reference¶
Ship this file with the package so the approval rules can be read before the plugin is installed, not only after it has already changed a session.
The permission hook never grants authority on its own: it runs after the rules OpenCode has already resolved and can only escalate an allow to ask; it never turns a configured ask or deny into allow. The ruleset this package ships is a separate thing: ordinary OpenCode configuration that you read before it is merged, with ask and allow entries the host resolves the same way it resolves your own rules.
Shipped rules¶
permissions.json holds the ruleset that opencode-rig init merges into your configuration, that opencode-rig check reports on, and that opencode-rig print-config prints.
Rules that stop¶
| Rule | Effect | Why it is here |
|---|---|---|
git push* |
ask |
Publication is human-owned. Bare git push and any refspec both stop. |
git reset --hard* |
ask |
Discards staged and working-tree changes without recovery. |
git clean* |
ask |
Deletes untracked files. The whole class stops, git clean -n included. |
git rebase* |
ask |
Rewrites commits that other branches may already contain. |
* --output=* |
ask |
git log, git show, and git rev-list write a file when they are given --output=FILE. |
* --output * |
ask |
The same option in the space-separated form. |
The two --output rules are the only ones that name no command. They exist because git log --output=FILE, git show --output=FILE, and git rev-list --output=FILE each create that file, and the read-only allowlist below would otherwise match them. They sit at the end of the ruleset so the last matching rule is the one that asks: a read-only inspection command that accepts --output now asks for an answer instead of writing silently.
A rule of your own still wins over them, exactly as it wins over every other shipped rule: the shipped rules are inserted ahead of yours in the file you are merged into.
Rules that allow¶
| Rule | Effect | Why it is here |
|---|---|---|
git rebase --abort |
allow |
Recovery, not a rewrite. The hook already lets this through. |
git rebase --continue |
allow |
Recovery after resolving a conflict. |
git rebase --skip |
allow |
Recovery by dropping the current patch. |
git rebase --quit |
allow |
Recovery that abandons the rebase state. |
The four allow entries exist so the blanket git rebase* rule does not stop the recovery commands a rebase needs to finish. OpenCode resolves the last matching rule, so they must stay after git rebase*; the ordering is documented in the OpenCode v2 permission documentation.
A plain local commit is deliberately absent. git commit is governed by the guardrail hook instead of a blanket rule, so a recorded grant can take effect; adding git commit* here would pin every commit to ask and make the grant useless.
The read-only shell allowlist¶
Every rule below is allow, and every one of them names a command that only reads. They exist because each role agent ships its own shell: * catch-all that asks, so without them every inspection command in a role agent stops for an answer.
The list lives in one place. opencode-rig init appends the shipped ruleset to each role agent, after that agent's own rules, so the list cannot drift per agent and a command that is not on it still asks. The shipped order is kept, so the two --output rules land after the allows they override and an agent resolves git log --output=FILE to ask as well.
Git inspection:
git show *
git log *
git status *
git blame *
git rev-parse *
git rev-list *
git ls-files *
git ls-tree *
git cat-file *
git describe *
git shortlog *
git show-ref *
git for-each-ref *
git merge-base *
git check-ignore *
git check-attr *
git count-objects *
git worktree list *
git stash list *
git config --get *
git var *
File and text inspection:
cat *
head *
tail *
grep *
wc *
cut *
tr *
comm *
cmp *
diff *
nl *
basename *
dirname *
realpath *
which *
type *
stat *
file *
du *
df *
date
ls *
pwd *
jq *
shasum *
sha256sum *
uniq *
echo *
printf *
A command whose wildcard also admits a writing or an executing form is not on the list: git diff (--output=FILE writes a file), git grep (--open-files-in-pager=<command> executes a command), git remote -v (git accepts -v before a mutating subcommand), rg (--pre <command> runs an arbitrary program before the search), sort (-o FILE), tree (-o DIR), sed and awk (-i, or a redirection inside the program), find (-exec, -delete), tee, xargs, env, and every interpreter or package manager. The Git families git branch, git tag, git stash, git config, and git remote are excluded for the same reason: their wildcard also matches their mutating forms. Two read-only forms of those families are on the list because their wildcard cannot reach a mutating form: git stash list * and git config --get *. git pull and git fetch write to the repository.
date is on the list without a wildcard on purpose: GNU date -s sets the system clock on Linux, so only the bare command is allowed and date -u still asks.
Two channels the allowlist does not close¶
Both come from how the host resolves a rule, not from a defect in this list.
- A redirection attached to an allowed command still matches.
cat a > bmatchescat *, and the shell performs the write. The list matches a command string, not its effect. Six of the seven role agents declareedit: *asdeny, and that rule does not cover a redirection, so inside those agents a redirection is a write path their own read-only contract does not close. - An agent rule is appended after the global rules. The OpenCode v2 permission documentation states that OpenCode combines rules in order and uses the last match: lower-priority configuration first, global rules next, agent rules last. An agent
allowtherefore outranks a globaldenyfor the same command. That consequence is the documented one; the composition order was not confirmed against the installed host binary, so read it as documented behaviour rather than a verified one. The host's own shipped agents append their permissions the same way, which is why this package does it too. The shipped ruleset contains nodenyrule, so the list contains no command that a shipped rule denies, and this only matters if you add a globaldenyfor a command on the list.
What the hook escalates¶
These run regardless of the rules above, because the hook classifies the command itself. They are listed so the shipped file can be reviewed against them rather than trusted on its own.
- Remote mutation —
git push,git remote add|remove|set-url|prune|update,gh pr create|merge|close|reopen|edit|comment|review|ready,gh release create|upload|delete|edit,gh repo create|delete|edit|rename|archive,npm publish,uv publish,twine upload,docker push, and agit tagthat is pushed. - History rewrite —
git rebaseoutside--abort|continue|skip|quit,git commit --amend,git filter-repo,git filter-branch,git replace. - Destructive local —
git reset --hard, agit cleancarrying-f,-d, or-x,git branch -D,git branch --delete --force, a forcedgit checkout(-for--force), agit checkout -- <path>that overwrites the working tree,git restoreoutside--staged,git stash drop|clear, a forcedgit worktree remove, recursiverm,find -delete, andtruncate -s 0. - Unreviewed commit —
git commitwith no recorded grant.
In strict mode the hook additionally escalates git merge and git cherry-pick.
The git clean rule stops the whole class, the hook only stops a git clean that carries -f, -d, or -x. So git clean -n, git clean -i, and a bare git clean are new stops that the configuration rule introduces and the hook never provided.
With guardrails set to off the hook stops escalating, and the shipped ask rules become the only stops for push, hard reset, clean, rebase, and the --output writes. Turning the hook off therefore removes the hook's stops and keeps the configuration's; the shipped rules still ask.
What still asks¶
Outside a role agent, OpenCode starts permissive, so most actions default to allow. You do not need an allow list for routine work there:
- Reading, globbing, and searching the project.
- Editing project files.
- Shell commands, including
git status,git diff,git log, test runners, linters, and build tools. external_directoryis allowed by default, not asked. The v2 documentation says no default rule asks for it, so a read or an edit outside the Location and worktree proceeds without a prompt. Add{ "action": "external_directory", "resource": "*", "effect": "ask" }to your own configuration to be asked.doom_loopandlspare not v2 Core permission actions, so a rule naming either of them does nothing.
Inside a role agent the shell is different: each role agent declares shell: * as ask, so anything outside the allowlist above asks, including test runners, linters, and build tools. The allowlist covers inspection, not execution.
Plain commits and the grant¶
git commit asks until an approval exists, and the approval is durable and project-scoped rather than a per-session answer. The grant below is one JSON file in the repository's common Git directory, outside version control and shared by every worktree, and the host's own Allow always answer is recorded the same way: the v2 documentation describes saved approvals as durable, project-scoped allow rules.
confirm must be the user's own words. The grant covers local commits only — never push, force, destructive operations, or history rewrites — and rig_git_grant action=revoke removes it. rig_git_grant action=status reports granted, absent, invalid, or no-git.
How init merges¶
opencode-rig init writes these rules into one configuration file. Without --project it targets the global configuration directory: ~/.config/opencode/opencode.jsonc when that file exists, otherwise ~/.config/opencode/opencode.json when that one exists, otherwise it creates ~/.config/opencode/opencode.jsonc. With --project it targets the current directory: opencode.jsonc when it exists, otherwise opencode.json when it exists, otherwise it creates opencode.jsonc. opencode-rig check resolves the same file, so the report and the merge always name the same one.
- Rules already present under the same action and resource are left as they are; your effect for that resource wins.
- Rules you wrote yourself are never removed, reordered, or weakened. Shipped rules are inserted ahead of yours, and OpenCode evaluates the last matching rule, so within that file your configuration keeps the final say.
- If you already declare a rule in the
git rebasefamily —git rebase*,git rebase --abort, or any other rule whose command starts withgit rebase— the four shipped recovery allows are not added. Your rule governs that whole family, sogit rebase --abortfollows your effect rather than the shipped one. The blanketgit rebase*stop is still added unless you declared that exact rule yourself. - That guarantee covers the rules in the file being merged. A
denyyou keep in another configuration file is not protected by it: cross-file precedence is your own configuration, and an agent's own rules are appended after all of them. - Comments outside the
permissionsarray, every other key, and the content of every rule survive. Comments inside thepermissionsarray are rewritten away, because the array is re-serialized. - Re-running
initis a no-op once the rules are present. - If the file still uses the deprecated singular
permissionkey,initleaves it untouched and prints the snippet instead, because mixing the two forms makes precedence ambiguous.
opencode-rig print-config prints the same snippet without writing anything, for a configuration you would rather edit by hand. opencode-rig check reports whether the shipped rules are present and never fails on the report, so a configuration you have reviewed and declined is not a blocker.
init rewrites the configuration file in place and keeps no backup of the previous content, so keep your own copy before the first merge. A file you already keep under version control already has one.