docs/design/2026-07-20-skills-default-disabled.md
skills.disabled is a case-insensitive union across settings scopes. That makes it a hard denylist: a project cannot enable a skill disabled by user or system settings. This is correct for policy, but it cannot represent a skill that should start off and remain available for project opt-in.
Add two case-insensitive union lists while keeping skills.disabled unchanged:
| Setting | Meaning |
|---|---|
skills.disabled | Hard disable. Always wins and preserves existing locks. |
skills.defaultDisabled | Disabled unless explicitly enabled. |
skills.enabled | Explicit opt-in; cannot override skills.disabled. |
Effective disables are disabled + (defaultDisabled - enabled). An explicit enabled list is used instead of replacement semantics so enabling one inherited default does not replace unrelated defaults.
One CLI-local resolver computes the effective disabled names and whether each disabled skill is hard or default. Existing runtime consumers continue reading the effective set through Config.getDisabledSkillNames(); core skill discovery and execution APIs do not change.
The /skills picker and daemon toggle apply the same rules:
skills.enabled only when needed;skills.disabled;skills.disabled entries remain locked;Workspace skill status adds a disable reason and optional lock scope so clients can distinguish a hard lock from an overridable default. The daemon-local and ACP status paths both read the same CLI-local resolver.
defaultDisabled by this change.disable-model-invocation and managed-skill ACP operations are unchanged.skills.disabled configuration remains compatible.