Permissions & evaluation
Everything skill_management changes is a permission rule. Reading this page is enough to
predict what the tool will report before you run it.
The rule
Section titled “The rule”{ "action": "skill", "resource": "windows-shell", "effect": "deny" } // skill_management| Field | Meaning |
|---|---|
action | The permission action. Skill permissions use the literal skill. |
resource | The skill id. Exact id, or a glob pattern. |
effect | allow, deny or ask. |
Rules live in the top-level permissions array of the global config. This is the V2 form:
{ "permissions": [ { "action": "skill", "resource": "windows-shell", "effect": "deny" } ]}Evaluation
Section titled “Evaluation”The host evaluates the caller agent’s rule set from the end to the beginning and takes the
first match. Equivalently: the last matching rule wins. Both action and resource are
matched as globs (* and ?).
rules[0] { action: "skill", resource: "*", effect: "allow" }rules[1] { action: "skill", resource: "windows-shell", effect: "deny" }
→ windows-shell is denied (rules[1] is later)Consequences:
- Order is part of the semantics. Appending a rule gives it priority over earlier rules in
the same array.
disableappends. - A broad rule can beat a specific one if it comes later.
inspectcalls this out; adding another rule for the same id would not fix it. - There is no implicit allow. When nothing matches, the host falls back to
ask, notallow.
no matching rule → askThat fallback is why ask is ambiguous:
| What you see | Possible cause |
|---|---|
ask | An explicit { action: "skill", resource: "<id>", effect: "ask" } rule matched. |
ask | No rule matched at all; the host fell back to ask. |
list shows ask for both. inspect distinguishes them by reporting
命中 rules[i] or 无规则命中 → 回退 ask.
Effects
Section titled “Effects”| Effect | Model-visible skill list | Loading through the skill tool |
|---|---|---|
allow | Listed | Allowed |
ask | Listed | Asks for confirmation |
deny | Hidden | Rejected (permission.rejected) |
deny vs ask
Section titled “deny vs ask”Both stop a skill from being loaded silently, but they stop it in different places.
deny | ask | |
|---|---|---|
| Model-visible list | Hidden | Still listed, still described |
| Load attempt | Rejected (permission.rejected) | Blocks on a confirmation |
| Interrupts the turn | No — it just fails | Yes — someone has to answer |
| Who can override | A later rule in the caller’s own rule set (e.g. an agent-level allow), or a policies entry | A later rule in the caller’s own rule set |
| Typical use | A skill you do not want in context at all | A skill that is fine to use, but not on its own initiative |
ask is not simply a softer deny. It does not reduce the probability that the model tries to
use the skill: the skill is still in the list, still described, still a candidate. What ask buys
is a decision point; what it costs is an interruption.
Three real cases, and the reasoning that picks an effect:
| Skill | Why it needs a switch | Effect |
|---|---|---|
codebuddy-second-opinion | Shells out to another model and spends quota/credits | ask — useful when you asked for a second opinion, wasteful when the model decides on its own |
disk-clean | Scans caches and actually deletes files | ask for review; deny if you never want the model reaching for it |
meld-deepresearch | Network access and long, expensive runs | deny — a mistaken invocation costs far more than the convenience is worth |
The granularity is the skill id, not an individual file or invocation. A skill whose id does
not exist fails with Unable to load skill <id>, which is a different error from a permission
rejection.
Whose rules are evaluated
Section titled “Whose rules are evaluated”Each agent has its own effective rule set. The plugin evaluates only the calling agent; it
never merges every agent’s rules into one list. Merging is wrong because sub-agents
(explorer, title, …) carry their own deny rules that do not apply to the caller.
A typical divergence: the global config denies a skill, but one agent has its own later allow
or vice versa. In that case:
listshows the caller’s state and says other visible agents differ (hidden internal agents are only listed ininspect).inspectreturns the per-agent results inagents[].disablereports that the rule was written but the caller’s actual effect is still different, instead of claiming success.
Rules set in agents.<id>.permissions are not edited by this tool. They are only reported.
Which config file is written
Section titled “Which config file is written”The plugin resolves the global config in this order:
<config>is$OPENCODE_CONFIG_DIRif that is set, else$XDG_CONFIG_HOME/opencodeifXDG_CONFIG_HOMEis set, else~/.config/opencode(C:\Users\<you>\.config\opencodeon Windows).- Candidates are
opencode.jsonandopencode.jsoncin<config>, plus the same names in~/.config/opencodewhen that is a different directory.opencode.jsoncis preferred overopencode.json. - If both locations contain a
permissionsarray, the plugin picks the~/.config/opencodefile and adds a warning to its output. - If no candidate has
permissions, it uses the last existing candidate, or creates<config>/opencode.jsoncif none exists.
opencode.managed.json is never written. That is OpenChamber’s second, managed global
config; editing it would fight the host.
Minimal text edits
Section titled “Minimal text edits”The config is not parsed into an object and re-serialized. The plugin inserts or removes the specific item as text, so comments, indentation, key order, trailing commas and the file’s own CRLF/LF and BOM survive. The result is parsed and validated before it is written.
Writes are atomic: a temp file in the same directory is renamed into place. Before writing, one
backup is saved under <config>/skill_management.backups/.
The sidecar
Section titled “The sidecar”<config>/skill_management.state.json records which rules this tool wrote (id, effect, file,
timestamp). It is a hint, not the source of truth:
enableprefers the in-file marker comment and only falls back to the sidecar.- If the sidecar and config disagree,
listreports the mismatch and does not “replay” anything.
The same-line marker is deliberate: it is the only provenance that survives a lost sidecar, and its format is part of the 1.x compatibility promise.
Patterns
Section titled “Patterns”| Pattern | Matches |
|---|---|
windows-shell | Exactly that id |
* | Every skill |
pdf-* | Ids starting with pdf- |
? | Exactly one character |
Prefer exact ids. A wildcard deny is reported by enable and never deleted automatically,
because deleting it would change more than the one skill you named.
Hand-written examples
Section titled “Hand-written examples”{ "permissions": [ // hide one skill completely { "action": "skill", "resource": "windows-shell", "effect": "deny" },
// keep it listed, ask on load { "action": "skill", "resource": "pdf-tools", "effect": "ask" } ]}A hand-written rule without the marker can still be removed by enable if the sidecar has a
matching record and the rule is unique. Otherwise enable prints it for you to remove yourself.
Next: Troubleshooting & FAQ for the failure modes in practice.