Skip to content

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.

{ "action": "skill", "resource": "windows-shell", "effect": "deny" } // skill_management
FieldMeaning
actionThe permission action. Skill permissions use the literal skill.
resourceThe skill id. Exact id, or a glob pattern.
effectallow, 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" }
]
}

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. disable appends.
  • A broad rule can beat a specific one if it comes later. inspect calls 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, not allow.
no matching rule → ask

That fallback is why ask is ambiguous:

What you seePossible cause
askAn explicit { action: "skill", resource: "<id>", effect: "ask" } rule matched.
askNo rule matched at all; the host fell back to ask.

list shows ask for both. inspect distinguishes them by reporting 命中 rules[i] or 无规则命中 → 回退 ask.

EffectModel-visible skill listLoading through the skill tool
allowListedAllowed
askListedAsks for confirmation
denyHiddenRejected (permission.rejected)

Both stop a skill from being loaded silently, but they stop it in different places.

denyask
Model-visible listHiddenStill listed, still described
Load attemptRejected (permission.rejected)Blocks on a confirmation
Interrupts the turnNo — it just failsYes — someone has to answer
Who can overrideA later rule in the caller’s own rule set (e.g. an agent-level allow), or a policies entryA later rule in the caller’s own rule set
Typical useA skill you do not want in context at allA 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:

SkillWhy it needs a switchEffect
codebuddy-second-opinionShells out to another model and spends quota/creditsask — useful when you asked for a second opinion, wasteful when the model decides on its own
disk-cleanScans caches and actually deletes filesask for review; deny if you never want the model reaching for it
meld-deepresearchNetwork access and long, expensive runsdeny — 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.

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:

  • list shows the caller’s state and says other visible agents differ (hidden internal agents are only listed in inspect).
  • inspect returns the per-agent results in agents[].
  • disable reports 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.

The plugin resolves the global config in this order:

  1. <config> is $OPENCODE_CONFIG_DIR if that is set, else $XDG_CONFIG_HOME/opencode if XDG_CONFIG_HOME is set, else ~/.config/opencode (C:\Users\<you>\.config\opencode on Windows).
  2. Candidates are opencode.json and opencode.jsonc in <config>, plus the same names in ~/.config/opencode when that is a different directory. opencode.jsonc is preferred over opencode.json.
  3. If both locations contain a permissions array, the plugin picks the ~/.config/opencode file and adds a warning to its output.
  4. If no candidate has permissions, it uses the last existing candidate, or creates <config>/opencode.jsonc if none exists.

opencode.managed.json is never written. That is OpenChamber’s second, managed global config; editing it would fight the host.

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/.

<config>/skill_management.state.json records which rules this tool wrote (id, effect, file, timestamp). It is a hint, not the source of truth:

  • enable prefers the in-file marker comment and only falls back to the sidecar.
  • If the sidecar and config disagree, list reports 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.

PatternMatches
windows-shellExactly 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.

{
"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.