vskill 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/audit/__fixtures__/clean-project/app.d.ts +1 -0
  2. package/dist/audit/__fixtures__/clean-project/app.js +8 -0
  3. package/dist/audit/__fixtures__/clean-project/app.js.map +1 -0
  4. package/dist/audit/__fixtures__/clean-project/utils.d.ts +2 -0
  5. package/dist/audit/__fixtures__/clean-project/utils.js +8 -0
  6. package/dist/audit/__fixtures__/clean-project/utils.js.map +1 -0
  7. package/dist/audit/__fixtures__/mixed-project/risky.d.ts +1 -0
  8. package/dist/audit/__fixtures__/mixed-project/risky.js +6 -0
  9. package/dist/audit/__fixtures__/mixed-project/risky.js.map +1 -0
  10. package/dist/audit/__fixtures__/mixed-project/safe.d.ts +1 -0
  11. package/dist/audit/__fixtures__/mixed-project/safe.js +5 -0
  12. package/dist/audit/__fixtures__/mixed-project/safe.js.map +1 -0
  13. package/dist/audit/__fixtures__/vulnerable-project/handler.d.ts +4 -0
  14. package/dist/audit/__fixtures__/vulnerable-project/handler.js +21 -0
  15. package/dist/audit/__fixtures__/vulnerable-project/handler.js.map +1 -0
  16. package/dist/audit/audit-integration.test.d.ts +1 -0
  17. package/dist/audit/audit-integration.test.js +92 -0
  18. package/dist/audit/audit-integration.test.js.map +1 -0
  19. package/dist/audit/audit-llm.d.ts +25 -0
  20. package/dist/audit/audit-llm.js +139 -0
  21. package/dist/audit/audit-llm.js.map +1 -0
  22. package/dist/audit/audit-llm.test.d.ts +1 -0
  23. package/dist/audit/audit-llm.test.js +110 -0
  24. package/dist/audit/audit-llm.test.js.map +1 -0
  25. package/dist/audit/audit-patterns.d.ts +31 -0
  26. package/dist/audit/audit-patterns.js +239 -0
  27. package/dist/audit/audit-patterns.js.map +1 -0
  28. package/dist/audit/audit-patterns.test.d.ts +1 -0
  29. package/dist/audit/audit-patterns.test.js +91 -0
  30. package/dist/audit/audit-patterns.test.js.map +1 -0
  31. package/dist/audit/audit-scanner.d.ts +16 -0
  32. package/dist/audit/audit-scanner.js +151 -0
  33. package/dist/audit/audit-scanner.js.map +1 -0
  34. package/dist/audit/audit-scanner.test.d.ts +1 -0
  35. package/dist/audit/audit-scanner.test.js +112 -0
  36. package/dist/audit/audit-scanner.test.js.map +1 -0
  37. package/dist/audit/audit-types.d.ts +98 -0
  38. package/dist/audit/audit-types.js +19 -0
  39. package/dist/audit/audit-types.js.map +1 -0
  40. package/dist/audit/audit-types.test.d.ts +1 -0
  41. package/dist/audit/audit-types.test.js +140 -0
  42. package/dist/audit/audit-types.test.js.map +1 -0
  43. package/dist/audit/config.d.ts +13 -0
  44. package/dist/audit/config.js +90 -0
  45. package/dist/audit/config.js.map +1 -0
  46. package/dist/audit/config.test.d.ts +1 -0
  47. package/dist/audit/config.test.js +44 -0
  48. package/dist/audit/config.test.js.map +1 -0
  49. package/dist/audit/file-discovery.d.ts +15 -0
  50. package/dist/audit/file-discovery.js +153 -0
  51. package/dist/audit/file-discovery.js.map +1 -0
  52. package/dist/audit/file-discovery.test.d.ts +1 -0
  53. package/dist/audit/file-discovery.test.js +120 -0
  54. package/dist/audit/file-discovery.test.js.map +1 -0
  55. package/dist/audit/fix-suggestions.d.ts +13 -0
  56. package/dist/audit/fix-suggestions.js +84 -0
  57. package/dist/audit/fix-suggestions.js.map +1 -0
  58. package/dist/audit/fix-suggestions.test.d.ts +1 -0
  59. package/dist/audit/fix-suggestions.test.js +35 -0
  60. package/dist/audit/fix-suggestions.test.js.map +1 -0
  61. package/dist/audit/formatters/json-formatter.d.ts +8 -0
  62. package/dist/audit/formatters/json-formatter.js +10 -0
  63. package/dist/audit/formatters/json-formatter.js.map +1 -0
  64. package/dist/audit/formatters/json-formatter.test.d.ts +1 -0
  65. package/dist/audit/formatters/json-formatter.test.js +49 -0
  66. package/dist/audit/formatters/json-formatter.test.js.map +1 -0
  67. package/dist/audit/formatters/report-formatter.d.ts +8 -0
  68. package/dist/audit/formatters/report-formatter.js +97 -0
  69. package/dist/audit/formatters/report-formatter.js.map +1 -0
  70. package/dist/audit/formatters/report-formatter.test.d.ts +1 -0
  71. package/dist/audit/formatters/report-formatter.test.js +51 -0
  72. package/dist/audit/formatters/report-formatter.test.js.map +1 -0
  73. package/dist/audit/formatters/sarif-formatter.d.ts +11 -0
  74. package/dist/audit/formatters/sarif-formatter.js +87 -0
  75. package/dist/audit/formatters/sarif-formatter.js.map +1 -0
  76. package/dist/audit/formatters/sarif-formatter.test.d.ts +1 -0
  77. package/dist/audit/formatters/sarif-formatter.test.js +71 -0
  78. package/dist/audit/formatters/sarif-formatter.test.js.map +1 -0
  79. package/dist/audit/formatters/terminal-formatter.d.ts +9 -0
  80. package/dist/audit/formatters/terminal-formatter.js +61 -0
  81. package/dist/audit/formatters/terminal-formatter.js.map +1 -0
  82. package/dist/audit/formatters/terminal-formatter.test.d.ts +1 -0
  83. package/dist/audit/formatters/terminal-formatter.test.js +51 -0
  84. package/dist/audit/formatters/terminal-formatter.test.js.map +1 -0
  85. package/dist/audit/index.d.ts +2 -0
  86. package/dist/audit/index.js +2 -0
  87. package/dist/audit/index.js.map +1 -0
  88. package/dist/blocklist/blocklist-e2e.test.d.ts +1 -0
  89. package/dist/blocklist/blocklist-e2e.test.js +346 -0
  90. package/dist/blocklist/blocklist-e2e.test.js.map +1 -0
  91. package/dist/commands/add-blocklist-e2e.test.d.ts +1 -0
  92. package/dist/commands/add-blocklist-e2e.test.js +390 -0
  93. package/dist/commands/add-blocklist-e2e.test.js.map +1 -0
  94. package/dist/commands/add.js +184 -7
  95. package/dist/commands/add.js.map +1 -1
  96. package/dist/commands/add.test.js +159 -19
  97. package/dist/commands/add.test.js.map +1 -1
  98. package/dist/commands/audit.d.ts +23 -0
  99. package/dist/commands/audit.js +100 -0
  100. package/dist/commands/audit.js.map +1 -0
  101. package/dist/commands/audit.test.d.ts +1 -0
  102. package/dist/commands/audit.test.js +79 -0
  103. package/dist/commands/audit.test.js.map +1 -0
  104. package/dist/discovery/github-tree.d.ts +15 -0
  105. package/dist/discovery/github-tree.js +56 -0
  106. package/dist/discovery/github-tree.js.map +1 -0
  107. package/dist/discovery/github-tree.test.d.ts +1 -0
  108. package/dist/discovery/github-tree.test.js +143 -0
  109. package/dist/discovery/github-tree.test.js.map +1 -0
  110. package/dist/index.js +24 -0
  111. package/dist/index.js.map +1 -1
  112. package/dist/lockfile/index.d.ts +1 -0
  113. package/dist/lockfile/index.js +1 -0
  114. package/dist/lockfile/index.js.map +1 -1
  115. package/dist/lockfile/lockfile.js +2 -1
  116. package/dist/lockfile/lockfile.js.map +1 -1
  117. package/dist/lockfile/project-root.d.ts +11 -0
  118. package/dist/lockfile/project-root.js +29 -0
  119. package/dist/lockfile/project-root.js.map +1 -0
  120. package/dist/lockfile/project-root.test.d.ts +1 -0
  121. package/dist/lockfile/project-root.test.js +49 -0
  122. package/dist/lockfile/project-root.test.js.map +1 -0
  123. package/package.json +1 -1
@@ -0,0 +1,390 @@
1
+ // ---------------------------------------------------------------------------
2
+ // E2E integration tests: vskill add with blocklist enforcement
3
+ //
4
+ // Tests the FULL error output chain when a user tries to install
5
+ // known-malicious skills from the ClawHub research. Verifies:
6
+ // 1. Installation is REFUSED with exit code 1
7
+ // 2. Error message shows exact threat type, severity, and reason
8
+ // 3. --force override shows prominent WARNING box with full details
9
+ // 4. Tier 1 scan is NEVER reached for blocked skills (without --force)
10
+ // ---------------------------------------------------------------------------
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
12
+ // ---------------------------------------------------------------------------
13
+ // Mocks
14
+ // ---------------------------------------------------------------------------
15
+ const mockMkdirSync = vi.fn();
16
+ const mockWriteFileSync = vi.fn();
17
+ const mockReadFileSync = vi.fn();
18
+ const mockExistsSync = vi.fn();
19
+ const mockCpSync = vi.fn();
20
+ const mockChmodSync = vi.fn();
21
+ const mockReaddirSync = vi.fn();
22
+ const mockStatSync = vi.fn();
23
+ vi.mock("node:fs", () => ({
24
+ mkdirSync: (...args) => mockMkdirSync(...args),
25
+ writeFileSync: (...args) => mockWriteFileSync(...args),
26
+ readFileSync: (...args) => mockReadFileSync(...args),
27
+ existsSync: (...args) => mockExistsSync(...args),
28
+ cpSync: (...args) => mockCpSync(...args),
29
+ chmodSync: (...args) => mockChmodSync(...args),
30
+ readdirSync: (...args) => mockReaddirSync(...args),
31
+ statSync: (...args) => mockStatSync(...args),
32
+ copyFileSync: vi.fn(),
33
+ }));
34
+ vi.mock("node:path", async () => {
35
+ const actual = await vi.importActual("node:path");
36
+ return { ...actual, join: (...args) => actual.join(...args) };
37
+ });
38
+ const mockDigest = vi.fn().mockReturnValue("abcdef123456xxxx");
39
+ const mockUpdate = vi.fn().mockReturnValue({ digest: mockDigest });
40
+ vi.mock("node:crypto", () => ({
41
+ createHash: () => ({ update: mockUpdate }),
42
+ }));
43
+ const mockDetectInstalledAgents = vi.fn();
44
+ vi.mock("../agents/agents-registry.js", () => ({
45
+ detectInstalledAgents: (...args) => mockDetectInstalledAgents(...args),
46
+ }));
47
+ const mockEnsureLockfile = vi.fn();
48
+ const mockWriteLockfile = vi.fn();
49
+ vi.mock("../lockfile/index.js", () => ({
50
+ ensureLockfile: (...args) => mockEnsureLockfile(...args),
51
+ writeLockfile: (...args) => mockWriteLockfile(...args),
52
+ }));
53
+ const mockRunTier1Scan = vi.fn();
54
+ vi.mock("../scanner/index.js", () => ({
55
+ runTier1Scan: (...args) => mockRunTier1Scan(...args),
56
+ }));
57
+ const mockCheckPlatformSecurity = vi.fn();
58
+ vi.mock("../security/index.js", () => ({
59
+ checkPlatformSecurity: (...args) => mockCheckPlatformSecurity(...args),
60
+ }));
61
+ // ---- REAL blocklist module (not mocked!) ----
62
+ // We mock only the filesystem layer so checkBlocklist reads from our test cache.
63
+ // The blocklist module itself runs its real logic.
64
+ // Mock output utilities to capture colored output as plain text
65
+ vi.mock("../utils/output.js", () => ({
66
+ bold: (s) => s,
67
+ green: (s) => s,
68
+ red: (s) => s,
69
+ yellow: (s) => s,
70
+ dim: (s) => s,
71
+ cyan: (s) => s,
72
+ spinner: () => ({ stop: vi.fn() }),
73
+ }));
74
+ const { addCommand } = await import("./add.js");
75
+ // ---------------------------------------------------------------------------
76
+ // Seed data cache — same data that the API would return
77
+ // ---------------------------------------------------------------------------
78
+ const SEED_CACHE = {
79
+ entries: [
80
+ {
81
+ skillName: "Clawhub",
82
+ sourceUrl: "https://github.com/hightower6eu/Clawhub",
83
+ sourceRegistry: "clawhub",
84
+ threatType: "platform-impersonation",
85
+ severity: "critical",
86
+ reason: "Impersonates the ClawHub/GitHub platform to trick users into running malicious code",
87
+ evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
88
+ discoveredAt: "2025-12-01T00:00:00.000Z",
89
+ },
90
+ {
91
+ skillName: "polymarket-traiding-bot",
92
+ sourceUrl: "https://github.com/Aslaep123/polymarket-traiding-bot",
93
+ sourceRegistry: "clawhub",
94
+ threatType: "credential-theft",
95
+ severity: "critical",
96
+ reason: "Base64-encoded credential exfiltration via hidden HTTP requests",
97
+ evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
98
+ discoveredAt: "2025-12-01T00:00:00.000Z",
99
+ },
100
+ {
101
+ skillName: "Skills Auto-Updater",
102
+ sourceUrl: "https://github.com/hightower6eu/skills-auto-updater",
103
+ sourceRegistry: "clawhub",
104
+ threatType: "auto-updater-trojan",
105
+ severity: "high",
106
+ reason: "Auto-updater trojan that downloads and executes malicious payloads",
107
+ evidenceUrls: ["https://snyk.io/blog/toxicskills-mcp-exploit"],
108
+ discoveredAt: "2025-12-01T00:00:00.000Z",
109
+ },
110
+ {
111
+ skillName: "google-qx4",
112
+ sourceUrl: "https://github.com/aztr0nutzs/google-qx4",
113
+ sourceRegistry: "clawhub",
114
+ threatType: "prompt-injection",
115
+ severity: "critical",
116
+ reason: "Prompt injection via fake Google integration skill",
117
+ evidenceUrls: ["https://www.aikido.dev/blog/malicious-mcp-servers"],
118
+ discoveredAt: "2025-12-01T00:00:00.000Z",
119
+ },
120
+ {
121
+ skillName: "clawhud",
122
+ sourceUrl: "https://github.com/zaycv/clawhud",
123
+ sourceRegistry: "clawhub",
124
+ threatType: "typosquatting",
125
+ severity: "high",
126
+ reason: "Typosquatting ClawHub (clawhud vs clawhub) to mislead users",
127
+ evidenceUrls: ["https://www.aikido.dev/blog/malicious-mcp-servers"],
128
+ discoveredAt: "2025-12-01T00:00:00.000Z",
129
+ },
130
+ ],
131
+ count: 5,
132
+ lastUpdated: "2026-02-19T00:00:00Z",
133
+ fetchedAt: new Date().toISOString(),
134
+ etag: '"seed"',
135
+ };
136
+ // ---------------------------------------------------------------------------
137
+ // Helpers
138
+ // ---------------------------------------------------------------------------
139
+ function setupBlocklistCache() {
140
+ // The blocklist module reads from ~/.vskill/blocklist.json
141
+ // We intercept existsSync and readFileSync to serve our seed cache
142
+ const originalExistsSync = mockExistsSync.getMockImplementation();
143
+ mockExistsSync.mockImplementation((p) => {
144
+ if (p.includes("blocklist.json"))
145
+ return true;
146
+ if (originalExistsSync)
147
+ return originalExistsSync(p);
148
+ return false;
149
+ });
150
+ const originalReadFileSync = mockReadFileSync.getMockImplementation();
151
+ mockReadFileSync.mockImplementation((p, ...args) => {
152
+ if (p.includes("blocklist.json"))
153
+ return JSON.stringify(SEED_CACHE);
154
+ if (originalReadFileSync)
155
+ return originalReadFileSync(p, ...args);
156
+ return "";
157
+ });
158
+ }
159
+ function collectOutput(spy) {
160
+ return spy.mock.calls.map((c) => String(c[0])).join("\n");
161
+ }
162
+ // ---------------------------------------------------------------------------
163
+ // Tests
164
+ // ---------------------------------------------------------------------------
165
+ const originalFetch = globalThis.fetch;
166
+ beforeEach(() => {
167
+ vi.clearAllMocks();
168
+ vi.spyOn(console, "log").mockImplementation(() => { });
169
+ vi.spyOn(console, "error").mockImplementation(() => { });
170
+ mockCheckPlatformSecurity.mockResolvedValue(null);
171
+ // Mock fetch for GitHub path (fetching SKILL.md)
172
+ globalThis.fetch = vi.fn().mockResolvedValue({
173
+ ok: true,
174
+ text: async () => "# Safe Skill\nNormal content",
175
+ json: async () => ({ entries: [], count: 0, lastUpdated: null }),
176
+ headers: { get: () => null },
177
+ });
178
+ setupBlocklistCache();
179
+ });
180
+ afterEach(() => {
181
+ globalThis.fetch = originalFetch;
182
+ });
183
+ // ============================================================================
184
+ // 1. GitHub path — blocking with exact error details
185
+ // ============================================================================
186
+ describe("E2E: vskill add blocks ClawHub malicious skills (GitHub path)", () => {
187
+ it("refuses to install hightower6eu's Clawhub with full threat details", async () => {
188
+ const mockExit = vi
189
+ .spyOn(process, "exit")
190
+ .mockImplementation(() => {
191
+ throw new Error("process.exit");
192
+ });
193
+ await expect(addCommand("hightower6eu/Clawhub", {})).rejects.toThrow("process.exit");
194
+ const errorOutput = collectOutput(console.error);
195
+ // Must show BLOCKED status
196
+ expect(errorOutput).toContain("BLOCKED");
197
+ // Must show the exact skill name
198
+ expect(errorOutput).toContain("Clawhub");
199
+ // Must show the threat type
200
+ expect(errorOutput).toContain("platform-impersonation");
201
+ // Must show the severity
202
+ expect(errorOutput).toContain("critical");
203
+ // Must show the reason WHY
204
+ expect(errorOutput).toContain("Impersonates the ClawHub");
205
+ // Must show how to override
206
+ expect(errorOutput).toContain("--force");
207
+ // Exit code must be 1
208
+ expect(mockExit).toHaveBeenCalledWith(1);
209
+ // Tier 1 scan must NEVER be called (blocked before scan)
210
+ expect(mockRunTier1Scan).not.toHaveBeenCalled();
211
+ mockExit.mockRestore();
212
+ });
213
+ it("refuses to install Aslaep123's credential-stealing bot with Base64 exfil details", async () => {
214
+ const mockExit = vi
215
+ .spyOn(process, "exit")
216
+ .mockImplementation(() => {
217
+ throw new Error("process.exit");
218
+ });
219
+ await expect(addCommand("Aslaep123/polymarket-traiding-bot", {})).rejects.toThrow("process.exit");
220
+ const errorOutput = collectOutput(console.error);
221
+ expect(errorOutput).toContain("BLOCKED");
222
+ expect(errorOutput).toContain("polymarket-traiding-bot");
223
+ expect(errorOutput).toContain("credential-theft");
224
+ expect(errorOutput).toContain("critical");
225
+ expect(errorOutput).toContain("Base64-encoded credential exfiltration");
226
+ expect(mockRunTier1Scan).not.toHaveBeenCalled();
227
+ mockExit.mockRestore();
228
+ });
229
+ it("refuses to install aztr0nutzs's prompt injection skill", async () => {
230
+ const mockExit = vi
231
+ .spyOn(process, "exit")
232
+ .mockImplementation(() => {
233
+ throw new Error("process.exit");
234
+ });
235
+ await expect(addCommand("aztr0nutzs/google-qx4", {})).rejects.toThrow("process.exit");
236
+ const errorOutput = collectOutput(console.error);
237
+ expect(errorOutput).toContain("BLOCKED");
238
+ expect(errorOutput).toContain("google-qx4");
239
+ expect(errorOutput).toContain("prompt-injection");
240
+ expect(errorOutput).toContain("critical");
241
+ expect(errorOutput).toContain("Prompt injection");
242
+ expect(mockRunTier1Scan).not.toHaveBeenCalled();
243
+ mockExit.mockRestore();
244
+ });
245
+ it("refuses to install zaycv's typosquatting skill", async () => {
246
+ const mockExit = vi
247
+ .spyOn(process, "exit")
248
+ .mockImplementation(() => {
249
+ throw new Error("process.exit");
250
+ });
251
+ await expect(addCommand("zaycv/clawhud", {})).rejects.toThrow("process.exit");
252
+ const errorOutput = collectOutput(console.error);
253
+ expect(errorOutput).toContain("BLOCKED");
254
+ expect(errorOutput).toContain("clawhud");
255
+ expect(errorOutput).toContain("typosquatting");
256
+ expect(errorOutput).toContain("high");
257
+ expect(errorOutput).toContain("clawhud vs clawhub");
258
+ mockExit.mockRestore();
259
+ });
260
+ });
261
+ // ============================================================================
262
+ // 2. --force override shows WARNING with full details
263
+ // ============================================================================
264
+ describe("E2E: --force override shows warning box with threat details", () => {
265
+ it("shows credential-theft warning box but continues for polymarket-traiding-bot", async () => {
266
+ mockRunTier1Scan.mockReturnValue({
267
+ verdict: "PASS",
268
+ findings: [],
269
+ score: 100,
270
+ patternsChecked: 37,
271
+ criticalCount: 0,
272
+ highCount: 0,
273
+ mediumCount: 0,
274
+ lowCount: 0,
275
+ infoCount: 0,
276
+ durationMs: 1,
277
+ });
278
+ mockDetectInstalledAgents.mockResolvedValue([
279
+ {
280
+ id: "claude-code",
281
+ displayName: "Claude Code",
282
+ localSkillsDir: ".claude/commands",
283
+ globalSkillsDir: "~/.claude/commands",
284
+ },
285
+ ]);
286
+ mockEnsureLockfile.mockReturnValue({
287
+ version: 1,
288
+ agents: [],
289
+ skills: {},
290
+ createdAt: "2026-01-01T00:00:00.000Z",
291
+ updatedAt: "2026-01-01T00:00:00.000Z",
292
+ });
293
+ await addCommand("Aslaep123/polymarket-traiding-bot", { force: true });
294
+ const errorOutput = collectOutput(console.error);
295
+ // WARNING box must be shown
296
+ expect(errorOutput).toContain("WARNING");
297
+ expect(errorOutput).toContain("known-malicious");
298
+ // Must show full threat details in the warning
299
+ expect(errorOutput).toContain("polymarket-traiding-bot");
300
+ expect(errorOutput).toContain("credential-theft");
301
+ expect(errorOutput).toContain("critical");
302
+ expect(errorOutput).toContain("Base64-encoded credential exfiltration");
303
+ // Must show --force was used
304
+ expect(errorOutput).toContain("--force");
305
+ // Despite the warning, Tier 1 scan MUST proceed
306
+ expect(mockRunTier1Scan).toHaveBeenCalled();
307
+ });
308
+ it("shows platform-impersonation warning for Clawhub with --force", async () => {
309
+ mockRunTier1Scan.mockReturnValue({
310
+ verdict: "PASS",
311
+ findings: [],
312
+ score: 100,
313
+ patternsChecked: 37,
314
+ criticalCount: 0,
315
+ highCount: 0,
316
+ mediumCount: 0,
317
+ lowCount: 0,
318
+ infoCount: 0,
319
+ durationMs: 1,
320
+ });
321
+ mockDetectInstalledAgents.mockResolvedValue([
322
+ {
323
+ id: "claude-code",
324
+ displayName: "Claude Code",
325
+ localSkillsDir: ".claude/commands",
326
+ globalSkillsDir: "~/.claude/commands",
327
+ },
328
+ ]);
329
+ mockEnsureLockfile.mockReturnValue({
330
+ version: 1,
331
+ agents: [],
332
+ skills: {},
333
+ createdAt: "2026-01-01T00:00:00.000Z",
334
+ updatedAt: "2026-01-01T00:00:00.000Z",
335
+ });
336
+ await addCommand("hightower6eu/Clawhub", { force: true });
337
+ const errorOutput = collectOutput(console.error);
338
+ expect(errorOutput).toContain("WARNING");
339
+ expect(errorOutput).toContain("Clawhub");
340
+ expect(errorOutput).toContain("platform-impersonation");
341
+ expect(errorOutput).toContain("critical");
342
+ expect(errorOutput).toContain("Impersonates the ClawHub");
343
+ expect(mockRunTier1Scan).toHaveBeenCalled();
344
+ });
345
+ });
346
+ // ============================================================================
347
+ // 3. Safe skills pass through without blocklist interference
348
+ // ============================================================================
349
+ describe("E2E: safe skills are not affected by blocklist", () => {
350
+ it("installs legitimate skill without any BLOCKED or WARNING output", async () => {
351
+ mockRunTier1Scan.mockReturnValue({
352
+ verdict: "PASS",
353
+ findings: [],
354
+ score: 100,
355
+ patternsChecked: 37,
356
+ criticalCount: 0,
357
+ highCount: 0,
358
+ mediumCount: 0,
359
+ lowCount: 0,
360
+ infoCount: 0,
361
+ durationMs: 1,
362
+ });
363
+ mockDetectInstalledAgents.mockResolvedValue([
364
+ {
365
+ id: "claude-code",
366
+ displayName: "Claude Code",
367
+ localSkillsDir: ".claude/commands",
368
+ globalSkillsDir: "~/.claude/commands",
369
+ },
370
+ ]);
371
+ mockEnsureLockfile.mockReturnValue({
372
+ version: 1,
373
+ agents: [],
374
+ skills: {},
375
+ createdAt: "2026-01-01T00:00:00.000Z",
376
+ updatedAt: "2026-01-01T00:00:00.000Z",
377
+ });
378
+ await addCommand("anthropic/code-review-assistant", {});
379
+ const allErrorOutput = collectOutput(console.error);
380
+ // No BLOCKED or WARNING messages
381
+ expect(allErrorOutput).not.toContain("BLOCKED");
382
+ expect(allErrorOutput).not.toContain("WARNING");
383
+ expect(allErrorOutput).not.toContain("malicious");
384
+ // Tier 1 scan MUST proceed
385
+ expect(mockRunTier1Scan).toHaveBeenCalled();
386
+ // Lockfile MUST be updated
387
+ expect(mockEnsureLockfile).toHaveBeenCalled();
388
+ });
389
+ });
390
+ //# sourceMappingURL=add-blocklist-e2e.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add-blocklist-e2e.test.js","sourceRoot":"","sources":["../../src/commands/add-blocklist-e2e.test.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // vskill add -- install a skill from GitHub or local plugin directory
3
3
  // ---------------------------------------------------------------------------
4
- import { mkdirSync, writeFileSync, readFileSync, existsSync, cpSync, chmodSync, readdirSync, } from "node:fs";
4
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, copyFileSync, statSync, chmodSync, readdirSync, rmSync, } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  import { createHash } from "node:crypto";
7
+ import { execSync } from "node:child_process";
7
8
  import { resolveTilde } from "../utils/paths.js";
8
9
  import { detectInstalledAgents } from "../agents/agents-registry.js";
9
10
  import { ensureLockfile, writeLockfile } from "../lockfile/index.js";
@@ -11,7 +12,74 @@ import { runTier1Scan } from "../scanner/index.js";
11
12
  import { getPluginSource, getPluginVersion } from "../marketplace/index.js";
12
13
  import { checkBlocklist } from "../blocklist/blocklist.js";
13
14
  import { checkPlatformSecurity } from "../security/index.js";
15
+ import { discoverSkills } from "../discovery/github-tree.js";
14
16
  import { bold, green, red, yellow, dim, cyan, spinner, } from "../utils/output.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Command file filter (prevents plugin internals leaking as slash commands)
19
+ // ---------------------------------------------------------------------------
20
+ /**
21
+ * Returns true for .md files that should NOT be installed into an agent's
22
+ * commands directory. Claude Code (and similar agents) register every .md
23
+ * file they find recursively as a slash command, so plugin-internal files
24
+ * must be excluded.
25
+ */
26
+ function shouldSkipFromCommands(relPath) {
27
+ const normalized = relPath.replace(/\\/g, "/");
28
+ const parts = normalized.split("/");
29
+ const filename = parts[parts.length - 1];
30
+ if (!filename.endsWith(".md"))
31
+ return false;
32
+ if (parts.length === 1 && filename === "PLUGIN.md")
33
+ return true;
34
+ if (filename === "README.md")
35
+ return true;
36
+ if (filename === "FRESHNESS.md")
37
+ return true;
38
+ if (parts.length > 1 && parts[0].startsWith("."))
39
+ return true;
40
+ const internalRootDirs = new Set(["knowledge-base", "lib", "templates", "scripts", "hooks"]);
41
+ if (parts.length > 1 && internalRootDirs.has(parts[0]))
42
+ return true;
43
+ if (parts[0] === "skills" && parts.length > 2 && filename !== "SKILL.md")
44
+ return true;
45
+ return false;
46
+ }
47
+ function copyPluginFiltered(sourceDir, targetDir, relBase = "") {
48
+ mkdirSync(targetDir, { recursive: true });
49
+ const entries = readdirSync(sourceDir);
50
+ for (const entry of entries) {
51
+ const relPath = relBase ? `${relBase}/${entry}` : entry;
52
+ const sourcePath = join(sourceDir, entry);
53
+ const stat = statSync(sourcePath);
54
+ if (stat.isDirectory()) {
55
+ // Flatten: root-level commands/ and skills/ merge into the parent target dir
56
+ const isFlattened = !relBase && (entry === "commands" || entry === "skills");
57
+ const nextTargetDir = isFlattened ? targetDir : join(targetDir, entry);
58
+ copyPluginFiltered(sourcePath, nextTargetDir, relPath);
59
+ }
60
+ else if (stat.isFile() && !shouldSkipFromCommands(relPath)) {
61
+ copyFileSync(sourcePath, join(targetDir, entry));
62
+ }
63
+ }
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Plugin cache cleanup
67
+ // ---------------------------------------------------------------------------
68
+ /**
69
+ * Remove a plugin installed via Claude Code's plugin system.
70
+ * The cache at ~/.claude/plugins/cache/ contains ALL files without filtering,
71
+ * causing internal .md files to leak as ghost slash commands.
72
+ *
73
+ * Uses `claude plugin uninstall` CLI to properly remove the plugin through
74
+ * Claude Code's own API rather than directly manipulating internal files.
75
+ */
76
+ function cleanPluginCache(pluginName, marketplace) {
77
+ const pluginKey = `${pluginName}@${marketplace}`;
78
+ try {
79
+ execSync(`claude plugin uninstall "${pluginKey}"`, { stdio: "ignore", timeout: 10_000 });
80
+ }
81
+ catch { /* ignore - plugin might not be installed via CLI */ }
82
+ }
15
83
  async function fetchSkillContent(url) {
16
84
  const spin = spinner("Fetching skill");
17
85
  try {
@@ -175,14 +243,27 @@ async function installPluginDir(basePath, pluginName, opts) {
175
243
  const marketplacePath = join(basePath, ".claude-plugin", "marketplace.json");
176
244
  const marketplaceContent = readFileSync(marketplacePath, "utf-8");
177
245
  const version = getPluginVersion(pluginName, marketplaceContent) || "0.0.0";
246
+ // Remove unfiltered plugin cache that Claude Code's plugin system creates.
247
+ // The cache at ~/.claude/plugins/cache/ contains ALL files without filtering,
248
+ // causing internal .md files to leak as ghost slash commands.
249
+ try {
250
+ const marketplaceName = JSON.parse(marketplaceContent).name;
251
+ if (marketplaceName) {
252
+ cleanPluginCache(pluginName, marketplaceName);
253
+ }
254
+ }
255
+ catch { /* ignore parse errors */ }
178
256
  // Install: recursively copy plugin directory to cache
179
257
  const sha = createHash("sha256").update(content).digest("hex").slice(0, 12);
180
258
  const locations = [];
181
259
  for (const agent of agents) {
182
260
  const cacheDir = join(opts.global ? resolveTilde(agent.globalSkillsDir) : join(process.cwd(), agent.localSkillsDir), pluginName);
183
261
  try {
184
- mkdirSync(cacheDir, { recursive: true });
185
- cpSync(pluginDir, cacheDir, { recursive: true });
262
+ // Full clean before copy: removes stale files from older installs
263
+ if (existsSync(cacheDir)) {
264
+ rmSync(cacheDir, { recursive: true, force: true });
265
+ }
266
+ copyPluginFiltered(pluginDir, cacheDir);
186
267
  fixHookPermissions(cacheDir);
187
268
  locations.push(`${agent.displayName}: ${cacheDir}`);
188
269
  }
@@ -209,6 +290,53 @@ async function installPluginDir(basePath, pluginName, opts) {
209
290
  }
210
291
  console.log(dim(`\nSHA: ${sha} | Version: ${version}`));
211
292
  }
293
+ async function installOneGitHubSkill(owner, repo, skillName, rawUrl, opts, agents) {
294
+ // Fetch content (non-exiting for multi-skill support)
295
+ let content;
296
+ try {
297
+ const res = await fetch(rawUrl);
298
+ if (!res.ok) {
299
+ return { skillName, installed: false, verdict: "FETCH_FAILED" };
300
+ }
301
+ content = await res.text();
302
+ }
303
+ catch {
304
+ return { skillName, installed: false, verdict: "FETCH_FAILED" };
305
+ }
306
+ // Blocklist check
307
+ const blocked = await checkBlocklist(skillName, undefined);
308
+ if (blocked && !opts.force) {
309
+ printBlockedError(blocked);
310
+ return { skillName, installed: false, verdict: "BLOCKED" };
311
+ }
312
+ if (blocked && opts.force) {
313
+ printBlockedWarning(blocked);
314
+ }
315
+ // Platform security check
316
+ const platformSecurity = await checkPlatformSecurity(skillName);
317
+ if (platformSecurity && platformSecurity.hasCritical && !opts.force) {
318
+ return { skillName, installed: false, verdict: "SECURITY_FAIL" };
319
+ }
320
+ // Tier 1 scan
321
+ const scanResult = runTier1Scan(content);
322
+ if ((scanResult.verdict === "FAIL" || scanResult.verdict === "CONCERNS") && !opts.force) {
323
+ return { skillName, installed: false, verdict: scanResult.verdict };
324
+ }
325
+ // Install to each agent
326
+ const sha = createHash("sha256").update(content).digest("hex").slice(0, 12);
327
+ for (const agent of agents) {
328
+ const baseDir = opts.global
329
+ ? resolveTilde(agent.globalSkillsDir)
330
+ : join(process.cwd(), agent.localSkillsDir);
331
+ const skillDir = join(baseDir, skillName);
332
+ try {
333
+ mkdirSync(skillDir, { recursive: true });
334
+ writeFileSync(join(skillDir, "SKILL.md"), content, "utf-8");
335
+ }
336
+ catch { /* skip agent on write error */ }
337
+ }
338
+ return { skillName, installed: true, verdict: scanResult.verdict, sha };
339
+ }
212
340
  // ---------------------------------------------------------------------------
213
341
  // Main entry point
214
342
  // ---------------------------------------------------------------------------
@@ -224,14 +352,63 @@ export async function addCommand(source, opts) {
224
352
  process.exit(1);
225
353
  }
226
354
  const [owner, repo] = parts;
227
- const skillSubpath = opts.skill
228
- ? `skills/${opts.skill}/SKILL.md`
229
- : "SKILL.md";
355
+ // --skill flag: single-skill mode (no discovery, backward compat)
356
+ if (opts.skill) {
357
+ return installSingleSkillLegacy(owner, repo, opts.skill, opts);
358
+ }
359
+ // Discovery mode: find all skills in the repo
360
+ const discovered = (await discoverSkills(owner, repo)) || [];
361
+ if (discovered.length === 0) {
362
+ // Fallback: discovery returned nothing — try root SKILL.md directly
363
+ return installSingleSkillLegacy(owner, repo, undefined, opts);
364
+ }
365
+ // Multi-skill install
366
+ const agents = await detectInstalledAgents();
367
+ if (agents.length === 0) {
368
+ console.error(red("No AI agents detected. Run ") + cyan("vskill init") + red(" first."));
369
+ process.exit(1);
370
+ }
371
+ const results = [];
372
+ for (const skill of discovered) {
373
+ console.log(dim(`\nInstalling skill: ${bold(skill.name)}...`));
374
+ const result = await installOneGitHubSkill(owner, repo, skill.name, skill.rawUrl, opts, agents);
375
+ results.push(result);
376
+ }
377
+ // Update lockfile with all installed skills
378
+ const lock = ensureLockfile();
379
+ for (const r of results) {
380
+ if (r.installed && r.sha) {
381
+ lock.skills[r.skillName] = {
382
+ version: "0.0.0",
383
+ sha: r.sha,
384
+ tier: "SCANNED",
385
+ installedAt: new Date().toISOString(),
386
+ source: `github:${owner}/${repo}`,
387
+ };
388
+ }
389
+ }
390
+ lock.agents = agents.map((a) => a.id);
391
+ writeLockfile(lock);
392
+ // Summary
393
+ const installed = results.filter((r) => r.installed);
394
+ const skipped = results.filter((r) => !r.installed);
395
+ console.log(green(`\nInstalled ${bold(String(installed.length))} of ${results.length} skills:\n`));
396
+ for (const r of results) {
397
+ const icon = r.installed ? green("✓") : red("✗");
398
+ const detail = r.installed ? dim(`(${r.verdict})`) : red(`(${r.verdict})`);
399
+ console.log(` ${icon} ${r.skillName} ${detail}`);
400
+ }
401
+ }
402
+ // ---------------------------------------------------------------------------
403
+ // Legacy single-skill install (preserves original behavior exactly)
404
+ // ---------------------------------------------------------------------------
405
+ async function installSingleSkillLegacy(owner, repo, skill, opts) {
406
+ const skillSubpath = skill ? `skills/${skill}/SKILL.md` : "SKILL.md";
230
407
  const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${skillSubpath}`;
231
408
  // Fetch SKILL.md
232
409
  const content = await fetchSkillContent(url);
233
410
  // Blocklist check (before scanning)
234
- const skillName = opts.skill || repo;
411
+ const skillName = skill || repo;
235
412
  const blocked = await checkBlocklist(skillName, undefined);
236
413
  if (blocked && !opts.force) {
237
414
  printBlockedError(blocked);