skill-checker 0.1.7 → 0.1.9

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 CHANGED
@@ -59,6 +59,18 @@ skill-checker scan ./my-skill --policy strict
59
59
 
60
60
  Exit code `0` = no critical issues, `1` = critical issues detected.
61
61
 
62
+ ### Recommended Scan Path
63
+
64
+ Skill Checker is designed to scan individual skill directories containing a `SKILL.md` file at the root. Running `scan .` from a project root or non-skill directory will produce noisy results (e.g. STRUCT-001 for missing SKILL.md).
65
+
66
+ ```bash
67
+ # Correct: point to a skill directory
68
+ skill-checker scan ./path/to/my-skill/
69
+
70
+ # Avoid: scanning project root or arbitrary directories
71
+ skill-checker scan .
72
+ ```
73
+
62
74
  ## Hook Integration
63
75
 
64
76
  Skill Checker can run automatically as a Claude Code [PreToolUse hook](https://docs.anthropic.com/en/docs/claude-code/hooks), intercepting skill file writes before they happen.
@@ -98,28 +110,6 @@ The hook is fail-closed — if the scanner is unavailable, JSON parsing fails, o
98
110
  - `jq` must be installed for JSON parsing
99
111
  - `skill-checker` must be globally installed or available via `npx`
100
112
 
101
- ## Dependency Security Maintenance
102
-
103
- Latest dependency audit follow-up (2026-03-07):
104
-
105
- - Production dependency risk remains unaffected (`npm audit --omit=dev`: **0 vulnerabilities**).
106
- - Current `npm audit` still reports **5 moderate** findings in dev tooling chain (`vitest` → `vite` → `esbuild`).
107
- - Upgrade to `vitest@4.0.18` is **temporarily deferred** because it requires Node `^20 || ^22 || >=24`, while this project currently supports Node `>=18`.
108
- - Scope of impact is limited to development/test tooling and does not affect runtime package dependencies.
109
- - Auditable risk acceptance record: [docs/RISK_ACCEPTANCE_DEVDEPS.md](docs/RISK_ACCEPTANCE_DEVDEPS.md).
110
-
111
- Verification commands used in this review cycle:
112
-
113
- ```bash
114
- npm run lint
115
- npm test
116
- npm run build
117
- npm audit --omit=dev
118
- npm audit
119
- ```
120
-
121
- Next review date: **2026-04-04**.
122
-
123
113
  ## Scoring
124
114
 
125
115
  Base score starts at **100**. Each finding deducts points by severity:
package/dist/cli.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // src/cli.ts
2
2
  import { createRequire } from "module";
3
+ import { existsSync as existsSync4 } from "fs";
4
+ import { join as join5 } from "path";
3
5
  import { Command } from "commander";
4
6
 
5
7
  // src/parser.ts
@@ -196,15 +198,12 @@ ${tailContent}` : headContent;
196
198
  // src/checks/structural.ts
197
199
  var HYPHEN_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
198
200
  var MAX_NAME_LENGTH = 64;
199
- var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
200
- ".exe",
201
- ".bat",
202
- ".cmd",
201
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
203
202
  ".sh",
204
203
  ".bash",
205
204
  ".ps1",
206
- ".com",
207
- ".msi"
205
+ ".bat",
206
+ ".cmd"
208
207
  ]);
209
208
  var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
210
209
  ".exe",
@@ -216,6 +215,10 @@ var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
216
215
  ".class",
217
216
  ".pyc"
218
217
  ]);
218
+ var INSTALLER_EXTENSIONS = /* @__PURE__ */ new Set([
219
+ ".com",
220
+ ".msi"
221
+ ]);
219
222
  var structuralChecks = {
220
223
  name: "Structural Validity",
221
224
  category: "STRUCT",
@@ -269,7 +272,7 @@ var structuralChecks = {
269
272
  }
270
273
  for (const file of skill.files) {
271
274
  const ext = file.extension.toLowerCase();
272
- if (BINARY_EXTENSIONS2.has(ext) || EXECUTABLE_EXTENSIONS.has(ext)) {
275
+ if (BINARY_EXTENSIONS2.has(ext) || INSTALLER_EXTENSIONS.has(ext)) {
273
276
  results.push({
274
277
  id: "STRUCT-006",
275
278
  category: "STRUCT",
@@ -278,6 +281,15 @@ var structuralChecks = {
278
281
  message: `Found unexpected file: ${file.path} (${ext})`,
279
282
  source: file.path
280
283
  });
284
+ } else if (SCRIPT_EXTENSIONS.has(ext)) {
285
+ results.push({
286
+ id: "STRUCT-006",
287
+ category: "STRUCT",
288
+ severity: "LOW",
289
+ title: "Script file present",
290
+ message: `Found script file: ${file.path} (${ext}). Content is scanned separately.`,
291
+ source: file.path
292
+ });
281
293
  }
282
294
  }
283
295
  const name = skill.frontmatter.name;
@@ -362,6 +374,15 @@ function isInDocumentationContext(lines, lineIndex) {
362
374
  }
363
375
  return false;
364
376
  }
377
+ function isNearDocumentationHeader(lines, lineIndex) {
378
+ for (let i = lineIndex; i >= Math.max(0, lineIndex - 15); i--) {
379
+ const l = lines[i];
380
+ if (/^#{1,4}\s+.*(install|setup|prerequisite|requirement|depend|getting\s+started|quickstart)/i.test(l)) {
381
+ return true;
382
+ }
383
+ }
384
+ return false;
385
+ }
365
386
  function isLicenseFile(filePath) {
366
387
  const name = filePath.split("/").pop()?.toUpperCase() ?? "";
367
388
  const base = name.replace(/\.[^.]+$/, "");
@@ -1702,14 +1723,58 @@ var supplyChainChecks = {
1702
1723
  if (NPM_INSTALL_PATTERN.test(line) || PIP_INSTALL_PATTERN.test(line)) {
1703
1724
  const allLines = getAllLines(skill);
1704
1725
  const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
1705
- const isDoc = globalIdx >= 0 && isInDocumentationContext(
1726
+ const isDoc = source === "SKILL.md" && globalIdx >= 0 && isInDocumentationContext(
1706
1727
  allLines.map((l) => l.line),
1707
1728
  globalIdx
1708
1729
  );
1709
- if (!isDoc) {
1730
+ const srcLines = getLinesForSource(skill, source);
1731
+ const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
1732
+ const inCodeBlock = localIdx >= 0 && isInCodeBlock(srcLines, localIdx);
1733
+ if (isDoc && !inCodeBlock) {
1734
+ } else {
1710
1735
  let severity = "HIGH";
1711
1736
  let reducedFrom;
1712
1737
  let msgSuffix = "";
1738
+ if (inCodeBlock) {
1739
+ const isNearDoc = source === "SKILL.md" && globalIdx >= 0 && isNearDocumentationHeader(
1740
+ allLines.map((l) => l.line),
1741
+ globalIdx
1742
+ );
1743
+ if (isNearDoc) {
1744
+ severity = "LOW";
1745
+ reducedFrom = "HIGH";
1746
+ msgSuffix = " [reduced: in code block within documentation]";
1747
+ } else {
1748
+ const r = reduceSeverity(severity, "in code block");
1749
+ severity = r.severity;
1750
+ reducedFrom = r.reducedFrom;
1751
+ msgSuffix = ` ${r.annotation}`;
1752
+ }
1753
+ }
1754
+ results.push({
1755
+ id: "SUPPLY-003",
1756
+ category: "SUPPLY",
1757
+ severity,
1758
+ title: "Package installation command",
1759
+ message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.${msgSuffix}`,
1760
+ line: lineNum,
1761
+ snippet: line.trim().slice(0, 120),
1762
+ source,
1763
+ reducedFrom
1764
+ });
1765
+ }
1766
+ }
1767
+ if (GIT_CLONE_PATTERN.test(line)) {
1768
+ const allLines = getAllLines(skill);
1769
+ const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
1770
+ const isDoc = source === "SKILL.md" && globalIdx >= 0 && isInDocumentationContext(
1771
+ allLines.map((l) => l.line),
1772
+ globalIdx
1773
+ );
1774
+ if (!isDoc) {
1775
+ let severity = "MEDIUM";
1776
+ let reducedFrom;
1777
+ let msgSuffix = "";
1713
1778
  const srcLines = getLinesForSource(skill, source);
1714
1779
  const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
1715
1780
  if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
@@ -1719,11 +1784,11 @@ var supplyChainChecks = {
1719
1784
  msgSuffix = ` ${r.annotation}`;
1720
1785
  }
1721
1786
  results.push({
1722
- id: "SUPPLY-003",
1787
+ id: "SUPPLY-006",
1723
1788
  category: "SUPPLY",
1724
1789
  severity,
1725
- title: "Package installation command",
1726
- message: `${source}:${lineNum}: Installs packages. Verify package names are legitimate.${msgSuffix}`,
1790
+ title: "git clone command",
1791
+ message: `${source}:${lineNum}: Clones a git repository. Verify the source.${msgSuffix}`,
1727
1792
  line: lineNum,
1728
1793
  snippet: line.trim().slice(0, 120),
1729
1794
  source,
@@ -1731,18 +1796,6 @@ var supplyChainChecks = {
1731
1796
  });
1732
1797
  }
1733
1798
  }
1734
- if (GIT_CLONE_PATTERN.test(line)) {
1735
- results.push({
1736
- id: "SUPPLY-006",
1737
- category: "SUPPLY",
1738
- severity: "MEDIUM",
1739
- title: "git clone command",
1740
- message: `${source}:${lineNum}: Clones a git repository. Verify the source.`,
1741
- line: lineNum,
1742
- snippet: line.trim().slice(0, 120),
1743
- source
1744
- });
1745
- }
1746
1799
  const urls = line.match(URL_PATTERN) || [];
1747
1800
  for (const url of urls) {
1748
1801
  if (url.startsWith("http://")) {
@@ -2572,6 +2625,11 @@ program.command("scan").description("Scan a skill directory for security issues"
2572
2625
  process.exit(1);
2573
2626
  }
2574
2627
  const config = loadConfig(path, opts.config);
2628
+ if (!existsSync4(join5(path, "SKILL.md"))) {
2629
+ console.error(
2630
+ "Warning: No SKILL.md found in the specified directory. This tool is designed to scan skill directories. Results may contain noise. See: skill-checker scan --help"
2631
+ );
2632
+ }
2575
2633
  if (opts.policy) {
2576
2634
  config.policy = opts.policy;
2577
2635
  }