diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs
index d05882c9c..5c01bb217 100644
--- a/get-shit-done/bin/lib/init.cjs
+++ b/get-shit-done/bin/lib/init.cjs
@@ -1456,6 +1456,8 @@ function cmdInitRemoveWorkspace(cwd, name, raw) {
*/
function buildAgentSkillsBlock(config, agentType, projectRoot) {
const { validatePath } = require('./security.cjs');
+ const os = require('os');
+ const globalSkillsBase = path.join(os.homedir(), '.claude', 'skills');
if (!config || !config.agent_skills || !agentType) return '';
@@ -1470,6 +1472,37 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
for (const skillPath of skillPaths) {
if (typeof skillPath !== 'string') continue;
+ // Support global: prefix for skills installed at ~/.claude/skills/ (#1992)
+ if (skillPath.startsWith('global:')) {
+ const skillName = skillPath.slice(7);
+ // Explicit empty-name guard before regex for clearer error message
+ if (!skillName) {
+ process.stderr.write(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`);
+ continue;
+ }
+ // Sanitize: skill name must be alphanumeric, hyphens, or underscores only
+ if (!/^[a-zA-Z0-9_-]+$/.test(skillName)) {
+ process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`);
+ continue;
+ }
+ const globalSkillDir = path.join(globalSkillsBase, skillName);
+ const globalSkillMd = path.join(globalSkillDir, 'SKILL.md');
+ if (!fs.existsSync(globalSkillMd)) {
+ process.stderr.write(`[agent-skills] WARNING: Global skill not found at "~/.claude/skills/${skillName}/SKILL.md" — skipping\n`);
+ continue;
+ }
+ // Symlink escape guard: validatePath resolves symlinks and enforces
+ // containment within globalSkillsBase. Prevents a skill directory
+ // symlinked to an arbitrary location from being injected (#1992).
+ const pathCheck = validatePath(globalSkillMd, globalSkillsBase, { allowAbsolute: true });
+ if (!pathCheck.safe) {
+ process.stderr.write(`[agent-skills] WARNING: Global skill "${skillName}" failed path check (symlink escape?) — skipping\n`);
+ continue;
+ }
+ validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: `~/.claude/skills/${skillName}` });
+ continue;
+ }
+
// Validate path safety — must resolve within project root
const pathCheck = validatePath(skillPath, projectRoot);
if (!pathCheck.safe) {
@@ -1484,12 +1517,12 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
continue;
}
- validPaths.push(skillPath);
+ validPaths.push({ ref: `${skillPath}/SKILL.md`, display: skillPath });
}
if (validPaths.length === 0) return '';
- const lines = validPaths.map(p => `- @${p}/SKILL.md`).join('\n');
+ const lines = validPaths.map(p => `- @${p.ref}`).join('\n');
return `\nRead these user-configured skills:\n${lines}\n`;
}
diff --git a/tests/agent-skills.test.cjs b/tests/agent-skills.test.cjs
index 4976a0400..4d2ad5fd1 100644
--- a/tests/agent-skills.test.cjs
+++ b/tests/agent-skills.test.cjs
@@ -205,3 +205,90 @@ describe('config-set agent_skills', () => {
);
});
});
+
+// ─── global: prefix support (#1992) ──────────────────────────────────────────
+
+describe('agent-skills global: prefix', () => {
+ let tmpDir;
+ let fakeHome;
+ let globalSkillsDir;
+
+ beforeEach(() => {
+ tmpDir = createTempProject();
+ // Create a fake HOME with ~/.claude/skills/ structure
+ fakeHome = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-1992-home-'));
+ globalSkillsDir = path.join(fakeHome, '.claude', 'skills');
+ fs.mkdirSync(globalSkillsDir, { recursive: true });
+ });
+
+ afterEach(() => {
+ cleanup(tmpDir);
+ fs.rmSync(fakeHome, { recursive: true, force: true });
+ });
+
+ function createGlobalSkill(name) {
+ const skillDir = path.join(globalSkillsDir, name);
+ fs.mkdirSync(skillDir, { recursive: true });
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), `# ${name}\nGlobal skill content.\n`);
+ return skillDir;
+ }
+
+ test('global:valid-skill resolves to $HOME/.claude/skills/valid-skill/SKILL.md', () => {
+ createGlobalSkill('valid-skill');
+ writeConfig(tmpDir, {
+ agent_skills: { 'gsd-executor': ['global:valid-skill'] },
+ });
+
+ const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome });
+ assert.ok(result.output.includes('valid-skill/SKILL.md'), `should reference the global skill: ${result.output}`);
+ assert.ok(result.output.includes(''), 'should emit agent_skills block');
+ });
+
+ test('global:invalid!name is rejected by regex and skipped', () => {
+ writeConfig(tmpDir, {
+ agent_skills: { 'gsd-executor': ['global:invalid!name'] },
+ });
+
+ const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome });
+ // No valid skills → empty output, command succeeds
+ assert.strictEqual(result.output, '', 'should skip invalid name without crashing');
+ });
+
+ test('global:missing-skill is skipped when directory is absent', () => {
+ // Do NOT create the skill directory
+ writeConfig(tmpDir, {
+ agent_skills: { 'gsd-executor': ['global:missing-skill'] },
+ });
+
+ const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome });
+ assert.strictEqual(result.output, '', 'should skip missing skill gracefully');
+ });
+
+ test('mix of global: and project-relative paths both resolve correctly', () => {
+ createGlobalSkill('shadcn');
+
+ // Create a project-relative skill
+ const projectSkillDir = path.join(tmpDir, 'skills', 'local-skill');
+ fs.mkdirSync(projectSkillDir, { recursive: true });
+ fs.writeFileSync(path.join(projectSkillDir, 'SKILL.md'), '# local\n');
+
+ writeConfig(tmpDir, {
+ agent_skills: { 'gsd-executor': ['global:shadcn', 'skills/local-skill'] },
+ });
+
+ const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome });
+ assert.ok(result.output.includes('shadcn/SKILL.md'), 'should include global shadcn');
+ assert.ok(result.output.includes('skills/local-skill/SKILL.md'), 'should include project-relative skill');
+ });
+
+ test('global: with empty name produces clear warning and skips', () => {
+ writeConfig(tmpDir, {
+ agent_skills: { 'gsd-executor': ['global:'] },
+ });
+
+ const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome });
+ assert.strictEqual(result.output, '', 'should skip empty global: prefix');
+ // The warning goes to stderr — cannot assert on it through runGsdTools's output field,
+ // but the command must not crash and must return empty.
+ });
+});