Summary
Two sources can publish skills with the same name, and installing the second one silently replaces the first. Until now the only workaround was renaming the folder by hand and deleting the lock entry, which stops updates.
skills add <source> --skill <name> --rename <new-name> installs one skill under a new name:
- The installed folder, the lock key, and the
namein the installedSKILL.mdall use the new name, so agents invoke it by that name. The source repo or local path is never modified. - The lock entry (project and global) records the source name in a new optional
skillNamefield. Lock files without the field read as before. update,check,experimental_install(including--frozen-lockfile) andexperimental_syncfind the skill upstream byskillNameand reinstall it under the lock key, so the new name survives.listshows(renamed from <name>);list --jsonand the API'slistSkillsreportrenamedFrom.- The API takes a map, so one call can rename several skills:
installSkills(source, { rename: { shadcn: 'shadcn-ui', button: 'ui-button' } }). Skills not in the map install as-is. Keys must match a skill being installed, and no two skills may end up with one name; both are checked before anything is written. - Install telemetry and the security audit keep using the source name.
npx skills add vercel-labs/json-render --skill shadcn --rename json-render-shadcn// skills-lock.json
"json-render-shadcn": {
"source": "vercel-labs/json-render",
"sourceType": "github",
"skillPath": "skills/shadcn/SKILL.md",
"skillName": "shadcn",
"computedHash": "..."
}--rename applies to exactly one selected skill (new diagnostic SKILLS_RENAME_MULTIPLE_SKILLS). The name must already be a safe folder name (sanitizeName(name) === name); otherwise it fails with a suggested name. This keeps the lock key, the folder, and SKILL.md identical.
In short: you can install a skill under your own name, and every command that reads the lock keeps using that name.
- refs #1802
- refs #1906
- refs #2394
- Supersedes the closed #369, which predates
skills-lock.jsonrestore, sync, and the API.
Change map
| Module | Before | After |
|---|---|---|
add.ts | Skills install under their own name | renameSkill() sets name to the new name and renamedFrom to the source name; installToTargets rewrites the installed SKILL.md; every lock writer adds skillName |
installer.ts, frontmatter.ts | - | setInstalledSkillName() / setFrontmatterName() replace only the frontmatter name line; symlinks are skipped, so link targets (sync's link mode, the user's source) are never written |
update.ts, skill-relocation.ts | Upstream skills matched by lock key | Matched by skillName ?? key for git, well-known and download sources, both scopes; reinstalled under the key only when the entry has skillName |
install.ts | Restore requests lock keys | Requests source names and installs each found skill under every lock key that points at it (the same skill can be installed as-is and renamed) |
sync.ts | Remote requests matched by key | Matched by source name; a sync-managed renamed entry is reinstalled under its key |
list.ts, api/ | - | renamedFrom in list output, installSkills({ rename: { [sourceName]: newName } }), InstalledSkillInfo.renamedFrom |
Boundaries and risks
| Invariant | Protection | Evidence |
|---|---|---|
| Entries never renamed behave exactly as before | Update only renames when the lock entry has skillName; restore only passes select when a group has a renamed entry | Existing update and install tests unchanged and passing |
| The user's source is never rewritten | Symlinks are skipped; a skipped result without a reason (source is the destination) is not rewritten | rename.test.ts: the source SKILL.md keeps name: alpha after a symlink-mode install |
| The lock hash tracks upstream content | computedHash is hashed from the source folder, before the rewrite | update reports "All project skills are up to date" right after an update (rename.test.ts) |
| Public API changes are additive only | tsnapi snapshot | Snapshot diff only adds rename (a map), renamedFrom, and SKILLS_RENAME_MULTIPLE_SKILLS (CLI only) |
Rollout and follow-up
None of these block merge:
- A renamed skill from a skills.sh pack installs from the pack's own copy, not from the original repo the pack points at.
- Skills shipped by npm packages (
node_modules) cannot be renamed;--renameonly applies toadd. - Global update with a rename is covered by a mocked unit test only.
add -gfrom a localfile://repo writes no global lock entry (existing behavior), so a real end-to-end global run needs a GitHub source. - Found while testing, not changed here: restoring a
file://git source fromskills-lock.jsonfails because the skill folder gets appended to the clone URL. It fails the same way without--rename, so the restore tests use a local-path source.
User prompts
- Model:
claude-opus-5-5, harness:Claude Code(in Paseo) - Asked whether skills can be renamed on install, with the rename stored in the lockfile and respected by install, sync, update, list, and so on, and whether there were existing PRs or issues.
- Chose the flag name
--rename, no prefix option in this PR, and to build it independently of #1914. - Asked to create a PR, then to resolve merge conflicts with
main. - Asked for
installSkillsto support renaming several skills at once (suggested a map).
Verification
pnpm type-check: passes (also at the first commit on its own).pnpm format:check: passes.pnpm build: passes; the tsnapi snapshot was updated withpnpm api:update(additive diff only).pnpm vitest run tests/rename.test.ts: 10 passed. Real git repos through the real CLI: add (copy and symlink modes),check, projectupdatewith an upstream change, restore,--frozen-lockfilerestore, the same skill installed as-is and renamed,list/list --json,remove, and both error cases.pnpm vitest run src/frontmatter.test.ts src/skill-relocation.test.ts tests/update.test.ts tests/api.test.ts: all passed (new: frontmatter rewrite cases, relocation byskillName, global update with a rename, APIrenamemap: one and several renames, swapping two names,renamedFrom, and each error case).pnpm test(after mergingmain): 1330 passed, 4 failed. The 4 failures (src/detect-agent.test.tsx3 for Cursor detection,src/use.test.tssingle local skill prompt) fail the same way on a clean checkout ofc413b44in this environment, likely because the tests run inside an agent session. Not related to this change.- Not verified: an end-to-end update against a live GitHub repo or a well-known endpoint (those paths are covered by unit tests only).
Written with an AI agent (Claude Code). A human gave the task and made the design choices above; the diff has not been reviewed by a human yet.
Security review details