feat(skills): add per-profile skills management#711
Conversation
Enable per-profile skill isolation by leveraging CLAUDE_CONFIG_DIR. When skills_mode is set to 'isolated', the instance's skills symlink is replaced with a real directory where shared skills are individually symlinked and profile-specific skills can coexist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kaitranntt
left a comment
There was a problem hiding this comment.
Thanks for the contribution! The feature direction is reasonable (per-profile skills). I reviewed and found a couple of items that should be addressed before this can land. Full report attached.
🔴 Blockers (must fix)
1. Shell injection via spawn(..., { shell: true }) with user-supplied args — src/commands/skills-command.ts:55
The current spawn('npx', [...userInput], { shell: true }) lets shell metacharacters in user-supplied skill names execute against the host. For example:
ccs skills personal add 'foo; rm -rf ~'would interpret the ; as a command separator. Please switch to spawn('npx', [...args]) without shell: true, or sanitize/validate args strictly before passing.
2. Destructive share mode — src/management/shared-manager.ts:585
The share operation rm -rfs the profile-only skills directory with only a single warning line. There's no confirmation prompt, no dry-run, no backup. Switching modes is reversible from a UX standpoint, but the data deletion is not. Please add either:
- An interactive confirmation prompt (consistent with how other CCS destructive ops work), or
- A
--dry-runmode that lists what would be deleted, or - An automatic backup to a timestamped path before deletion.
🟡 Should address
3. Dashboard parity missing — CCS rule (CLAUDE.md "Feature Interface Requirements"): configuration features must work in both CLI and Dashboard. The new skills_mode toggle only has a CLI surface. Please add a Dashboard control, or add a follow-up issue and reference it here.
4. Windows symlink fallback untested for the per-skill case — CCS uses ~/.ccs/shared/ symlinks on Unix and copies on Windows. The new per-profile model needs to handle both. Please add Windows-path test coverage or document the behavior.
5. Cross-profile remove symlink behavior — When removing a per-profile skill that's actually a symlink into ~/.claude/, please verify the deletion does not cascade into the shared source. Add an explicit test for this case.
✅ What looks good
- No new dependencies in
package.json/ui/package.json - No new network calls, no token reads, no
eval/ base64 obfuscation ~/.claude/accessed read-only viarealpathenumeration- Type changes are backward-compatible (new field optional, validators updated)
getCcsDir()used correctly (noos.homedir()direct)- ASCII-only CLI output, TTY-aware UI helpers
- 7 tests cover the shared↔isolated boundary including profile-skill preservation
Architectural note (non-blocking)
This PR diverges slightly from the existing model where commands/agents/skills are all SHARED-only via ~/.ccs/shared/. By making skills opt-in per-profile, we introduce an exception. The default stays shared so it's backward compatible — but a brief design note in the PR body explaining why skills get this special treatment (or generalizing to a resources_mode for all three resource types) would help future maintainers.
Happy to re-review once the two blockers are addressed.
|
Following up, since
The earlier blockers still stand too: the The feature direction is still welcome. Are you still planning to take this forward? If so, a rebase plus the single owner change above is the way in. If not, no worries, just say the word and we can pick it up or close it out. |
Summary
skills_mode(shared|isolated) to account config, enabling per-profile skill isolationsyncSkills()in SharedManager that converts the instanceskills/symlink to a real directory with individually symlinked shared skillsccs skills <profile> <command>CLI that wrapsnpx skillswithCLAUDE_CONFIG_DIRtargeting the correct instance directoryccs skills <profile> add, sync shared skills duringccs syncHow it works
When
skills_mode: isolatedis set for an account profile:skills/symlink (→ shared → ~/.claude/skills) is replaced with a real directory~/.claude/skills/is individually symlinked into the instance directorynpx skills add -gruns withCLAUDE_CONFIG_DIRpointing to the instance, so new skills install directly into the isolated directoryChanged files
src/auth/account-context.tsAccountSkillsModetype,skillsModein policy/metadatasrc/config/unified-config-types.tsskills_modeonAccountConfigsrc/types/config.tsskills_modeonProfileMetadatasrc/management/shared-manager.tssyncSkills()+syncSharedSkillsToIsolated()src/management/instance-manager.tssyncSkills()inensureInstance()src/commands/sync-command.tsccs syncsrc/commands/skills-command.tsnpx skillssrc/ccs.tsskillssubcommandsrc/commands/help-command.tstests/unit/skills-sync.test.tsTest plan
bun run format— cleanbun run typecheck— no errorsbun run lint:fix— cleanbun run validate— exit 0 (pre-existing failures unrelated)bun run validate:ci-parity— passed (pre-push hook)ccs skills personal isolate→ccs skills personal add <pkg>→ccs skills personal listccs skills personal sharerestores shared modeccs syncpreserves isolated skills🤖 Generated with Claude Code