skillsio 1.1.1 → 1.1.3
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.
- package/README.md +230 -6
- package/dist/cli.mjs +181 -31
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
A security-hardened fork of the [skills](https://github.com/vercel-labs/skills) CLI that scans agent skills for
|
|
4
4
|
malicious content before installation.
|
|
5
5
|
|
|
6
|
+
<!-- agent-list:start -->
|
|
7
|
+
Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [37 more](#available-agents).
|
|
8
|
+
<!-- agent-list:end -->
|
|
9
|
+
|
|
6
10
|
The open agent skills ecosystem makes it trivial to install third-party instruction sets into coding agents — but that
|
|
7
11
|
same ease of installation is a vector for prompt injection, data exfiltration, and credential theft.
|
|
8
12
|
[Snyk's analysis](https://snyk.io/blog/) of 3,984 published skills found that **13.4% had critical security issues** and
|
|
@@ -50,6 +54,23 @@ domain patterns that regex rules can't — letting you eyeball where a skill wan
|
|
|
50
54
|
With `--yes`, URL-only prompts are auto-continued. Skills with high/critical findings always show URLs alongside the
|
|
51
55
|
findings summary.
|
|
52
56
|
|
|
57
|
+
### Third-Party Audits via skills.sh
|
|
58
|
+
|
|
59
|
+
For GitHub-sourced skills, the CLI automatically checks [skills.sh](https://skills.sh) — Vercel's official skill
|
|
60
|
+
directory — which runs independent third-party security audits from three auditors: **Snyk**, **Socket**, and
|
|
61
|
+
**Gen Agent Trust Hub**. Results appear alongside local scan output:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
◆ skills.sh: 3 audits [Snyk ✗] [Socket ✓] [Trust Hub ✗]
|
|
65
|
+
https://skills.sh/inference-sh-3/skills/agent-tools
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- Green ✓ = auditor passed, Red ✗ = auditor failed, Dim ~ = no result yet
|
|
69
|
+
- If any auditor returns a **Fail** verdict, severity is escalated to at least **High**, triggering a confirmation
|
|
70
|
+
prompt
|
|
71
|
+
- skills.sh lookup runs in parallel with VT and never blocks installation on error (graceful fallback)
|
|
72
|
+
- Only fires for GitHub-sourced skills that are listed on skills.sh — silent for everything else
|
|
73
|
+
|
|
53
74
|
### Optional: VirusTotal Integration
|
|
54
75
|
|
|
55
76
|
When a [VirusTotal](https://www.virustotal.com/) API key is provided, the CLI also hashes each skill's content
|
|
@@ -242,18 +263,19 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
|
|
|
242
263
|
<!-- supported-agents:start -->
|
|
243
264
|
| Agent | `--agent` | Project Path | Global Path |
|
|
244
265
|
|-------|-----------|--------------|-------------|
|
|
245
|
-
| Amp, Kimi Code CLI, Replit | `amp`, `kimi-cli`, `replit` | `.agents/skills/` | `~/.config/agents/skills/` |
|
|
266
|
+
| Amp, Kimi Code CLI, Replit, Universal | `amp`, `kimi-cli`, `replit`, `universal` | `.agents/skills/` | `~/.config/agents/skills/` |
|
|
246
267
|
| Antigravity | `antigravity` | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
|
|
247
268
|
| Augment | `augment` | `.augment/skills/` | `~/.augment/skills/` |
|
|
248
269
|
| Claude Code | `claude-code` | `.claude/skills/` | `~/.claude/skills/` |
|
|
249
|
-
| OpenClaw | `openclaw` | `skills/` | `~/.
|
|
270
|
+
| OpenClaw | `openclaw` | `skills/` | `~/.openclaw/skills/` |
|
|
250
271
|
| Cline | `cline` | `.cline/skills/` | `~/.cline/skills/` |
|
|
251
272
|
| CodeBuddy | `codebuddy` | `.codebuddy/skills/` | `~/.codebuddy/skills/` |
|
|
252
273
|
| Codex | `codex` | `.agents/skills/` | `~/.codex/skills/` |
|
|
253
274
|
| Command Code | `command-code` | `.commandcode/skills/` | `~/.commandcode/skills/` |
|
|
254
275
|
| Continue | `continue` | `.continue/skills/` | `~/.continue/skills/` |
|
|
276
|
+
| Cortex Code | `cortex` | `.cortex/skills/` | `~/.snowflake/cortex/skills/` |
|
|
255
277
|
| Crush | `crush` | `.crush/skills/` | `~/.config/crush/skills/` |
|
|
256
|
-
| Cursor | `cursor` | `.
|
|
278
|
+
| Cursor | `cursor` | `.agents/skills/` | `~/.cursor/skills/` |
|
|
257
279
|
| Droid | `droid` | `.factory/skills/` | `~/.factory/skills/` |
|
|
258
280
|
| Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
|
|
259
281
|
| GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
|
|
@@ -281,7 +303,154 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [35 more](#su
|
|
|
281
303
|
| AdaL | `adal` | `.adal/skills/` | `~/.adal/skills/` |
|
|
282
304
|
<!-- supported-agents:end -->
|
|
283
305
|
|
|
284
|
-
|
|
306
|
+
> [!NOTE]
|
|
307
|
+
> **Kiro CLI users:** After installing skills, manually add them to your custom agent's `resources` in
|
|
308
|
+
> `.kiro/agents/<agent>.json`:
|
|
309
|
+
>
|
|
310
|
+
> ```json
|
|
311
|
+
> {
|
|
312
|
+
> "resources": ["skill://.kiro/skills/**/SKILL.md"]
|
|
313
|
+
> }
|
|
314
|
+
> ```
|
|
315
|
+
|
|
316
|
+
The CLI automatically detects which coding agents you have installed. If none are detected, you'll be prompted to select
|
|
317
|
+
which agents to install to.
|
|
318
|
+
|
|
319
|
+
## Creating Skills
|
|
320
|
+
|
|
321
|
+
Skills are directories containing a `SKILL.md` file with YAML frontmatter:
|
|
322
|
+
|
|
323
|
+
```markdown
|
|
324
|
+
---
|
|
325
|
+
name: my-skill
|
|
326
|
+
description: What this skill does and when to use it
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
# My Skill
|
|
330
|
+
|
|
331
|
+
Instructions for the agent to follow when this skill is activated.
|
|
332
|
+
|
|
333
|
+
## When to Use
|
|
334
|
+
|
|
335
|
+
Describe the scenarios where this skill should be used.
|
|
336
|
+
|
|
337
|
+
## Steps
|
|
338
|
+
|
|
339
|
+
1. First, do this
|
|
340
|
+
2. Then, do that
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Required Fields
|
|
344
|
+
|
|
345
|
+
- `name`: Unique identifier (lowercase, hyphens allowed)
|
|
346
|
+
- `description`: Brief explanation of what the skill does
|
|
347
|
+
|
|
348
|
+
### Optional Fields
|
|
349
|
+
|
|
350
|
+
- `metadata.internal`: Set to `true` to hide the skill from normal discovery. Internal skills are only visible and
|
|
351
|
+
installable when `INSTALL_INTERNAL_SKILLS=1` is set. Useful for work-in-progress skills or skills meant only for
|
|
352
|
+
internal tooling.
|
|
353
|
+
|
|
354
|
+
```markdown
|
|
355
|
+
---
|
|
356
|
+
name: my-internal-skill
|
|
357
|
+
description: An internal skill not shown by default
|
|
358
|
+
metadata:
|
|
359
|
+
internal: true
|
|
360
|
+
---
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Skill Discovery
|
|
364
|
+
|
|
365
|
+
The CLI searches for skills in these locations within a repository:
|
|
366
|
+
|
|
367
|
+
<!-- skill-discovery:start -->
|
|
368
|
+
- Root directory (if it contains `SKILL.md`)
|
|
369
|
+
- `skills/`
|
|
370
|
+
- `skills/.curated/`
|
|
371
|
+
- `skills/.experimental/`
|
|
372
|
+
- `skills/.system/`
|
|
373
|
+
- `.agents/skills/`
|
|
374
|
+
- `.agent/skills/`
|
|
375
|
+
- `.augment/skills/`
|
|
376
|
+
- `.claude/skills/`
|
|
377
|
+
- `./skills/`
|
|
378
|
+
- `.cline/skills/`
|
|
379
|
+
- `.codebuddy/skills/`
|
|
380
|
+
- `.commandcode/skills/`
|
|
381
|
+
- `.continue/skills/`
|
|
382
|
+
- `.cortex/skills/`
|
|
383
|
+
- `.crush/skills/`
|
|
384
|
+
- `.factory/skills/`
|
|
385
|
+
- `.goose/skills/`
|
|
386
|
+
- `.junie/skills/`
|
|
387
|
+
- `.iflow/skills/`
|
|
388
|
+
- `.kilocode/skills/`
|
|
389
|
+
- `.kiro/skills/`
|
|
390
|
+
- `.kode/skills/`
|
|
391
|
+
- `.mcpjam/skills/`
|
|
392
|
+
- `.vibe/skills/`
|
|
393
|
+
- `.mux/skills/`
|
|
394
|
+
- `.openhands/skills/`
|
|
395
|
+
- `.pi/skills/`
|
|
396
|
+
- `.qoder/skills/`
|
|
397
|
+
- `.qwen/skills/`
|
|
398
|
+
- `.roo/skills/`
|
|
399
|
+
- `.trae/skills/`
|
|
400
|
+
- `.windsurf/skills/`
|
|
401
|
+
- `.zencoder/skills/`
|
|
402
|
+
- `.neovate/skills/`
|
|
403
|
+
- `.pochi/skills/`
|
|
404
|
+
- `.adal/skills/`
|
|
405
|
+
<!-- skill-discovery:end -->
|
|
406
|
+
|
|
407
|
+
### Plugin Manifest Discovery
|
|
408
|
+
|
|
409
|
+
If `.claude-plugin/marketplace.json` or `.claude-plugin/plugin.json` exists, skills declared in those files are also discovered:
|
|
410
|
+
|
|
411
|
+
```json
|
|
412
|
+
// .claude-plugin/marketplace.json
|
|
413
|
+
{
|
|
414
|
+
"metadata": { "pluginRoot": "./plugins" },
|
|
415
|
+
"plugins": [{
|
|
416
|
+
"name": "my-plugin",
|
|
417
|
+
"source": "my-plugin",
|
|
418
|
+
"skills": ["./skills/review", "./skills/test"]
|
|
419
|
+
}]
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
This enables compatibility with the [Claude Code plugin marketplace](https://code.claude.com/docs/en/plugin-marketplaces) ecosystem.
|
|
424
|
+
|
|
425
|
+
If no skills are found in standard locations, a recursive search is performed.
|
|
426
|
+
|
|
427
|
+
## Compatibility
|
|
428
|
+
|
|
429
|
+
Skills are generally compatible across agents since they follow a
|
|
430
|
+
shared [Agent Skills specification](https://agentskills.io). However, some features may be agent-specific:
|
|
431
|
+
|
|
432
|
+
| Feature | OpenCode | OpenHands | Claude Code | Cline | CodeBuddy | Codex | Command Code | Kiro CLI | Cursor | Antigravity | Roo Code | Github Copilot | Amp | OpenClaw | Neovate | Pi | Qoder | Zencoder |
|
|
433
|
+
| --------------- | -------- | --------- | ----------- | ----- | --------- | ----- | ------------ | -------- | ------ | ----------- | -------- | -------------- | --- | -------- | ------- | --- | ----- | -------- |
|
|
434
|
+
| Basic skills | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
435
|
+
| `allowed-tools` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
|
|
436
|
+
| `context: fork` | No | No | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
|
437
|
+
| Hooks | No | No | Yes | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No |
|
|
438
|
+
|
|
439
|
+
## Troubleshooting
|
|
440
|
+
|
|
441
|
+
### "No skills found"
|
|
442
|
+
|
|
443
|
+
Ensure the repository contains valid `SKILL.md` files with both `name` and `description` in the frontmatter.
|
|
444
|
+
|
|
445
|
+
### Skill not loading in agent
|
|
446
|
+
|
|
447
|
+
- Verify the skill was installed to the correct path
|
|
448
|
+
- Check the agent's documentation for skill loading requirements
|
|
449
|
+
- Ensure the `SKILL.md` frontmatter is valid YAML
|
|
450
|
+
|
|
451
|
+
### Permission errors
|
|
452
|
+
|
|
453
|
+
Ensure you have write access to the target directory.
|
|
285
454
|
|
|
286
455
|
## Environment Variables
|
|
287
456
|
|
|
@@ -306,10 +475,12 @@ pnpm format # Format code with Prettier
|
|
|
306
475
|
- `src/scanner.ts` — Rules engine. Defines ~81 regex rules across 8 threat categories, a correlation engine for
|
|
307
476
|
multi-signal detection, and optional deep taint analysis integration. Supports loading external rules from JSON
|
|
308
477
|
files via `--rules`.
|
|
309
|
-
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs
|
|
310
|
-
escalation logic and user confirmation prompts.
|
|
478
|
+
- `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs VT and skills.sh lookups in parallel,
|
|
479
|
+
handles escalation logic and user confirmation prompts.
|
|
311
480
|
- `src/vt.ts` — VirusTotal API client. SHA-256 hashing, `GET /api/v3/files/{hash}` lookup, verdict mapping, graceful
|
|
312
481
|
error handling.
|
|
482
|
+
- `src/skills-sh.ts` — skills.sh audit client. Fetches and HTML-parses third-party audit results (Snyk, Socket, Gen
|
|
483
|
+
Agent Trust Hub) for GitHub-sourced skills with a 5-second timeout; always resolves gracefully.
|
|
313
484
|
- `src/deep-scan/` — Deep taint analysis engine (enabled via `--deep-scan`). Regex-based tokenizers extract sources,
|
|
314
485
|
sinks, and assignments from Python/JS/TS files; a forward taint tracker propagates data flow; a cross-file analyzer
|
|
315
486
|
detects multi-file attack patterns via import graph analysis. See [docs/deep-scan.md](docs/deep-scan.md).
|
|
@@ -318,6 +489,28 @@ pnpm format # Format code with Prettier
|
|
|
318
489
|
|
|
319
490
|
## Changelog
|
|
320
491
|
|
|
492
|
+
### 1.1.3
|
|
493
|
+
|
|
494
|
+
- **Synced with upstream** ([vercel-labs/skills](https://github.com/vercel-labs/skills)): universal agent support
|
|
495
|
+
(`.agents/skills/` as a single install target symlinked across agents), new agents (Cortex Code, and others), Kiro CLI
|
|
496
|
+
note, Creating Skills docs, Compatibility table, Troubleshooting section, agent-list badge
|
|
497
|
+
- **Replaced HTML scraping with structured API**: third-party audit now uses the `add-skill.vercel.sh/audit` JSON
|
|
498
|
+
endpoint instead of scraping `skills.sh` HTML — more reliable and richer data (risk levels + alert counts per auditor)
|
|
499
|
+
- **Stronger blocking**: critical and high risk from the API both always prompt for confirmation; `--yes` is ignored for
|
|
500
|
+
both (previously high was bypassed by `--yes` and critical only prompted rather than blocked)
|
|
501
|
+
- **Audit failure warning**: if the skills.sh API is unreachable, a yellow warning is shown rather than silently
|
|
502
|
+
skipping — local scan still runs regardless
|
|
503
|
+
- Removed duplicate audit display (previously showed both an inline scan note and a separate "Security Risk Assessments"
|
|
504
|
+
panel for the same data)
|
|
505
|
+
|
|
506
|
+
### 1.1.2
|
|
507
|
+
|
|
508
|
+
- **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
|
|
509
|
+
[skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
|
|
510
|
+
- Critical or High risk from any auditor escalates the install gate — critical always prompts even with `--yes`, high
|
|
511
|
+
blocks unless `--yes` is set; uses the structured skills.sh JSON API instead of HTML scraping
|
|
512
|
+
- Audit runs in parallel with VirusTotal and fails silently on any network or parse error
|
|
513
|
+
|
|
321
514
|
### 1.1.1
|
|
322
515
|
|
|
323
516
|
- Removed anonymous usage telemetry inherited from the original Vercel `skills` CLI
|
|
@@ -347,6 +540,37 @@ pnpm format # Format code with Prettier
|
|
|
347
540
|
- URL transparency: all external URLs in skill files are shown before installation
|
|
348
541
|
- Scanner rules informed by Snyk and ClawHavoc research
|
|
349
542
|
|
|
543
|
+
## Links
|
|
544
|
+
|
|
545
|
+
- [Agent Skills Specification](https://agentskills.io)
|
|
546
|
+
- [Skills Directory](https://skills.sh)
|
|
547
|
+
- [Amp Skills Documentation](https://ampcode.com/manual#agent-skills)
|
|
548
|
+
- [Antigravity Skills Documentation](https://antigravity.google/docs/skills)
|
|
549
|
+
- [Factory AI / Droid Skills Documentation](https://docs.factory.ai/cli/configuration/skills)
|
|
550
|
+
- [Claude Code Skills Documentation](https://code.claude.com/docs/en/skills)
|
|
551
|
+
- [OpenClaw Skills Documentation](https://docs.openclaw.ai/tools/skills)
|
|
552
|
+
- [Cline Skills Documentation](https://docs.cline.bot/features/skills)
|
|
553
|
+
- [CodeBuddy Skills Documentation](https://www.codebuddy.ai/docs/ide/Features/Skills)
|
|
554
|
+
- [Codex Skills Documentation](https://developers.openai.com/codex/skills)
|
|
555
|
+
- [Command Code Skills Documentation](https://commandcode.ai/docs/skills)
|
|
556
|
+
- [Crush Skills Documentation](https://github.com/charmbracelet/crush?tab=readme-ov-file#agent-skills)
|
|
557
|
+
- [Cursor Skills Documentation](https://cursor.com/docs/context/skills)
|
|
558
|
+
- [Gemini CLI Skills Documentation](https://geminicli.com/docs/cli/skills/)
|
|
559
|
+
- [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
|
|
560
|
+
- [iFlow CLI Skills Documentation](https://platform.iflow.cn/en/cli/examples/skill)
|
|
561
|
+
- [Kimi Code CLI Skills Documentation](https://moonshotai.github.io/kimi-cli/en/customization/skills.html)
|
|
562
|
+
- [Kiro CLI Skills Documentation](https://kiro.dev/docs/cli/custom-agents/configuration-reference/#skill-resources)
|
|
563
|
+
- [Kode Skills Documentation](https://github.com/shareAI-lab/kode/blob/main/docs/skills.md)
|
|
564
|
+
- [OpenCode Skills Documentation](https://opencode.ai/docs/skills)
|
|
565
|
+
- [Qwen Code Skills Documentation](https://qwenlm.github.io/qwen-code-docs/en/users/features/skills/)
|
|
566
|
+
- [OpenHands Skills Documentation](https://docs.openhands.ai/modules/usage/how-to/using-skills)
|
|
567
|
+
- [Pi Skills Documentation](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md)
|
|
568
|
+
- [Qoder Skills Documentation](https://docs.qoder.com/cli/Skills)
|
|
569
|
+
- [Replit Skills Documentation](https://docs.replit.com/replitai/skills)
|
|
570
|
+
- [Roo Code Skills Documentation](https://docs.roocode.com/features/skills)
|
|
571
|
+
- [Trae Skills Documentation](https://docs.trae.ai/ide/skills)
|
|
572
|
+
- [Vercel Agent Skills Repository](https://github.com/vercel-labs/agent-skills)
|
|
573
|
+
|
|
350
574
|
## Research
|
|
351
575
|
|
|
352
576
|
The scanner rules are informed by the following research into malicious agent skills:
|
package/dist/cli.mjs
CHANGED
|
@@ -50,7 +50,10 @@ function isDirectSkillUrl(input) {
|
|
|
50
50
|
if (input.includes("gitlab.com/") && !input.includes("/-/raw/")) return false;
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
|
+
const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
|
|
53
54
|
function parseSource(input) {
|
|
55
|
+
const alias = SOURCE_ALIASES[input];
|
|
56
|
+
if (alias) input = alias;
|
|
54
57
|
if (isLocalPath(input)) {
|
|
55
58
|
const resolvedPath = resolve(input);
|
|
56
59
|
return {
|
|
@@ -169,7 +172,8 @@ const S_STEP_CANCEL = import_picocolors.default.red("■");
|
|
|
169
172
|
const S_STEP_SUBMIT = import_picocolors.default.green("◇");
|
|
170
173
|
const S_RADIO_ACTIVE = import_picocolors.default.green("●");
|
|
171
174
|
const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
|
|
172
|
-
|
|
175
|
+
import_picocolors.default.green("✓");
|
|
176
|
+
const S_BULLET = import_picocolors.default.green("•");
|
|
173
177
|
const S_BAR = import_picocolors.default.dim("│");
|
|
174
178
|
const S_BAR_H = import_picocolors.default.dim("─");
|
|
175
179
|
const cancelSymbol = Symbol("cancel");
|
|
@@ -212,10 +216,11 @@ async function searchMultiselect(options) {
|
|
|
212
216
|
if (state === "active") {
|
|
213
217
|
if (lockedSection && lockedSection.items.length > 0) {
|
|
214
218
|
lines.push(`${S_BAR}`);
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
|
|
220
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
221
|
+
for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
|
|
217
222
|
lines.push(`${S_BAR}`);
|
|
218
|
-
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("
|
|
223
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
|
|
219
224
|
}
|
|
220
225
|
const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
|
|
221
226
|
lines.push(searchLine);
|
|
@@ -428,6 +433,7 @@ async function parseSkillMd(skillMdPath, options) {
|
|
|
428
433
|
const content = await readFile(skillMdPath, "utf-8");
|
|
429
434
|
const { data } = (0, import_gray_matter.default)(content);
|
|
430
435
|
if (!data.name || !data.description) return null;
|
|
436
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") return null;
|
|
431
437
|
if (data.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
|
|
432
438
|
return {
|
|
433
439
|
name: data.name,
|
|
@@ -477,7 +483,6 @@ async function discoverSkills(basePath, subpath, options) {
|
|
|
477
483
|
join(searchPath, ".codex/skills"),
|
|
478
484
|
join(searchPath, ".commandcode/skills"),
|
|
479
485
|
join(searchPath, ".continue/skills"),
|
|
480
|
-
join(searchPath, ".cursor/skills"),
|
|
481
486
|
join(searchPath, ".github/skills"),
|
|
482
487
|
join(searchPath, ".goose/skills"),
|
|
483
488
|
join(searchPath, ".iflow/skills"),
|
|
@@ -536,6 +541,12 @@ const home = homedir();
|
|
|
536
541
|
const configHome = xdgConfig ?? join(home, ".config");
|
|
537
542
|
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
538
543
|
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
544
|
+
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
|
|
545
|
+
if (pathExists(join(homeDir, ".openclaw"))) return join(homeDir, ".openclaw/skills");
|
|
546
|
+
if (pathExists(join(homeDir, ".clawdbot"))) return join(homeDir, ".clawdbot/skills");
|
|
547
|
+
if (pathExists(join(homeDir, ".moltbot"))) return join(homeDir, ".moltbot/skills");
|
|
548
|
+
return join(homeDir, ".openclaw/skills");
|
|
549
|
+
}
|
|
539
550
|
const agents = {
|
|
540
551
|
amp: {
|
|
541
552
|
name: "amp",
|
|
@@ -577,7 +588,7 @@ const agents = {
|
|
|
577
588
|
name: "openclaw",
|
|
578
589
|
displayName: "OpenClaw",
|
|
579
590
|
skillsDir: "skills",
|
|
580
|
-
globalSkillsDir:
|
|
591
|
+
globalSkillsDir: getOpenClawGlobalSkillsDir(),
|
|
581
592
|
detectInstalled: async () => {
|
|
582
593
|
return existsSync(join(home, ".openclaw")) || existsSync(join(home, ".clawdbot")) || existsSync(join(home, ".moltbot"));
|
|
583
594
|
}
|
|
@@ -627,6 +638,15 @@ const agents = {
|
|
|
627
638
|
return existsSync(join(process.cwd(), ".continue")) || existsSync(join(home, ".continue"));
|
|
628
639
|
}
|
|
629
640
|
},
|
|
641
|
+
cortex: {
|
|
642
|
+
name: "cortex",
|
|
643
|
+
displayName: "Cortex Code",
|
|
644
|
+
skillsDir: ".cortex/skills",
|
|
645
|
+
globalSkillsDir: join(home, ".snowflake/cortex/skills"),
|
|
646
|
+
detectInstalled: async () => {
|
|
647
|
+
return existsSync(join(home, ".snowflake/cortex"));
|
|
648
|
+
}
|
|
649
|
+
},
|
|
630
650
|
crush: {
|
|
631
651
|
name: "crush",
|
|
632
652
|
displayName: "Crush",
|
|
@@ -639,7 +659,7 @@ const agents = {
|
|
|
639
659
|
cursor: {
|
|
640
660
|
name: "cursor",
|
|
641
661
|
displayName: "Cursor",
|
|
642
|
-
skillsDir: ".
|
|
662
|
+
skillsDir: ".agents/skills",
|
|
643
663
|
globalSkillsDir: join(home, ".cursor/skills"),
|
|
644
664
|
detectInstalled: async () => {
|
|
645
665
|
return existsSync(join(home, ".cursor"));
|
|
@@ -888,6 +908,14 @@ const agents = {
|
|
|
888
908
|
detectInstalled: async () => {
|
|
889
909
|
return existsSync(join(home, ".adal"));
|
|
890
910
|
}
|
|
911
|
+
},
|
|
912
|
+
universal: {
|
|
913
|
+
name: "universal",
|
|
914
|
+
displayName: "Universal",
|
|
915
|
+
skillsDir: ".agents/skills",
|
|
916
|
+
globalSkillsDir: join(configHome, "agents/skills"),
|
|
917
|
+
showInUniversalList: false,
|
|
918
|
+
detectInstalled: async () => false
|
|
891
919
|
}
|
|
892
920
|
};
|
|
893
921
|
async function detectInstalledAgents() {
|
|
@@ -3296,6 +3324,24 @@ async function lookupFileHash(sha256, apiKey) {
|
|
|
3296
3324
|
async function checkSkillOnVT(skillContent, apiKey) {
|
|
3297
3325
|
return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
|
|
3298
3326
|
}
|
|
3327
|
+
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
3328
|
+
async function fetchAuditData(source, skillSlugs, timeoutMs = 3e3) {
|
|
3329
|
+
if (skillSlugs.length === 0) return null;
|
|
3330
|
+
try {
|
|
3331
|
+
const params = new URLSearchParams({
|
|
3332
|
+
source,
|
|
3333
|
+
skills: skillSlugs.join(",")
|
|
3334
|
+
});
|
|
3335
|
+
const controller = new AbortController();
|
|
3336
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3337
|
+
const response = await fetch(`${AUDIT_URL}?${params.toString()}`, { signal: controller.signal });
|
|
3338
|
+
clearTimeout(timeout);
|
|
3339
|
+
if (!response.ok) return null;
|
|
3340
|
+
return await response.json();
|
|
3341
|
+
} catch {
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3299
3345
|
const SEVERITY_LABELS = {
|
|
3300
3346
|
critical: import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" CRITICAL "))),
|
|
3301
3347
|
high: import_picocolors.default.red(import_picocolors.default.bold("HIGH")),
|
|
@@ -3310,6 +3356,14 @@ const SEVERITY_ORDER = {
|
|
|
3310
3356
|
high: 3,
|
|
3311
3357
|
critical: 4
|
|
3312
3358
|
};
|
|
3359
|
+
const AUDIT_RISK_ORDER = {
|
|
3360
|
+
unknown: 0,
|
|
3361
|
+
safe: 0,
|
|
3362
|
+
low: 1,
|
|
3363
|
+
medium: 2,
|
|
3364
|
+
high: 3,
|
|
3365
|
+
critical: 4
|
|
3366
|
+
};
|
|
3313
3367
|
function displayVTVerdict(verdict) {
|
|
3314
3368
|
if (!verdict.found) {
|
|
3315
3369
|
M.message(import_picocolors.default.dim(" VirusTotal: not found (local scan only)"));
|
|
@@ -3324,22 +3378,95 @@ function displayVTVerdict(verdict) {
|
|
|
3324
3378
|
}
|
|
3325
3379
|
if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
|
|
3326
3380
|
}
|
|
3381
|
+
function auditRiskBadge(displayName, audit) {
|
|
3382
|
+
const alerts = audit.alerts != null && audit.alerts > 0 ? ` ${audit.alerts} alert${audit.alerts !== 1 ? "s" : ""}` : "";
|
|
3383
|
+
switch (audit.risk) {
|
|
3384
|
+
case "critical": return `[${import_picocolors.default.red(import_picocolors.default.bold(`${displayName} ✗ critical${alerts}`))}]`;
|
|
3385
|
+
case "high": return `[${import_picocolors.default.red(`${displayName} ✗ high${alerts}`)}]`;
|
|
3386
|
+
case "medium": return `[${import_picocolors.default.yellow(`${displayName} ⚠ medium${alerts}`)}]`;
|
|
3387
|
+
case "low": return `[${import_picocolors.default.green(`${displayName} ✓ low`)}]`;
|
|
3388
|
+
case "safe": return `[${import_picocolors.default.green(`${displayName} ✓`)}]`;
|
|
3389
|
+
default: return `[${import_picocolors.default.dim(`${displayName} ~`)}]`;
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
const AUDITORS = [
|
|
3393
|
+
{
|
|
3394
|
+
id: "ath",
|
|
3395
|
+
displayName: "Trust Hub"
|
|
3396
|
+
},
|
|
3397
|
+
{
|
|
3398
|
+
id: "socket",
|
|
3399
|
+
displayName: "Socket"
|
|
3400
|
+
},
|
|
3401
|
+
{
|
|
3402
|
+
id: "snyk",
|
|
3403
|
+
displayName: "Snyk"
|
|
3404
|
+
}
|
|
3405
|
+
];
|
|
3406
|
+
function displayAuditResults(skillNames, auditData, source) {
|
|
3407
|
+
if (!skillNames.some((name) => {
|
|
3408
|
+
const data = auditData[name];
|
|
3409
|
+
return data && Object.keys(data).length > 0;
|
|
3410
|
+
})) return;
|
|
3411
|
+
for (const skillName of skillNames) {
|
|
3412
|
+
const data = auditData[skillName];
|
|
3413
|
+
if (!data || Object.keys(data).length === 0) continue;
|
|
3414
|
+
const badges = AUDITORS.map(({ id, displayName }) => {
|
|
3415
|
+
const audit = data[id];
|
|
3416
|
+
return audit ? auditRiskBadge(displayName, audit) : `[${import_picocolors.default.dim(`${displayName} ~`)}]`;
|
|
3417
|
+
}).join(" ");
|
|
3418
|
+
const label = skillNames.length > 1 ? `${import_picocolors.default.cyan(skillName)}: ` : "";
|
|
3419
|
+
M.message(` ${import_picocolors.default.cyan("◆")} ${label}${badges}`);
|
|
3420
|
+
}
|
|
3421
|
+
M.message(import_picocolors.default.dim(` https://skills.sh/${source}`));
|
|
3422
|
+
}
|
|
3423
|
+
function maxAuditRisk(skillNames, auditData) {
|
|
3424
|
+
let max = 0;
|
|
3425
|
+
for (const skillName of skillNames) {
|
|
3426
|
+
const data = auditData[skillName];
|
|
3427
|
+
if (!data) continue;
|
|
3428
|
+
for (const audit of Object.values(data)) {
|
|
3429
|
+
const level = AUDIT_RISK_ORDER[audit.risk] ?? 0;
|
|
3430
|
+
if (level > max) max = level;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
if (max >= 4) return "critical";
|
|
3434
|
+
if (max >= 3) return "high";
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3327
3437
|
async function presentScanResults(results, options) {
|
|
3328
3438
|
const allFindings = results.flatMap((r) => r.findings.map((f) => ({
|
|
3329
3439
|
...f,
|
|
3330
3440
|
skillName: r.skillName
|
|
3331
3441
|
})));
|
|
3332
3442
|
const allUrls = [...new Set(results.flatMap((r) => r.urls))];
|
|
3443
|
+
const skillNames = results.map((r) => r.skillName);
|
|
3333
3444
|
const vtVerdicts = /* @__PURE__ */ new Map();
|
|
3445
|
+
let auditData = null;
|
|
3446
|
+
let auditFailed = false;
|
|
3334
3447
|
let vtEscalate = false;
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3448
|
+
let auditEscalate = null;
|
|
3449
|
+
await Promise.all([(async () => {
|
|
3450
|
+
if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
|
|
3451
|
+
const verdict = await checkSkillOnVT(content, options.vtKey);
|
|
3452
|
+
vtVerdicts.set(skillName, verdict);
|
|
3453
|
+
if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
|
|
3454
|
+
} catch {}
|
|
3455
|
+
})(), (async () => {
|
|
3456
|
+
if (options.auditSource) {
|
|
3457
|
+
const data = await fetchAuditData(options.auditSource, skillNames);
|
|
3458
|
+
if (data) {
|
|
3459
|
+
auditData = data;
|
|
3460
|
+
auditEscalate = maxAuditRisk(skillNames, data);
|
|
3461
|
+
} else auditFailed = true;
|
|
3462
|
+
}
|
|
3463
|
+
})()]);
|
|
3464
|
+
const anyEscalation = vtEscalate || auditEscalate !== null;
|
|
3465
|
+
if (allFindings.length === 0 && !anyEscalation) {
|
|
3341
3466
|
M.success(import_picocolors.default.green("Security scan passed — no issues found"));
|
|
3342
3467
|
if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3468
|
+
if (auditData && options.auditSource) displayAuditResults(skillNames, auditData, options.auditSource);
|
|
3469
|
+
if (auditFailed) M.warn(import_picocolors.default.yellow("skills.sh audit unavailable — third-party risk data could not be fetched"));
|
|
3343
3470
|
if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
|
|
3344
3471
|
return true;
|
|
3345
3472
|
}
|
|
@@ -3363,6 +3490,11 @@ async function presentScanResults(results, options) {
|
|
|
3363
3490
|
console.log();
|
|
3364
3491
|
for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
|
|
3365
3492
|
}
|
|
3493
|
+
if (auditData && options.auditSource) {
|
|
3494
|
+
console.log();
|
|
3495
|
+
displayAuditResults(skillNames, auditData, options.auditSource);
|
|
3496
|
+
}
|
|
3497
|
+
if (auditFailed) M.warn(import_picocolors.default.yellow("skills.sh audit unavailable — third-party risk data could not be fetched"));
|
|
3366
3498
|
if (allUrls.length > 0) {
|
|
3367
3499
|
console.log();
|
|
3368
3500
|
M.info(`External URLs found in skill files (${allUrls.length}):`);
|
|
@@ -3370,24 +3502,18 @@ async function presentScanResults(results, options) {
|
|
|
3370
3502
|
}
|
|
3371
3503
|
console.log();
|
|
3372
3504
|
if (vtEscalate) overallMax = "critical";
|
|
3505
|
+
if (auditEscalate === "critical") overallMax = "critical";
|
|
3506
|
+
else if (auditEscalate === "high" && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
|
|
3373
3507
|
if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
|
|
3374
3508
|
M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
|
|
3375
3509
|
return true;
|
|
3376
3510
|
}
|
|
3377
|
-
if (overallMax === "critical")
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
if (pD(confirmed) || !confirmed) return false;
|
|
3384
|
-
return true;
|
|
3385
|
-
}
|
|
3386
|
-
if (options.yes) {
|
|
3387
|
-
M.warn(import_picocolors.default.yellow("High severity findings detected — proceeding (--yes flag set)"));
|
|
3388
|
-
return true;
|
|
3389
|
-
}
|
|
3390
|
-
const confirmed = await ye({ message: import_picocolors.default.yellow("Security warnings found. Continue with installation?") });
|
|
3511
|
+
if (overallMax === "critical") M.error(import_picocolors.default.red(import_picocolors.default.bold("Critical security issues detected. This skill may be malicious.")));
|
|
3512
|
+
else M.error(import_picocolors.default.red("High severity security issues detected."));
|
|
3513
|
+
const confirmed = await ye({
|
|
3514
|
+
message: import_picocolors.default.yellow("Install anyway?"),
|
|
3515
|
+
initialValue: false
|
|
3516
|
+
});
|
|
3391
3517
|
if (pD(confirmed) || !confirmed) return false;
|
|
3392
3518
|
return true;
|
|
3393
3519
|
}
|
|
@@ -3414,6 +3540,16 @@ function resolveExternalRules(options) {
|
|
|
3414
3540
|
_externalRulesCache.set(key, loaded);
|
|
3415
3541
|
return loaded;
|
|
3416
3542
|
}
|
|
3543
|
+
function extractAuditSource(sourceUrl) {
|
|
3544
|
+
if (!sourceUrl?.includes("github.com")) return void 0;
|
|
3545
|
+
try {
|
|
3546
|
+
const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
|
|
3547
|
+
if (parts.length < 2) return void 0;
|
|
3548
|
+
return parts.slice(0, 2).join("/");
|
|
3549
|
+
} catch {
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3417
3553
|
function shortenPath$1(fullPath, cwd) {
|
|
3418
3554
|
const home = homedir();
|
|
3419
3555
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
@@ -3673,10 +3809,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
|
|
|
3673
3809
|
});
|
|
3674
3810
|
const vtKey = options.vtKey || process.env.VT_API_KEY;
|
|
3675
3811
|
const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
|
|
3812
|
+
const auditSource = extractAuditSource(remoteSkill.sourceUrl);
|
|
3676
3813
|
if (!await presentScanResults([scanResult], {
|
|
3677
3814
|
yes: options.yes,
|
|
3678
3815
|
vtKey,
|
|
3679
|
-
skillContents
|
|
3816
|
+
skillContents,
|
|
3817
|
+
auditSource
|
|
3680
3818
|
})) {
|
|
3681
3819
|
xe("Installation cancelled due to security concerns");
|
|
3682
3820
|
process.exit(0);
|
|
@@ -4348,6 +4486,7 @@ async function runAdd(args, options = {}) {
|
|
|
4348
4486
|
}
|
|
4349
4487
|
selectedSkills = selected;
|
|
4350
4488
|
}
|
|
4489
|
+
const auditSource = getOwnerRepo(parsed) ?? void 0;
|
|
4351
4490
|
let targetAgents;
|
|
4352
4491
|
const validAgents = Object.keys(agents);
|
|
4353
4492
|
if (options.agent?.includes("*")) {
|
|
@@ -4488,7 +4627,8 @@ async function runAdd(args, options = {}) {
|
|
|
4488
4627
|
if (!await presentScanResults(scanResults, {
|
|
4489
4628
|
yes: options.yes,
|
|
4490
4629
|
vtKey,
|
|
4491
|
-
skillContents
|
|
4630
|
+
skillContents,
|
|
4631
|
+
auditSource
|
|
4492
4632
|
})) {
|
|
4493
4633
|
xe("Installation cancelled due to security concerns");
|
|
4494
4634
|
await cleanup(tempDir);
|
|
@@ -4701,7 +4841,14 @@ const RESET$2 = "\x1B[0m";
|
|
|
4701
4841
|
const BOLD$2 = "\x1B[1m";
|
|
4702
4842
|
const DIM$2 = "\x1B[38;5;102m";
|
|
4703
4843
|
const TEXT$1 = "\x1B[38;5;145m";
|
|
4844
|
+
const CYAN$1 = "\x1B[36m";
|
|
4704
4845
|
const SEARCH_API_BASE = process.env.SKILLS_API_URL || "https://skills.sh";
|
|
4846
|
+
function formatInstalls(count) {
|
|
4847
|
+
if (!count || count <= 0) return "";
|
|
4848
|
+
if (count >= 1e6) return `${(count / 1e6).toFixed(1).replace(/\.0$/, "")}M installs`;
|
|
4849
|
+
if (count >= 1e3) return `${(count / 1e3).toFixed(1).replace(/\.0$/, "")}K installs`;
|
|
4850
|
+
return `${count} install${count === 1 ? "" : "s"}`;
|
|
4851
|
+
}
|
|
4705
4852
|
async function searchSkillsAPI(query) {
|
|
4706
4853
|
try {
|
|
4707
4854
|
const url = `${SEARCH_API_BASE}/api/search?q=${encodeURIComponent(query)}&limit=10`;
|
|
@@ -4751,8 +4898,10 @@ async function runSearchPrompt(initialQuery = "") {
|
|
|
4751
4898
|
const arrow = isSelected ? `${BOLD$2}>${RESET$2}` : " ";
|
|
4752
4899
|
const name = isSelected ? `${BOLD$2}${skill.name}${RESET$2}` : `${TEXT$1}${skill.name}${RESET$2}`;
|
|
4753
4900
|
const source = skill.source ? ` ${DIM$2}${skill.source}${RESET$2}` : "";
|
|
4901
|
+
const installs = formatInstalls(skill.installs);
|
|
4902
|
+
const installsBadge = installs ? ` ${CYAN$1}${installs}${RESET$2}` : "";
|
|
4754
4903
|
const loadingIndicator = loading && i === 0 ? ` ${DIM$2}...${RESET$2}` : "";
|
|
4755
|
-
lines.push(` ${arrow} ${name}${source}${loadingIndicator}`);
|
|
4904
|
+
lines.push(` ${arrow} ${name}${source}${installsBadge}${loadingIndicator}`);
|
|
4756
4905
|
}
|
|
4757
4906
|
}
|
|
4758
4907
|
lines.push("");
|
|
@@ -4865,7 +5014,8 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
|
|
|
4865
5014
|
console.log();
|
|
4866
5015
|
for (const skill of results.slice(0, 6)) {
|
|
4867
5016
|
const pkg = skill.source || skill.slug;
|
|
4868
|
-
|
|
5017
|
+
const installs = formatInstalls(skill.installs);
|
|
5018
|
+
console.log(`${TEXT$1}${pkg}@${skill.name}${RESET$2}${installs ? ` ${CYAN$1}${installs}${RESET$2}` : ""}`);
|
|
4869
5019
|
console.log(`${DIM$2}└ https://skills.sh/${skill.slug}${RESET$2}`);
|
|
4870
5020
|
console.log();
|
|
4871
5021
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillsio",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "The SECURE open agent skills ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"codex",
|
|
47
47
|
"command-code",
|
|
48
48
|
"continue",
|
|
49
|
+
"cortex",
|
|
49
50
|
"crush",
|
|
50
51
|
"cursor",
|
|
51
52
|
"droid",
|
|
@@ -74,7 +75,8 @@
|
|
|
74
75
|
"zencoder",
|
|
75
76
|
"neovate",
|
|
76
77
|
"pochi",
|
|
77
|
-
"adal"
|
|
78
|
+
"adal",
|
|
79
|
+
"universal"
|
|
78
80
|
],
|
|
79
81
|
"repository": {
|
|
80
82
|
"type": "git",
|