skillsio 1.1.0 → 1.1.2

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 (3) hide show
  1. package/README.md +46 -7
  2. package/dist/cli.mjs +235 -140
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ security gate so you can still move fast without running untrusted code.
14
14
 
15
15
  ## What It Does
16
16
 
17
- Every `skillsio add` command runs a local security scan **before** anything is installed. The scanner applies ~52 regex
17
+ Every `skillsio add` command runs a local security scan **before** anything is installed. The scanner applies ~81 regex
18
18
  rules and a correlation engine derived from the Snyk and ClawHavoc research, organized into 8 threat categories:
19
19
 
20
20
  | Category | What it catches |
@@ -50,6 +50,23 @@ domain patterns that regex rules can't — letting you eyeball where a skill wan
50
50
  With `--yes`, URL-only prompts are auto-continued. Skills with high/critical findings always show URLs alongside the
51
51
  findings summary.
52
52
 
53
+ ### Third-Party Audits via skills.sh
54
+
55
+ For GitHub-sourced skills, the CLI automatically checks [skills.sh](https://skills.sh) — Vercel's official skill
56
+ directory — which runs independent third-party security audits from three auditors: **Snyk**, **Socket**, and
57
+ **Gen Agent Trust Hub**. Results appear alongside local scan output:
58
+
59
+ ```
60
+ ◆ skills.sh: 3 audits [Snyk ✗] [Socket ✓] [Trust Hub ✗]
61
+ https://skills.sh/inference-sh-3/skills/agent-tools
62
+ ```
63
+
64
+ - Green ✓ = auditor passed, Red ✗ = auditor failed, Dim ~ = no result yet
65
+ - If any auditor returns a **Fail** verdict, severity is escalated to at least **High**, triggering a confirmation
66
+ prompt
67
+ - skills.sh lookup runs in parallel with VT and never blocks installation on error (graceful fallback)
68
+ - Only fires for GitHub-sourced skills that are listed on skills.sh — silent for everything else
69
+
53
70
  ### Optional: VirusTotal Integration
54
71
 
55
72
  When a [VirusTotal](https://www.virustotal.com/) API key is provided, the CLI also hashes each skill's content
@@ -120,7 +137,7 @@ npx skillsio add owner/repo --rules ./my-rules.json
120
137
  npx skillsio add owner/repo --rules ./rules/
121
138
  ```
122
139
 
123
- External rules are applied **in addition to** the built-in ~52 rules — they never replace them. Findings from external
140
+ External rules are applied **in addition to** the built-in ~81 rules — they never replace them. Findings from external
124
141
  rules follow the same severity-based prompt flow as built-in findings.
125
142
 
126
143
  See [docs/EXTERNAL-RULES.md](docs/EXTERNAL-RULES.md) for the full format reference, more examples, and tips for writing
@@ -289,8 +306,6 @@ The CLI automatically detects which coding agents you have installed.
289
306
  | --- | --- |
290
307
  | `VT_API_KEY` | VirusTotal API key for optional threat intelligence during security scans |
291
308
  | `INSTALL_INTERNAL_SKILLS` | Set to `1` to show and install skills marked as `internal: true` |
292
- | `DISABLE_TELEMETRY` | Disable anonymous usage telemetry |
293
- | `DO_NOT_TRACK` | Alternative way to disable telemetry |
294
309
 
295
310
  ## Development
296
311
 
@@ -305,13 +320,15 @@ pnpm format # Format code with Prettier
305
320
 
306
321
  ### Scanner Architecture
307
322
 
308
- - `src/scanner.ts` — Rules engine. Defines ~52 regex rules across 8 threat categories, a correlation engine for
323
+ - `src/scanner.ts` — Rules engine. Defines ~81 regex rules across 8 threat categories, a correlation engine for
309
324
  multi-signal detection, and optional deep taint analysis integration. Supports loading external rules from JSON
310
325
  files via `--rules`.
311
- - `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs optional VT lookups, handles
312
- escalation logic and user confirmation prompts.
326
+ - `src/scanner-ui.ts` — Presentation layer. Displays findings by severity, runs VT and skills.sh lookups in parallel,
327
+ handles escalation logic and user confirmation prompts.
313
328
  - `src/vt.ts` — VirusTotal API client. SHA-256 hashing, `GET /api/v3/files/{hash}` lookup, verdict mapping, graceful
314
329
  error handling.
330
+ - `src/skills-sh.ts` — skills.sh audit client. Fetches and HTML-parses third-party audit results (Snyk, Socket, Gen
331
+ Agent Trust Hub) for GitHub-sourced skills with a 5-second timeout; always resolves gracefully.
315
332
  - `src/deep-scan/` — Deep taint analysis engine (enabled via `--deep-scan`). Regex-based tokenizers extract sources,
316
333
  sinks, and assignments from Python/JS/TS files; a forward taint tracker propagates data flow; a cross-file analyzer
317
334
  detects multi-file attack patterns via import graph analysis. See [docs/deep-scan.md](docs/deep-scan.md).
@@ -320,6 +337,20 @@ pnpm format # Format code with Prettier
320
337
 
321
338
  ## Changelog
322
339
 
340
+ ### 1.1.2
341
+
342
+ - **skills.sh audit integration**: for GitHub-sourced skills, the CLI now fetches third-party audit results from
343
+ [skills.sh](https://skills.sh) (Snyk, Socket, Gen Agent Trust Hub) and displays them alongside local scan output
344
+ - A skills.sh Fail verdict from any auditor escalates severity to at least High, triggering a confirmation prompt
345
+ - Lookups run in parallel with VirusTotal and fail silently on any network or parse error
346
+
347
+ ### 1.1.1
348
+
349
+ - Removed anonymous usage telemetry inherited from the original Vercel `skills` CLI
350
+ - The upstream tool sent events to `https://add-skill.vercel.sh/t` on every command (install, remove, find, check, update) — this has been completely stripped out
351
+ - Removed `DISABLE_TELEMETRY` and `DO_NOT_TRACK` environment variables (no longer needed)
352
+ - Added 12 more regex rules to the scanner
353
+
323
354
  ### 1.1.0
324
355
 
325
356
  - Added `--rules <path>` flag to load external scan rules from JSON files or directories
@@ -342,6 +373,14 @@ pnpm format # Format code with Prettier
342
373
  - URL transparency: all external URLs in skill files are shown before installation
343
374
  - Scanner rules informed by Snyk and ClawHavoc research
344
375
 
376
+ ## Research
377
+
378
+ The scanner rules are informed by the following research into malicious agent skills:
379
+
380
+ - **Snyk (2025)** — [Analysis of 3,984 published agent skills](https://snyk.io/blog/), finding 76 confirmed malicious skills (13.4% of clawhub.ai had critical issues). Identified core attack taxonomy: data exfiltration, prompt injection, credential theft, and obfuscated payloads.
381
+ - **Koi Security (2025)** — [ClawHavoc: 341 Malicious ClawedBot Skills](https://www.koi.ai/blog/clawhavoc-341-malicious-clawedbot-skills-found-by-the-bot-they-were-targeting). Documented AMOS stealer droppers, password-protected archives, base64 payloads, macOS quarantine bypasses, and reverse shells in the wild.
382
+ - **arxiv 2602.06547v1 (2025)** — [Malicious Agent Skills at Scale](https://arxiv.org/abs/2602.06547v1). Large-scale analysis identifying attack taxonomies (E1-E3 exfiltration, P1-P4 prompt injection, SC1-SC3 supply chain, PE2-PE3 privilege escalation), MCP server abuse, agent hook interception, permission bypass flags, environment-gated sleeper patterns, invisible Unicode instruction smuggling, and the "industrial actor fingerprint" (credential access + remote execution, 97.6% sensitivity).
383
+
345
384
  ## Acknowledgments
346
385
 
347
386
  This project is a fork of [skills](https://github.com/vercel-labs/skills) by
package/dist/cli.mjs CHANGED
@@ -1357,27 +1357,6 @@ async function listInstalledSkills(options = {}) {
1357
1357
  } catch {}
1358
1358
  return Array.from(skillsMap.values());
1359
1359
  }
1360
- const TELEMETRY_URL = "https://add-skill.vercel.sh/t";
1361
- let cliVersion = null;
1362
- function isCI() {
1363
- return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION);
1364
- }
1365
- function isEnabled() {
1366
- return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK;
1367
- }
1368
- function setVersion(version) {
1369
- cliVersion = version;
1370
- }
1371
- function track(data) {
1372
- if (!isEnabled()) return;
1373
- try {
1374
- const params = new URLSearchParams();
1375
- if (cliVersion) params.set("v", cliVersion);
1376
- if (isCI()) params.set("ci", "1");
1377
- for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) params.set(key, String(value));
1378
- fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => {});
1379
- } catch {}
1380
- }
1381
1360
  var ProviderRegistryImpl = class {
1382
1361
  providers = [];
1383
1362
  register(provider) {
@@ -2569,6 +2548,12 @@ const SCAN_RULES = [
2569
2548
  description: "Base64 encoding piped to network command",
2570
2549
  pattern: /base64\s.*\|\s*(?:curl|wget|fetch|nc|ncat)/i
2571
2550
  },
2551
+ {
2552
+ id: "exfil-fs-enumeration",
2553
+ severity: "high",
2554
+ description: "Programmatic scanning of sensitive directories",
2555
+ pattern: /(?:glob\.(?:glob|iglob)|os\.walk|os\.listdir|os\.scandir|pathlib\.Path\s*\([^)]*\)\.(?:glob|iterdir|rglob))\s*\(\s*['"].*(?:\.ssh|\.aws|\.gnupg|\.config|\.env|credential|secret|\.kube|\.docker)/i
2556
+ },
2572
2557
  {
2573
2558
  id: "injection-ignore-instructions",
2574
2559
  severity: "critical",
@@ -2599,6 +2584,24 @@ const SCAN_RULES = [
2599
2584
  description: "Known jailbreak phrase (DAN)",
2600
2585
  pattern: /\bDAN\b.*(?:do\s+anything\s+now|jailbreak|ignore\s+(?:all\s+)?(?:safety|rules))/i
2601
2586
  },
2587
+ {
2588
+ id: "injection-markdown-comment",
2589
+ severity: "high",
2590
+ description: "Hidden instructions in markdown reference-link comment syntax",
2591
+ pattern: /\[\/\/\]:\s*#\s*[\("']/i
2592
+ },
2593
+ {
2594
+ id: "injection-coercive-language",
2595
+ severity: "medium",
2596
+ description: "Authoritative urgency language used in social engineering",
2597
+ pattern: /\b(?:NON[\s-]?NEGOTIABLE|MANDATORY\s+(?:ACTIVATION|COMPLIANCE|EXECUTION)\s+PROTOCOL|SEVERE\s+VIOLATION|CRITICAL\s+COMPLIANCE\s+REQUIRED|IMMEDIATE\s+(?:ACTION|EXECUTION)\s+REQUIRED)\b/
2598
+ },
2599
+ {
2600
+ id: "injection-invisible-unicode",
2601
+ severity: "high",
2602
+ description: "Clusters of zero-width/invisible Unicode characters (instruction smuggling)",
2603
+ pattern: /[\u200B\u200C\u200D\u2060\u2062\u2063\u2064\uFEFF]{3,}|[\u2066\u2067\u2069]{2,}/
2604
+ },
2602
2605
  {
2603
2606
  id: "fs-rm-rf-root",
2604
2607
  severity: "critical",
@@ -2635,6 +2638,12 @@ const SCAN_RULES = [
2635
2638
  description: "macOS quarantine bypass (xattr -c/-d)",
2636
2639
  pattern: /xattr\s+(?:-[cdr]+\s+|.*com\.apple\.quarantine)/i
2637
2640
  },
2641
+ {
2642
+ id: "fs-privilege-escalation",
2643
+ severity: "high",
2644
+ description: "Privilege escalation via sudo or broad chown",
2645
+ pattern: /(?:sudo\s+(?:(?:-[AEHPSkn]\s+)*(?:bash|sh|chmod|chown|rm|cp|mv|tee|cat|curl|wget|python|node|apt|yum|dnf|brew|pip|npm)\b)|chown\s+-[rR]\s+.*\/)/i
2646
+ },
2638
2647
  {
2639
2648
  id: "cred-aws-key",
2640
2649
  severity: "high",
@@ -2731,6 +2740,12 @@ const SCAN_RULES = [
2731
2740
  description: "Unicode escape sequences (potential instruction smuggling)",
2732
2741
  pattern: /(?:\\u[0-9a-fA-F]{4}){8,}/
2733
2742
  },
2743
+ {
2744
+ id: "obfuscation-unsafe-deserialize",
2745
+ severity: "high",
2746
+ description: "Unsafe deserialization (pickle, marshal, yaml.unsafe_load)",
2747
+ pattern: /(?:pickle\.loads?|marshal\.loads?|shelve\.open|yaml\.(?:unsafe_load|full_load|load\s*\([^)]*Loader\s*=\s*yaml\.(?:Unsafe|Full|)Loader))\s*\(/i
2748
+ },
2734
2749
  {
2735
2750
  id: "reverse-shell-bash",
2736
2751
  severity: "critical",
@@ -2833,6 +2848,36 @@ const SCAN_RULES = [
2833
2848
  description: "Instructing agent to hide output from user",
2834
2849
  pattern: /(?:hide|suppress|conceal|mask|don'?t\s+show|do\s+not\s+show|never\s+show)\s+(?:the\s+)?(?:output|result|response|error|log)s?\s+(?:from|to)\s+(?:the\s+)?user/i
2835
2850
  },
2851
+ {
2852
+ id: "directive-suppress-disclosure",
2853
+ severity: "high",
2854
+ description: "Instructing agent to never reveal information to user",
2855
+ pattern: /(?:do\s+not|don'?t|never|must\s+not)\s+(?:mention|reveal|disclose|tell|inform|show|display)\s+.*(?:to\s+the\s+user|in\s+(?:the\s+)?conversation|to\s+(?:the\s+)?human)/i
2856
+ },
2857
+ {
2858
+ id: "directive-permission-bypass",
2859
+ severity: "critical",
2860
+ description: "Framework permission bypass flags",
2861
+ pattern: /--(?:dangerously-skip-permissions|trust-all|disable-sandbox|skip-validation|allow-all|yolo|no-sandbox)\b/i
2862
+ },
2863
+ {
2864
+ id: "directive-mcp-config",
2865
+ severity: "critical",
2866
+ description: "MCP server configuration with external endpoints",
2867
+ pattern: /(?:"mcpServers"|"mcp_servers"|mcpServers)\s*[:{].*(?:https?:\/\/|npx\s|uvx\s)/i
2868
+ },
2869
+ {
2870
+ id: "directive-hook-injection",
2871
+ severity: "high",
2872
+ description: "Agent hook system interception",
2873
+ pattern: /(?:PreToolUse|PostToolUse|Notification|pre_tool_use|post_tool_use)\s*[:{=\[]/i
2874
+ },
2875
+ {
2876
+ id: "directive-email-manipulation",
2877
+ severity: "high",
2878
+ description: "BCC injection or email auto-forwarding",
2879
+ pattern: /(?:(?:add|include|always\s+(?:add|include|append))\s+.*(?:bcc|cc)\s*[:=]|(?:forward|redirect)\s+.*(?:email|mail|message)\s+to\s+|auto[\s-]?forward\s+.*to\s+)/i
2880
+ },
2836
2881
  {
2837
2882
  id: "directive-disable-safety",
2838
2883
  severity: "critical",
@@ -2922,6 +2967,12 @@ const SCAN_RULES = [
2922
2967
  severity: "high",
2923
2968
  description: "Temp file write + execute chain",
2924
2969
  pattern: /(?:\/tmp\/|%TEMP%|TMPDIR|\$TMPDIR|tempfile|mktemp).*(?:chmod\s+\+x|\.\/|python|node|bash|sh)\b/
2970
+ },
2971
+ {
2972
+ id: "exec-environment-gated",
2973
+ severity: "high",
2974
+ description: "Environment/hostname/time-gated conditional execution (sleeper pattern)",
2975
+ pattern: /(?:if\s+.*(?:hostname|socket\.gethostname|os\.uname|platform\.node|os\.getenv\s*\(\s*['"](?:ENV|ENVIRONMENT|NODE_ENV|DEPLOY|STAGE)|getpass\.getuser|os\.getuid)\s*.*(?:==|!=|in\s+|not\s+in))|(?:datetime|time)\..*(?:hour|weekday|isoweekday)\s*(?:\(\))?\s*(?:>=?|<=?|==|!=|in\s+|not\s+in)\s*(?:\d|[\[(])/i
2925
2976
  }
2926
2977
  ];
2927
2978
  const CORRELATION_RULES = [
@@ -2990,6 +3041,28 @@ const CORRELATION_RULES = [
2990
3041
  "obfuscation-base64-block"
2991
3042
  ] }]
2992
3043
  },
3044
+ {
3045
+ id: "corr-credential-remote-exec",
3046
+ severity: "critical",
3047
+ description: "Credential access combined with remote script execution (industrial actor pattern)",
3048
+ conditions: [{ anyOf: [
3049
+ "cred-aws-key",
3050
+ "cred-openai-key",
3051
+ "cred-private-key",
3052
+ "cred-github-token",
3053
+ "cred-slack-token",
3054
+ "cred-stripe-key",
3055
+ "cred-anthropic-key",
3056
+ "cred-agent-config-access",
3057
+ "exfil-env-read"
3058
+ ] }, { anyOf: [
3059
+ "download-curl-pipe-sh",
3060
+ "download-pipe-python",
3061
+ "download-exec-binary",
3062
+ "download-curl-subshell",
3063
+ "remote-instruction-load"
3064
+ ] }]
3065
+ },
2993
3066
  {
2994
3067
  id: "corr-injection-exec",
2995
3068
  severity: "critical",
@@ -2999,7 +3072,9 @@ const CORRELATION_RULES = [
2999
3072
  "injection-new-persona",
3000
3073
  "injection-hidden-html",
3001
3074
  "injection-system-prompt",
3002
- "injection-do-anything-now"
3075
+ "injection-do-anything-now",
3076
+ "injection-markdown-comment",
3077
+ "injection-invisible-unicode"
3003
3078
  ] }, { anyOf: [
3004
3079
  "exec-python-os-system",
3005
3080
  "exec-python-subprocess",
@@ -3016,7 +3091,8 @@ const CORRELATION_RULES = [
3016
3091
  conditions: [{ anyOf: [
3017
3092
  "directive-silent-exec",
3018
3093
  "directive-hide-output",
3019
- "directive-no-confirm"
3094
+ "directive-no-confirm",
3095
+ "directive-suppress-disclosure"
3020
3096
  ] }, { anyOf: SCAN_RULES.filter((r) => /^(?:exec-|download-|fs-)/.test(r.id)).map((r) => r.id) }]
3021
3097
  }
3022
3098
  ];
@@ -3179,7 +3255,7 @@ function extractRemoteSkillFiles(remoteSkill) {
3179
3255
  function extractWellKnownSkillFiles(skill) {
3180
3256
  return skill.files;
3181
3257
  }
3182
- const NOT_FOUND = {
3258
+ const NOT_FOUND$1 = {
3183
3259
  found: false,
3184
3260
  verdict: "unknown",
3185
3261
  maliciousCount: 0,
@@ -3190,19 +3266,19 @@ async function lookupFileHash(sha256, apiKey) {
3190
3266
  try {
3191
3267
  response = await fetch(`https://www.virustotal.com/api/v3/files/${sha256}`, { headers: { "x-apikey": apiKey } });
3192
3268
  } catch {
3193
- return NOT_FOUND;
3269
+ return NOT_FOUND$1;
3194
3270
  }
3195
- if (response.status === 404) return NOT_FOUND;
3196
- if (response.status === 429) return NOT_FOUND;
3197
- if (!response.ok) return NOT_FOUND;
3271
+ if (response.status === 404) return NOT_FOUND$1;
3272
+ if (response.status === 429) return NOT_FOUND$1;
3273
+ if (!response.ok) return NOT_FOUND$1;
3198
3274
  let body;
3199
3275
  try {
3200
3276
  body = await response.json();
3201
3277
  } catch {
3202
- return NOT_FOUND;
3278
+ return NOT_FOUND$1;
3203
3279
  }
3204
3280
  const attrs = body.data?.attributes;
3205
- if (!attrs) return NOT_FOUND;
3281
+ if (!attrs) return NOT_FOUND$1;
3206
3282
  const stats = attrs.last_analysis_stats;
3207
3283
  const maliciousCount = stats?.malicious ?? 0;
3208
3284
  const totalEngines = stats ? Object.values(stats).reduce((sum, n) => sum + n, 0) : 0;
@@ -3228,6 +3304,64 @@ async function lookupFileHash(sha256, apiKey) {
3228
3304
  async function checkSkillOnVT(skillContent, apiKey) {
3229
3305
  return lookupFileHash(createHash("sha256").update(skillContent).digest("hex"), apiKey);
3230
3306
  }
3307
+ const AUDITORS = [
3308
+ {
3309
+ id: "snyk",
3310
+ displayName: "Snyk"
3311
+ },
3312
+ {
3313
+ id: "socket",
3314
+ displayName: "Socket"
3315
+ },
3316
+ {
3317
+ id: "agent-trust-hub",
3318
+ displayName: "Gen Agent Trust Hub"
3319
+ }
3320
+ ];
3321
+ const NOT_FOUND = {
3322
+ found: false,
3323
+ permalink: "",
3324
+ audits: [],
3325
+ anyFail: false
3326
+ };
3327
+ function parseAudits(html, baseUrl) {
3328
+ return AUDITORS.map(({ id, displayName }) => {
3329
+ const pattern = new RegExp(`\\/security\\/${id}[^]{0,400}?\\b(Pass|Fail)\\b`, "is");
3330
+ const match = html.match(pattern);
3331
+ let status = "unknown";
3332
+ if (match) status = match[1].toLowerCase() === "pass" ? "pass" : "fail";
3333
+ return {
3334
+ auditor: id,
3335
+ displayName,
3336
+ status,
3337
+ permalink: `${baseUrl}/security/${id}`
3338
+ };
3339
+ });
3340
+ }
3341
+ async function checkSkillOnSkillsSh(source) {
3342
+ const { owner, repo, skillFolder } = source;
3343
+ const permalink = `https://skills.sh/${owner}/${repo}/${skillFolder}`;
3344
+ try {
3345
+ const controller = new AbortController();
3346
+ const timeout = setTimeout(() => controller.abort(), 5e3);
3347
+ let response;
3348
+ try {
3349
+ response = await fetch(permalink, { signal: controller.signal });
3350
+ } finally {
3351
+ clearTimeout(timeout);
3352
+ }
3353
+ if (!response.ok) return NOT_FOUND;
3354
+ const audits = parseAudits(await response.text(), permalink);
3355
+ return {
3356
+ found: true,
3357
+ permalink,
3358
+ audits,
3359
+ anyFail: audits.some((a) => a.status === "fail")
3360
+ };
3361
+ } catch {
3362
+ return NOT_FOUND;
3363
+ }
3364
+ }
3231
3365
  const SEVERITY_LABELS = {
3232
3366
  critical: import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" CRITICAL "))),
3233
3367
  high: import_picocolors.default.red(import_picocolors.default.bold("HIGH")),
@@ -3256,6 +3390,17 @@ function displayVTVerdict(verdict) {
3256
3390
  }
3257
3391
  if (verdict.permalink) M.message(import_picocolors.default.dim(` ${verdict.permalink}`));
3258
3392
  }
3393
+ function displaySkillsShResult(result) {
3394
+ if (!result.found || result.audits.length === 0) return;
3395
+ const badges = result.audits.map((a) => {
3396
+ const name = a.auditor === "agent-trust-hub" ? "Trust Hub" : a.displayName;
3397
+ if (a.status === "pass") return `[${import_picocolors.default.green(`${name} ✓`)}]`;
3398
+ if (a.status === "fail") return `[${import_picocolors.default.red(`${name} ✗`)}]`;
3399
+ return `[${import_picocolors.default.dim(`${name} ~`)}]`;
3400
+ }).join(" ");
3401
+ M.message(` ${import_picocolors.default.cyan("◆")} skills.sh: ${result.audits.length} audits ${badges}`);
3402
+ M.message(import_picocolors.default.dim(` ${result.permalink}`));
3403
+ }
3259
3404
  async function presentScanResults(results, options) {
3260
3405
  const allFindings = results.flatMap((r) => r.findings.map((f) => ({
3261
3406
  ...f,
@@ -3263,15 +3408,26 @@ async function presentScanResults(results, options) {
3263
3408
  })));
3264
3409
  const allUrls = [...new Set(results.flatMap((r) => r.urls))];
3265
3410
  const vtVerdicts = /* @__PURE__ */ new Map();
3411
+ const skillsShResults = /* @__PURE__ */ new Map();
3266
3412
  let vtEscalate = false;
3267
- if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3268
- const verdict = await checkSkillOnVT(content, options.vtKey);
3269
- vtVerdicts.set(skillName, verdict);
3270
- if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3271
- } catch {}
3272
- if (allFindings.length === 0 && !vtEscalate) {
3413
+ let skillsShEscalate = false;
3414
+ await Promise.all([(async () => {
3415
+ if (options.vtKey && options.skillContents) for (const [skillName, content] of options.skillContents) try {
3416
+ const verdict = await checkSkillOnVT(content, options.vtKey);
3417
+ vtVerdicts.set(skillName, verdict);
3418
+ if (verdict.found && verdict.verdict === "malicious") vtEscalate = true;
3419
+ } catch {}
3420
+ })(), (async () => {
3421
+ if (options.skillsShSources) await Promise.all([...options.skillsShSources.entries()].map(async ([skillName, source]) => {
3422
+ const result = await checkSkillOnSkillsSh(source);
3423
+ skillsShResults.set(skillName, result);
3424
+ if (result.anyFail) skillsShEscalate = true;
3425
+ }));
3426
+ })()]);
3427
+ if (allFindings.length === 0 && !vtEscalate && !skillsShEscalate) {
3273
3428
  M.success(import_picocolors.default.green("Security scan passed — no issues found"));
3274
3429
  if (vtVerdicts.size > 0) for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
3430
+ for (const [, result] of skillsShResults) displaySkillsShResult(result);
3275
3431
  if (allUrls.length > 0) return displayUrlsAndPrompt(allUrls, options);
3276
3432
  return true;
3277
3433
  }
@@ -3295,6 +3451,10 @@ async function presentScanResults(results, options) {
3295
3451
  console.log();
3296
3452
  for (const [, verdict] of vtVerdicts) displayVTVerdict(verdict);
3297
3453
  }
3454
+ if (skillsShResults.size > 0) {
3455
+ console.log();
3456
+ for (const [, result] of skillsShResults) displaySkillsShResult(result);
3457
+ }
3298
3458
  if (allUrls.length > 0) {
3299
3459
  console.log();
3300
3460
  M.info(`External URLs found in skill files (${allUrls.length}):`);
@@ -3302,6 +3462,7 @@ async function presentScanResults(results, options) {
3302
3462
  }
3303
3463
  console.log();
3304
3464
  if (vtEscalate) overallMax = "critical";
3465
+ if (skillsShEscalate && SEVERITY_ORDER[overallMax] < SEVERITY_ORDER["high"]) overallMax = "high";
3305
3466
  if (SEVERITY_ORDER[overallMax] <= SEVERITY_ORDER["medium"]) {
3306
3467
  M.info(import_picocolors.default.dim("Low/medium severity findings — proceeding with installation"));
3307
3468
  return true;
@@ -3335,16 +3496,7 @@ async function displayUrlsAndPrompt(urls, options) {
3335
3496
  if (pD(confirmed) || !confirmed) return false;
3336
3497
  return true;
3337
3498
  }
3338
- var version$1 = "1.1.0";
3339
3499
  const isCancelled = (value) => typeof value === "symbol";
3340
- async function isSourcePrivate(source) {
3341
- const ownerRepo = parseOwnerRepo(source);
3342
- if (!ownerRepo) return false;
3343
- return isRepoPrivate(ownerRepo.owner, ownerRepo.repo);
3344
- }
3345
- function initTelemetry(version) {
3346
- setVersion(version);
3347
- }
3348
3500
  const _externalRulesCache = /* @__PURE__ */ new Map();
3349
3501
  function resolveExternalRules(options) {
3350
3502
  if (!options.rules) return void 0;
@@ -3355,6 +3507,24 @@ function resolveExternalRules(options) {
3355
3507
  _externalRulesCache.set(key, loaded);
3356
3508
  return loaded;
3357
3509
  }
3510
+ function buildSkillsShSourcesForRemote(remoteSkill, _url) {
3511
+ const sourceUrl = remoteSkill.sourceUrl;
3512
+ if (!sourceUrl || !sourceUrl.includes("github.com")) return void 0;
3513
+ try {
3514
+ const parts = new URL(sourceUrl).pathname.slice(1).replace(/\.git$/, "").split("/").filter(Boolean);
3515
+ const ownerRepo = parseOwnerRepo(parts.slice(0, 2).join("/"));
3516
+ if (!ownerRepo) return void 0;
3517
+ const skillFolder = parts.length > 2 ? parts.at(-1) : remoteSkill.installName;
3518
+ const sources = /* @__PURE__ */ new Map();
3519
+ sources.set(remoteSkill.installName, {
3520
+ ...ownerRepo,
3521
+ skillFolder
3522
+ });
3523
+ return sources;
3524
+ } catch {
3525
+ return;
3526
+ }
3527
+ }
3358
3528
  function shortenPath$1(fullPath, cwd) {
3359
3529
  const home = homedir();
3360
3530
  if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
@@ -3468,7 +3638,6 @@ async function selectAgentsInteractive(options) {
3468
3638
  } catch {}
3469
3639
  return selected;
3470
3640
  }
3471
- setVersion(version$1);
3472
3641
  async function handleRemoteSkill(source, url, options, spinner) {
3473
3642
  const provider = findProvider(url);
3474
3643
  if (!provider) {
@@ -3615,10 +3784,12 @@ async function handleRemoteSkill(source, url, options, spinner) {
3615
3784
  });
3616
3785
  const vtKey = options.vtKey || process.env.VT_API_KEY;
3617
3786
  const skillContents = vtKey ? new Map([[remoteSkill.installName, remoteSkill.content]]) : void 0;
3787
+ const skillsShSources = buildSkillsShSourcesForRemote(remoteSkill, url);
3618
3788
  if (!await presentScanResults([scanResult], {
3619
3789
  yes: options.yes,
3620
3790
  vtKey,
3621
- skillContents
3791
+ skillContents,
3792
+ skillsShSources
3622
3793
  })) {
3623
3794
  xe("Installation cancelled due to security concerns");
3624
3795
  process.exit(0);
@@ -3648,15 +3819,6 @@ async function handleRemoteSkill(source, url, options, spinner) {
3648
3819
  console.log();
3649
3820
  const successful = results.filter((r) => r.success);
3650
3821
  const failed = results.filter((r) => !r.success);
3651
- if (await isSourcePrivate(remoteSkill.sourceIdentifier) !== true) track({
3652
- event: "install",
3653
- source: remoteSkill.sourceIdentifier,
3654
- skills: remoteSkill.installName,
3655
- agents: targetAgents.join(","),
3656
- ...installGlobally && { global: "1" },
3657
- skillFiles: JSON.stringify({ [remoteSkill.installName]: url }),
3658
- sourceType: remoteSkill.providerId
3659
- });
3660
3822
  if (successful.length > 0 && installGlobally) try {
3661
3823
  let skillFolderHash = "";
3662
3824
  if (remoteSkill.providerId === "github") {
@@ -3927,19 +4089,8 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3927
4089
  console.log();
3928
4090
  const successful = results.filter((r) => r.success);
3929
4091
  const failed = results.filter((r) => !r.success);
3930
- const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3931
- const skillFiles = {};
3932
- for (const skill of selectedSkills) skillFiles[skill.installName] = skill.sourceUrl;
3933
- if (await isSourcePrivate(sourceIdentifier) !== true) track({
3934
- event: "install",
3935
- source: sourceIdentifier,
3936
- skills: selectedSkills.map((s) => s.installName).join(","),
3937
- agents: targetAgents.join(","),
3938
- ...installGlobally && { global: "1" },
3939
- skillFiles: JSON.stringify(skillFiles),
3940
- sourceType: "well-known"
3941
- });
3942
4092
  if (successful.length > 0 && installGlobally) {
4093
+ const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3943
4094
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
3944
4095
  for (const skill of selectedSkills) if (successfulSkillNames.has(skill.installName)) try {
3945
4096
  await addSkillToLock(skill.installName, {
@@ -4150,15 +4301,6 @@ async function handleDirectUrlSkillLegacy(source, url, options, spinner) {
4150
4301
  console.log();
4151
4302
  const successful = results.filter((r) => r.success);
4152
4303
  const failed = results.filter((r) => !r.success);
4153
- track({
4154
- event: "install",
4155
- source: "mintlify/com",
4156
- skills: remoteSkill.installName,
4157
- agents: targetAgents.join(","),
4158
- ...installGlobally && { global: "1" },
4159
- skillFiles: JSON.stringify({ [remoteSkill.installName]: url }),
4160
- sourceType: "mintlify"
4161
- });
4162
4304
  if (successful.length > 0 && installGlobally) try {
4163
4305
  await addSkillToLock(remoteSkill.installName, {
4164
4306
  source: `mintlify/${remoteSkill.installName}`,
@@ -4456,10 +4598,24 @@ async function runAdd(args, options = {}) {
4456
4598
  }
4457
4599
  }
4458
4600
  spinner.stop("Security scan complete");
4601
+ const skillsShSources = /* @__PURE__ */ new Map();
4602
+ if (parsed.type === "github") {
4603
+ const ownerRepoStr = getOwnerRepo(parsed);
4604
+ const ownerRepo = ownerRepoStr ? parseOwnerRepo(ownerRepoStr) : null;
4605
+ if (ownerRepo) for (const skill of selectedSkills) {
4606
+ const displayName = getSkillDisplayName(skill);
4607
+ const skillFolder = skill.path.split("/").at(-1) ?? skill.name;
4608
+ skillsShSources.set(displayName, {
4609
+ ...ownerRepo,
4610
+ skillFolder
4611
+ });
4612
+ }
4613
+ }
4459
4614
  if (!await presentScanResults(scanResults, {
4460
4615
  yes: options.yes,
4461
4616
  vtKey,
4462
- skillContents
4617
+ skillContents,
4618
+ skillsShSources: skillsShSources.size > 0 ? skillsShSources : void 0
4463
4619
  })) {
4464
4620
  xe("Installation cancelled due to security concerns");
4465
4621
  await cleanup(tempDir);
@@ -4491,6 +4647,7 @@ async function runAdd(args, options = {}) {
4491
4647
  console.log();
4492
4648
  const successful = results.filter((r) => r.success);
4493
4649
  const failed = results.filter((r) => !r.success);
4650
+ const normalizedSource = getOwnerRepo(parsed);
4494
4651
  const skillFiles = {};
4495
4652
  for (const skill of selectedSkills) {
4496
4653
  let relativePath;
@@ -4499,27 +4656,6 @@ async function runAdd(args, options = {}) {
4499
4656
  else continue;
4500
4657
  skillFiles[skill.name] = relativePath;
4501
4658
  }
4502
- const normalizedSource = getOwnerRepo(parsed);
4503
- if (normalizedSource) {
4504
- const ownerRepo = parseOwnerRepo(normalizedSource);
4505
- if (ownerRepo) {
4506
- if (await isRepoPrivate(ownerRepo.owner, ownerRepo.repo) === false) track({
4507
- event: "install",
4508
- source: normalizedSource,
4509
- skills: selectedSkills.map((s) => s.name).join(","),
4510
- agents: targetAgents.join(","),
4511
- ...installGlobally && { global: "1" },
4512
- skillFiles: JSON.stringify(skillFiles)
4513
- });
4514
- } else track({
4515
- event: "install",
4516
- source: normalizedSource,
4517
- skills: selectedSkills.map((s) => s.name).join(","),
4518
- agents: targetAgents.join(","),
4519
- ...installGlobally && { global: "1" },
4520
- skillFiles: JSON.stringify(skillFiles)
4521
- });
4522
- }
4523
4659
  if (successful.length > 0 && installGlobally && normalizedSource) {
4524
4660
  const successfulSkillNames = new Set(successful.map((r) => r.skill));
4525
4661
  for (const skill of selectedSkills) {
@@ -4848,11 +4984,6 @@ ${DIM$2} 1) npx skills find [query]${RESET$2}
4848
4984
  ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4849
4985
  if (query) {
4850
4986
  const results = await searchSkillsAPI(query);
4851
- track({
4852
- event: "find",
4853
- query,
4854
- resultCount: String(results.length)
4855
- });
4856
4987
  if (results.length === 0) {
4857
4988
  console.log(`${DIM$2}No skills found for "${query}"${RESET$2}`);
4858
4989
  return;
@@ -4872,12 +5003,6 @@ ${DIM$2} 2) npx skills add <owner/repo@skill>${RESET$2}`;
4872
5003
  console.log();
4873
5004
  }
4874
5005
  const selected = await runSearchPrompt();
4875
- track({
4876
- event: "find",
4877
- query: "",
4878
- resultCount: selected ? "1" : "0",
4879
- interactive: "1"
4880
- });
4881
5006
  if (!selected) {
4882
5007
  console.log(`${DIM$2}Search cancelled${RESET$2}`);
4883
5008
  console.log();
@@ -5091,24 +5216,6 @@ async function removeCommand(skillNames, options) {
5091
5216
  spinner.stop("Removal process complete");
5092
5217
  const successful = results.filter((r) => r.success);
5093
5218
  const failed = results.filter((r) => !r.success);
5094
- if (successful.length > 0) {
5095
- const bySource = /* @__PURE__ */ new Map();
5096
- for (const r of successful) {
5097
- const source = r.source || "local";
5098
- const existing = bySource.get(source) || { skills: [] };
5099
- existing.skills.push(r.skill);
5100
- existing.sourceType = r.sourceType;
5101
- bySource.set(source, existing);
5102
- }
5103
- for (const [source, data] of bySource) track({
5104
- event: "remove",
5105
- source,
5106
- skills: data.skills.join(","),
5107
- agents: targetAgents.join(","),
5108
- ...isGlobal && { global: "1" },
5109
- sourceType: data.sourceType
5110
- });
5111
- }
5112
5219
  if (successful.length > 0) M.success(import_picocolors.default.green(`Successfully removed ${successful.length} skill(s)`));
5113
5220
  if (failed.length > 0) {
5114
5221
  M.error(import_picocolors.default.red(`Failed to remove ${failed.length} skill(s)`));
@@ -5152,7 +5259,6 @@ function getVersion() {
5152
5259
  }
5153
5260
  }
5154
5261
  const VERSION = getVersion();
5155
- initTelemetry(VERSION);
5156
5262
  const RESET = "\x1B[0m";
5157
5263
  const BOLD = "\x1B[1m";
5158
5264
  const DIM = "\x1B[38;5;102m";
@@ -5436,11 +5542,6 @@ async function runCheck(args = []) {
5436
5542
  console.log();
5437
5543
  console.log(`${DIM}Could not check ${errors.length} skill(s) (may need reinstall)${RESET}`);
5438
5544
  }
5439
- track({
5440
- event: "check",
5441
- skillCount: String(totalSkills),
5442
- updatesAvailable: String(updates.length)
5443
- });
5444
5545
  console.log();
5445
5546
  }
5446
5547
  async function runUpdate() {
@@ -5516,12 +5617,6 @@ async function runUpdate() {
5516
5617
  console.log();
5517
5618
  if (successCount > 0) console.log(`${TEXT}✓ Updated ${successCount} skill(s)${RESET}`);
5518
5619
  if (failCount > 0) console.log(`${DIM}Failed to update ${failCount} skill(s)${RESET}`);
5519
- track({
5520
- event: "update",
5521
- skillCount: String(updates.length),
5522
- successCount: String(successCount),
5523
- failCount: String(failCount)
5524
- });
5525
5620
  console.log();
5526
5621
  }
5527
5622
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillsio",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "The SECURE open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {