Universal Agents vs. Agent-Specific Symlinked Installations in the Skills CLI
Universal agents share a single skill directory and skip symlinks entirely, while agent-specific installations create symlinks from the canonical store to each agent's isolated folder.
The skills CLI from Vercel Labs manages AI skills across multiple agents using two distinct installation strategies. Understanding how universal agents versus agent-specific symlinked installations work helps you predict where files land, why some agents need symlinks, and how to troubleshoot installation issues.
How the Skills CLI Installs Skills
Every skill installation begins at the canonical location: .agents/skills/<skill-name> for project installs or $XDG_CONFIG_HOME/agents/skills/<skill-name> for global installs. From there, the CLI decides whether additional steps are needed based on the target agent's type.
This logic lives in src/installer.ts, which handles file writing, symlink creation, and fallback copying when symlinks aren't supported.
Universal Agents: Shared Directory, No Symlinks Needed
Universal agents read skills directly from the canonical .agents/skills directory. No per-agent symlinks are created because these agents are already configured to look in the shared location.
How Universal Agents Are Identified
In src/agents.ts, the getUniversalAgents() function filters the agent registry using two criteria:
// src/agents.ts
export function getUniversalAgents(): AgentType[] {
return (Object.entries(agents) as [AgentType, AgentConfig][])
.filter(
([_, config]) => config.skillsDir === '.agents/skills' && config.showInUniversalList !== false
)
.map(([type]) => type);
}
A universal agent must have:
skillsDirset to'.agents/skills'— the canonical shared pathshowInUniversalListnot explicitly set tofalse
Installation Behavior for Universal Agents
When installing to a universal agent, src/installer.ts returns early after writing to the canonical directory:
// src/installer.ts (symlink mode)
if (isGlobal && isUniversalAgent(agentType)) {
return {
success: true,
path: canonicalDir,
canonicalPath: canonicalDir,
mode: 'symlink',
};
}
The mode: 'symlink' here is slightly misleading — no actual symlink is created. The field indicates this path would be the symlink target for non-universal agents, but universal agents use the canonical path directly.
Agent-Specific Symlinked Installations: Isolated Folders with Symlinked Access
Non-universal agents maintain their own isolated skill directories. The CLI creates symlinks from the canonical store into each agent's private folder, preserving a single source of truth while respecting the agent's expected layout.
Installation Flow for Symlinked Agents
In src/installer.ts, when the target agent is not universal and the installation is global, the full symlink creation runs:
- Write skill files to the canonical directory
- Create the agent's skills directory if missing
- Create a symlink:
~/.agents/<agent>/skills/<skill-name>→../../.agents/skills/<skill-name> - Fall back to file copying if symlinks aren't supported
// src/installer.ts - conceptual flow for symlinked agents
const agentSkillsDir = path.join(AGENTS_DIR, agentType, 'skills');
const symlinkPath = path.join(agentSkillsDir, skillName);
// Create symlink to canonical location
fs.symlinkSync(relativePathToCanonical, symlinkPath);
Why Symlinks Instead of Copies?
Symlinks serve three purposes in the skills CLI architecture:
- Single source of truth — Update or delete the canonical skill, and all symlinked agents see the change immediately
- Disk efficiency — Skills aren't duplicated across every agent
- Agent isolation — Agents that require specific folder structures still get their expected layout
When symlinks fail (Windows without developer mode, certain CI environments), the CLI transparently falls back to full file copies.
CLI Output: How the Skills CLI Shows the Difference
When you run skills add, the CLI groups agents visually to make the installation behavior transparent. In src/add.ts, the splitAgentsByType function separates universal and symlinked agents:
// src/add.ts - splitAgentsByType
function splitAgentsByType(agentTypes: AgentType[]) {
const universal: string[] = [];
const symlinked: string[] = [];
for (const a of agentTypes) {
if (isUniversalAgent(a)) {
universal.push(agents[a].displayName);
} else {
symlinked.push(agents[a].displayName);
}
}
return { universal, symlinked };
}
Typical CLI output shows:
$ skills add vercel-labs/github-copilot
Installing github-copilot...
✓ Installed to .agents/skills/github-copilot
Available to:
universal: GitHub Copilot, Vercel CLI
symlink →: ChatGPT, Claude Desktop
This output makes it immediately clear which agents use the skill directly versus which receive symlinks.
Comparing Universal and Symlinked Installations
| Aspect | Universal Agents | Agent-Specific Symlinked |
|---|---|---|
| Skill directory | Shared .agents/skills |
Private ~/.agents/<name>/skills |
| Symlink created? | No | Yes (with copy fallback) |
Code path in installer.ts |
Early return after canonical write | Full symlink creation logic |
| Identification | skillsDir === '.agents/skills' and showInUniversalList !== false |
Any agent not matching universal criteria |
| Use case | Agents designed for shared skill discovery | Agents requiring isolated skill environments |
| Update propagation | Immediate for all universal agents | Follows symlinks from canonical source |
Practical Examples
Installing a Skill for All Universal Agents Globally
# Install globally to universal agents only
skills add vercel-labs/github-copilot -g
Result: Files written to ~/.config/agents/skills/github-copilot. No symlinks created. Universal agents like GitHub Copilot and Vercel CLI see the skill immediately.
Installing a Skill for a Specific Non-Universal Agent
# Install to a specific agent that requires symlinked access
skills add my-custom-skill --agent chatgpt
Result: Files written to ./.agents/skills/my-custom-skill, then symlinked to ~/.agents/chatgpt/skills/my-custom-skill.
Checking Agent Type Programmatically
import { isUniversalAgent, getUniversalAgents } from 'skills/src/agents';
// Check if a specific agent is universal
console.log(isUniversalAgent('copilot')); // true
// List all universal agents
console.log(getUniversalAgents()); // ['copilot', 'vercel-cli', ...]
Summary
- Universal agents share the canonical
.agents/skillsdirectory and require no symlinks — the skills CLI writes once and all universal agents immediately have access. - Agent-specific installations use symlinks to bridge the canonical store into private agent folders, maintaining single-source-of-truth while respecting agent isolation requirements.
- The distinction is controlled by agent configuration in
src/agents.ts, specificallyskillsDirandshowInUniversalListproperties. - The
src/installer.tsfile implements the branching logic: early return for universal agents, full symlink creation for others.
Frequently Asked Questions
How do I know if an agent is universal or requires symlinked installation?
Check the agent's configuration in src/agents.ts. Universal agents have skillsDir === '.agents/skills' and showInUniversalList !== false. You can also run skills add with verbose output — the CLI groups agents into "universal" and "symlink →" sections.
Can I force a symlink for a universal agent?
No. The skills CLI intentionally skips symlinks for universal agents because they already read from the canonical directory. Creating symlinks would be redundant and could cause path resolution issues.
What happens if symlinks aren't supported on my system?
The installer in src/installer.ts falls back to copying files when symlink creation fails. This ensures skills work on Windows without developer mode, in CI environments, or on filesystems without symlink support — though updates require re-installation rather than automatically following the symlink.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →