This document proposes a first-class skill management layer for the skills CLI that adds:
- Scope-aware enable/disable operations for installed skills
- User-defined skill groups
- A single protected
skills-managerskill per scope - Management-aware listing and lifecycle behavior
The design keeps the current install/update/remove model intact and layers management state on top of it.
The key decisions are:
- Disabled state is derived from on-disk location, not stored in lockfiles
- A disabled skill is represented by moving its installed entry from
skills/todisabled_skills/ - Groups are multi-membership collections of skill names
- Project and global scopes remain completely separate
- Group membership and the designated
skills-managerskill are stored in both lockfiles - Lifecycle commands must preserve group membership and disabled state when reinstalling an existing skill in place
- Removing a skill must fully scrub it from the relevant lockfile, including group membership and
skills-managerdesignation
The skills CLI already supports installation, discovery, update checks, removal, local project restore, and node_modules sync. What it lacks is a lightweight way to control which already-installed skills are active without uninstalling them, and a way to organize installed skills into reusable collections.
That gap matters because harnesses typically frontload skill metadata into the prompt. Users need a way to keep many skills installed while keeping only a chosen working set active.
| Command | Aliases | Purpose | Lockfile Relationship |
|---|---|---|---|
skills add <source> |
a, install, i |
Install skills from repo/path/well-known source | Writes global lock for global installs, writes project lock for project installs |
skills remove [skills...] |
rm, r |
Remove installed skills | Currently removes from global lock only; project lock cleanup must be added |
skills list |
ls |
List installed skills | Reads filesystem and global lock plugin metadata |
skills find [query] |
search, f, s |
Search remote skills | No lockfile mutation |
skills check |
none | Check for updates | Reads global lock only |
skills update |
upgrade |
Reinstall updated global skills | Reads global lock only |
skills experimental_install |
none | Restore project skills from skills-lock.json |
Reads project lock only |
skills experimental_sync |
none | Sync node_modules skills into project scope | Reads and writes project lock |
skills init [name] |
none | Scaffold a SKILL.md |
No lockfile mutation |
The current CLI surface already includes the following relevant flags:
add:-g,-a,-s,-l,-y,--copy,--all,--full-depthremove:-g,-a,-y,--alllist:-g,-a,--jsonexperimental_sync:-a,-y,-f
This design keeps the existing scope convention:
- Project scope is the default
- Global scope is selected with
-g/--global
There are currently two persistent lockfiles:
- Path:
<cwd>/skills-lock.json - Current version:
1 - Purpose: reproducible project restore
- Current contents:
skillsmap with source metadata andcomputedHash - Project-scope groups and
skills-managerdesignation must be persisted only in this file
- Path:
$XDG_STATE_HOME/skills/.skill-lock.jsonwhenXDG_STATE_HOMEis set; otherwise~/.agents/.skill-lock.json - Current version:
3 - Purpose: global install tracking and update checks
- Current contents:
skillsmap with source metadata, timestamps,skillFolderHash, optionalpluginName, plus global-only fields likedismissedandlastSelectedAgents - Global-scope groups and
skills-managerdesignation must be persisted only in this file - This global state file may continue to store global UX and preference state such as
dismissedprompts orlastSelectedAgents, but it must not store project management state. Only global management state and groups.
Installed skills are materialized in a canonical skill directory and may also appear in agent-specific directories:
- Project canonical path:
<cwd>/.agents/skills/<skill> - Global canonical path:
~/.agents/skills/<skill>or XDG equivalent - Universal agents share the canonical directory
- Non-universal agents either symlink to the canonical directory or get copied files when
--copyis used or symlinks fail
This matters because enable/disable must work for both:
- Symlinked installs, where the canonical skill directory and any agent-specific symlink entries must move between active and disabled roots together
- Copied installs, where every materialized copy must move between the active and disabled roots
- Let users enable and disable installed skills without uninstalling them
- Let users define named groups of skills
- Support both project and global management with separate state
- Preserve management state across reinstall-like lifecycle commands
- Keep
skills listfamiliar and low-friction - Keep the design orthogonal to existing
add,remove,update,experimental_install, andexperimental_syncbehavior
- Agent-specific enable/disable state in v1
- Group-level stored enabled/disabled state
- Automatic group assignment during fresh install
- Changing how
checkandupdatedecide what is updatable - Redesigning the broader install layout beyond adding a parallel
disabled_skillsdirectory
Disabled state is derived from on-disk location:
- Enabled:
<scope-root>/skills/<skill>/SKILL.md - Disabled:
<scope-root>/disabled_skills/<skill>/SKILL.md
Enabling or disabling a skill moves its installed entry between those sibling roots. The SKILL.md file itself is unchanged.
For example, disabling project skill api-design moves it from <cwd>/.agents/skills/api-design to <cwd>/.agents/disabled_skills/api-design.
No lockfile field stores enabled/disabled state.
This keeps the runtime source of truth aligned with what harnesses actually load, because harnesses continue to discover active skills from the skills/ tree only.
Groups are named collections of skill names.
- A skill may belong to zero, one, or many groups
- Group membership is scope-local
- A group has no independent enabled/disabled flag
- A group is considered "enabled" when all of its installed members are enabled
Project and global management are independent.
- The same group name may exist in both scopes
- Membership is not shared across scopes
- Each scope may have a different
skills-managerskill
Each scope may designate at most one special skill as skills-manager.
Rules:
- It cannot be disabled
- It cannot be added to any group
- Bulk disable operations skip it
- Explicit attempts to disable it fail with a clear error
Bulk commands are best-effort:
- Successful targets are applied
- Invalid or missing targets are reported individually
- Protected
skills-managerskips in bulk are reported as skips, not failures - Any true failures produce a non-zero exit code
This design uses the Option 1 command family.
skills enable foo bar
skills disable foo barskills enable --group ai architecture
skills disable --group ai architectureskills enable --all
skills disable --allskills group create ai architecture
skills group delete ai architectureskills group add ai --skill foo bar
skills group remove ai --skill foo barskills manager set find-skills
skills manager show
skills manager clearEvery management command supports scope selection the same way existing install/remove/list commands do:
skills disable foo -g
skills group create ai -g
skills manager set find-skills -genable and disable accept exactly one selector type per invocation:
- Positional skill names
--group <groups...>--all
Mixed selectors are rejected. For example:
skills disable foo --group aiis invalidskills disable --group ai --allis invalid
This keeps command behavior easy to reason about.
The existing layout stays the same. The only change is a status prefix before the skill name.
Enabled:
[+] api-design ~/dev-projects/skills/.agents/skills/api-design
Agents: Claude Code, Codex, Cursor, GitHub Copilot, OpenCode
Disabled:
[-] browser-testing ~/dev-projects/skills/.agents/disabled_skills/browser-testing
Agents: Claude Code, Codex, Cursor, GitHub Copilot, OpenCode
Notes:
- The command keeps the current structure, headings, path rendering, and agent lines
- Existing plugin-based grouping in normal list output stays intact
- The only new default signal is
[+]or[-]
This new mode shows group-centric output:
ai (5/5 enabled)
[+] ai-gateway
[+] ai-generation-persistence
[+] ai-sdk
[+] develop-ai-functions-example
[+] ai-architecture-patterns
architecture (3/5 enabled)
[+] nodejs-backend-patterns
[-] openapi-spec-generation
[+] request-refactor-plan
[-] typescript-advanced-types
[+] ai-architecture-patterns
UNGROUPED SKILLS (1/4 enabled)
[-] two-factor-authentication-best-practices
[+] deployments-cicd
[-] drizzle-orm
[-] email
Ordering:
- Groups are listed alphabetically
- Skills within each group are listed alphabetically
UNGROUPED SKILLSis always last
Empty groups are shown:
ai (0/0 enabled)
The two main steady states are:
[+]enabled[-]disabled
Two additional drift states are useful for diagnostics:
[!]inconsistent across copies or invalid on disk[?]referenced by management metadata but missing on disk
[!] and [?] are primarily relevant to skills list --groups, where lockfile metadata may outlive manual filesystem edits.
Per skill within a concrete install root:
skills/<skill>/SKILL.mdpresent anddisabled_skills/<skill>absent => enableddisabled_skills/<skill>/SKILL.mdpresent andskills/<skill>absent => disabled- Both present or neither present => invalid/inconsistent
Management commands operate on a skill across the full selected scope, not per agent.
To do that safely, the CLI must enumerate every concrete installed copy of a skill in the selected scope:
- Canonical directory
- Agent-specific copies created by
--copy - Agent-specific fallback copies created when symlinks failed
For symlinked installs, the implementation should treat the canonical directory and agent-specific alias entries separately:
- The canonical target should only be moved once
- Agent-specific symlink entries should still move between
skills/anddisabled_skills/so discovery paths remain clean
Disabled state intentionally lives in the filesystem because harnesses already key off the active skills/ discovery tree.
This has one important consequence:
- In-place reinstall/update operations can preserve disabled state by snapshotting whether each installed entry currently lives in
skills/ordisabled_skills/and restoring that placement afterward - A clean restore into an empty scope cannot reconstruct disabled state from lockfiles alone
That tradeoff is accepted in this design.
Bump project lock version from 1 to 2.
Proposed shape:
{
"version": 2,
"skills": {
"api-design": {
"source": "vercel-labs/skills",
"sourceType": "github",
"computedHash": "abc123"
}
},
"management": {
"groups": {
"ai": ["api-design", "browser-testing"]
},
"managerSkill": "find-skills"
}
}Bump global lock version from 3 to 4.
Proposed shape:
{
"version": 4,
"skills": {
"api-design": {
"source": "vercel-labs/skills",
"sourceType": "github",
"sourceUrl": "https://github.com/vercel-labs/skills",
"skillFolderHash": "tree-sha",
"installedAt": "2026-04-07T00:00:00.000Z",
"updatedAt": "2026-04-07T00:00:00.000Z"
}
},
"dismissed": {},
"lastSelectedAgents": ["codex"],
"management": {
"groups": {
"ai": ["api-design", "browser-testing"]
},
"managerSkill": "find-skills"
}
}management.groupskeys are stored sorted alphabetically- Group member arrays are sorted alphabetically and deduplicated
managerSkillis optional- Group names are case-insensitive at the CLI boundary and stored canonically as lowercase CLI-friendly identifiers
- Skill names in groups are stored by canonical skill name as used in the lockfile
The current implementation wipes older lockfile versions. That is too destructive for this feature.
This design should add explicit migrations:
- Project
v1->v2by preservingskillsand adding emptymanagement - Global
v3->v4by preservingskills,dismissed, andlastSelectedAgents, then adding emptymanagement
Only invalid or unsupported lockfiles should be reset.
Supported forms:
skills enable foo barskills enable --group ai architectureskills enable --all
Behavior:
- Expands the selector to concrete skill names in the selected scope
- Ignores duplicates after expansion
- Skips the
skills-managerskill in bulk if it is already enabled - Moves each installed skill entry from
disabled_skills/toskills/in every concrete installed location - Reports missing or inconsistent copies as failures
Supported forms:
skills disable foo barskills disable --group ai architectureskills disable --all
Behavior:
- Expands the selector to concrete skill names in the selected scope
- Explicitly targeting the
skills-managerskill fails - Bulk operations skip the
skills-managerskill and report the skip - Moves each installed skill entry from
skills/todisabled_skills/in every concrete installed location - Reports missing or inconsistent copies as failures
Supported form:
skills group create ai architectureBehavior:
- Creates one or more empty groups in the selected scope
- Fails only for invalid group names or already-existing groups
- Empty groups are first-class and remain until explicitly deleted
Supported form:
skills group delete ai architectureBehavior:
- Deletes the named groups from the selected scope
- Does not uninstall skills
- Does not change skill enabled/disabled state
- Deleting a group only removes membership metadata
Supported form:
skills group add ai --skill foo barBehavior:
- Requires the target group to already exist
- Adds one or more installed skills to the group
- Disabled skills may be grouped
- The
skills-managerskill is rejected - Missing or unknown skills are reported individually
- Duplicate membership is ignored
Supported form:
skills group remove ai --skill foo barBehavior:
- Removes one or more skills from the named group
- If the group becomes empty, it remains as an empty group
- Missing skills are reported individually
Supported form:
skills manager set find-skillsBehavior:
- Requires the target skill to exist in the selected scope
- Ensures the target skill ends in an enabled state
- If the skill currently belongs to groups in the selected scope, it is removed from those groups and the command reports that cleanup
- Replaces any previous
managerSkillin the selected scope
Rationale:
- The command is explicit
- Auto-removing the skill from groups avoids a tedious two-step workflow
- The cleanup is local, deterministic, and easy to report
Behavior:
- Prints the designated
managerSkillfor the selected scope - If none is set, prints a clear "not set" message
- If the lockfile points at a skill that is missing on disk, prints the name plus a warning
Behavior:
- Clears the
managerSkillfield in the selected scope - Does not touch groups
- Does not change enabled/disabled state
Fresh installs do not infer groups.
However, when add reinstalls a skill that already exists in the selected scope, it must preserve:
- Existing group membership from the relevant lockfile
- Existing
skills-managerdesignation if the reinstalled skill is the manager skill - Existing disabled state by snapshotting the current active-vs-disabled location before replacement and restoring it afterward
This allows skills add to behave sensibly as both an install and a reinstall mechanism.
update remains global-only and source-driven exactly as today.
For each updated skill:
- Existing global management metadata is preserved
- If the skill was disabled before update, it is disabled again after update completes
- If the skill is the global
skills-manager, it remains protected and enabled
experimental_install remains project-only.
Behavior:
- Group metadata and
managerSkillcome from the project lockfile - If reinstalling over an existing disabled skill directory, disabled state is preserved
- If restoring into an empty project from lockfile only, disabled state cannot be recreated because it is intentionally not stored in the lockfile
experimental_sync should follow the same preservation rule as other reinstall-like commands in project scope:
- Preserve existing group membership
- Preserve existing
managerSkill - Preserve disabled state when syncing over an existing installed skill
remove must be updated so that it becomes the authoritative scrub operation for both scopes.
For every successfully removed skill:
- Delete installed files from canonical and agent-specific locations
- Remove the skill entry from the relevant lockfile
- Remove the skill from all groups in the relevant scope
- Clear
managerSkillif it pointed to the removed skill
Important correction:
- Project-scope remove must also mutate
skills-lock.json - This is currently missing from the codebase and should be fixed as part of this work
This document intentionally does not auto-prune stale management metadata during read-only commands.
Chosen behavior:
- Preserve metadata by default
- Surface the inconsistency to the user
- Let explicit commands clean it up
Details:
skills listcontinues to show installed skills onlyskills list --groupsmay show[?] <skill> (missing)when a group references a skill that no longer exists on disk- Group enabled counts exclude missing skills from the enabled numerator but include them in the total denominator
- A warning footer should recommend cleanup
Example:
ai (4/5 enabled)
[+] api-design
[+] browser-testing
[+] data-loader
[+] prompt-refactor
[?] stale-skill (missing)
Cleanup path:
skills remove stale-skillin the relevant scope should still scrub lockfile metadata even if the files are already gone
This is preferable to silent auto-pruning because it avoids surprising data loss and keeps management writes explicit.
skills list --json should gain management-aware fields.
Proposed per-skill JSON shape:
{
"name": "api-design",
"path": "/abs/path/.agents/skills/api-design",
"scope": "project",
"agents": ["Codex", "Cursor"],
"status": "enabled",
"groups": ["ai", "architecture"],
"isManager": false
}skills list --groups --json can return a grouped structure:
{
"groups": {
"ai": [{ "name": "api-design", "status": "enabled" }]
},
"ungrouped": [{ "name": "email", "status": "disabled" }],
"managerSkill": "find-skills",
"warnings": []
}- Add read/write helpers for
management.groupsandmanagement.managerSkill - Add explicit lockfile migrations
- Add sorted deterministic writing for groups and members
- Add helper APIs for:
- resolving group membership
- resolving
managerSkill - scrubbing a removed skill from management state
- Add a helper to detect enabled, disabled, inconsistent, and missing states
- Add a helper to enumerate all concrete installed copies for a skill in a scope
- Track canonical targets separately from agent-specific symlink alias entries
- Add move helpers that switch installed entries between
skills/anddisabled_skills/
- Add
enable - Add
disable - Add
group create - Add
group delete - Add
group add - Add
group remove - Add
manager set - Add
manager show - Add
manager clear
- Update
listand--json - Add
list --groups - Update
removeto scrub project and global management state - Update
add,update,experimental_install, andexperimental_syncto preserve management state across reinstalls
- Lockfile migration tests
- Group CRUD tests for both scopes
- Manager skill tests for both scopes
- Enable/disable tests for:
- canonical installs
- symlink installs
- copy installs
- mixed and invalid states
listandlist --groupsoutput tests- Reinstall preservation tests for:
addupdateexperimental_installexperimental_sync
- Remove cleanup tests for:
- project lockfile
- global lockfile
- group scrubbing
- manager clearing
- Missing/manual deletion edge-case tests
The biggest tradeoff in this design is the decision to derive disabled state entirely from on-disk location instead of storing it in lockfiles.
That buys:
- Very simple runtime semantics
- Direct compatibility with harnesses that load from the active
skills/tree - No duplicated source of truth
It costs:
- No portable disabled-state restore from an otherwise empty lockfile-only install
If portable disabled state later becomes a strong requirement, the clean extension point is to add explicit disabled metadata in a future lockfile version. That is intentionally out of scope for this design.
Proceed with this design as a management-focused extension to the existing CLI.
It introduces meaningful new control without changing the mental model of how skills are installed, restored, or updated. It also keeps the prompt-window problem front and center: users can keep many skills installed while only activating the ones they want loaded by their harness.