skill-checker 0.1.6 → 0.1.8

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
@@ -150,14 +152,23 @@ function enumerateFiles(dirPath, warnings) {
150
152
  content = readFileSync(fullPath, "utf-8");
151
153
  } catch {
152
154
  }
153
- } else if (lstats.size <= 5e7) {
155
+ } else {
154
156
  let fd;
155
157
  try {
156
158
  fd = openSync(fullPath, "r");
157
- const buf = Buffer.alloc(PARTIAL_READ_LIMIT);
158
- const bytesRead = readSync(fd, buf, 0, PARTIAL_READ_LIMIT, 0);
159
- content = buf.slice(0, bytesRead).toString("utf-8");
160
- warnings.push(`Large file partially scanned (first ${PARTIAL_READ_LIMIT} bytes): ${relativePath} (${lstats.size} bytes total)`);
159
+ const headBuf = Buffer.alloc(PARTIAL_READ_LIMIT);
160
+ const headBytesRead = readSync(fd, headBuf, 0, PARTIAL_READ_LIMIT, 0);
161
+ const headContent = headBuf.slice(0, headBytesRead).toString("utf-8");
162
+ const tailOffset = Math.max(0, lstats.size - PARTIAL_READ_LIMIT);
163
+ const tailBuf = Buffer.alloc(PARTIAL_READ_LIMIT);
164
+ const tailBytesRead = readSync(fd, tailBuf, 0, PARTIAL_READ_LIMIT, tailOffset);
165
+ const tailContent = tailBuf.slice(0, tailBytesRead).toString("utf-8");
166
+ content = tailOffset > 0 ? `${headContent}
167
+ /* ... window gap ... */
168
+ ${tailContent}` : headContent;
169
+ warnings.push(
170
+ `Large file window-scanned (head+tail ${PARTIAL_READ_LIMIT} bytes each): ${relativePath} (${lstats.size} bytes total)`
171
+ );
161
172
  } catch {
162
173
  warnings.push(`Large file could not be read: ${relativePath} (${lstats.size} bytes)`);
163
174
  } finally {
@@ -168,8 +179,6 @@ function enumerateFiles(dirPath, warnings) {
168
179
  }
169
180
  }
170
181
  }
171
- } else {
172
- warnings.push(`File too large to scan: ${relativePath} (${lstats.size} bytes). Content not checked.`);
173
182
  }
174
183
  }
175
184
  files.push({
@@ -189,15 +198,12 @@ function enumerateFiles(dirPath, warnings) {
189
198
  // src/checks/structural.ts
190
199
  var HYPHEN_CASE_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
191
200
  var MAX_NAME_LENGTH = 64;
192
- var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
193
- ".exe",
194
- ".bat",
195
- ".cmd",
201
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
196
202
  ".sh",
197
203
  ".bash",
198
204
  ".ps1",
199
- ".com",
200
- ".msi"
205
+ ".bat",
206
+ ".cmd"
201
207
  ]);
202
208
  var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
203
209
  ".exe",
@@ -209,6 +215,10 @@ var BINARY_EXTENSIONS2 = /* @__PURE__ */ new Set([
209
215
  ".class",
210
216
  ".pyc"
211
217
  ]);
218
+ var INSTALLER_EXTENSIONS = /* @__PURE__ */ new Set([
219
+ ".com",
220
+ ".msi"
221
+ ]);
212
222
  var structuralChecks = {
213
223
  name: "Structural Validity",
214
224
  category: "STRUCT",
@@ -262,7 +272,7 @@ var structuralChecks = {
262
272
  }
263
273
  for (const file of skill.files) {
264
274
  const ext = file.extension.toLowerCase();
265
- if (BINARY_EXTENSIONS2.has(ext) || EXECUTABLE_EXTENSIONS.has(ext)) {
275
+ if (BINARY_EXTENSIONS2.has(ext) || INSTALLER_EXTENSIONS.has(ext)) {
266
276
  results.push({
267
277
  id: "STRUCT-006",
268
278
  category: "STRUCT",
@@ -271,6 +281,15 @@ var structuralChecks = {
271
281
  message: `Found unexpected file: ${file.path} (${ext})`,
272
282
  source: file.path
273
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
+ });
274
293
  }
275
294
  }
276
295
  const name = skill.frontmatter.name;
@@ -355,6 +374,15 @@ function isInDocumentationContext(lines, lineIndex) {
355
374
  }
356
375
  return false;
357
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
+ }
358
386
  function isLicenseFile(filePath) {
359
387
  const name = filePath.split("/").pop()?.toUpperCase() ?? "";
360
388
  const base = name.replace(/\.[^.]+$/, "");
@@ -1695,14 +1723,58 @@ var supplyChainChecks = {
1695
1723
  if (NPM_INSTALL_PATTERN.test(line) || PIP_INSTALL_PATTERN.test(line)) {
1696
1724
  const allLines = getAllLines(skill);
1697
1725
  const globalIdx = findGlobalLineIndex(allLines, source, lineNum);
1698
- const isDoc = globalIdx >= 0 && isInDocumentationContext(
1726
+ const isDoc = source === "SKILL.md" && globalIdx >= 0 && isInDocumentationContext(
1699
1727
  allLines.map((l) => l.line),
1700
1728
  globalIdx
1701
1729
  );
1702
- 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 {
1703
1735
  let severity = "HIGH";
1704
1736
  let reducedFrom;
1705
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 = "";
1706
1778
  const srcLines = getLinesForSource(skill, source);
1707
1779
  const localIdx = getLocalIndex(source, lineNum, skill.bodyStartLine);
1708
1780
  if (localIdx >= 0 && isInCodeBlock(srcLines, localIdx)) {
@@ -1712,11 +1784,11 @@ var supplyChainChecks = {
1712
1784
  msgSuffix = ` ${r.annotation}`;
1713
1785
  }
1714
1786
  results.push({
1715
- id: "SUPPLY-003",
1787
+ id: "SUPPLY-006",
1716
1788
  category: "SUPPLY",
1717
1789
  severity,
1718
- title: "Package installation command",
1719
- 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}`,
1720
1792
  line: lineNum,
1721
1793
  snippet: line.trim().slice(0, 120),
1722
1794
  source,
@@ -1724,18 +1796,6 @@ var supplyChainChecks = {
1724
1796
  });
1725
1797
  }
1726
1798
  }
1727
- if (GIT_CLONE_PATTERN.test(line)) {
1728
- results.push({
1729
- id: "SUPPLY-006",
1730
- category: "SUPPLY",
1731
- severity: "MEDIUM",
1732
- title: "git clone command",
1733
- message: `${source}:${lineNum}: Clones a git repository. Verify the source.`,
1734
- line: lineNum,
1735
- snippet: line.trim().slice(0, 120),
1736
- source
1737
- });
1738
- }
1739
1799
  const urls = line.match(URL_PATTERN) || [];
1740
1800
  for (const url of urls) {
1741
1801
  if (url.startsWith("http://")) {
@@ -2565,6 +2625,11 @@ program.command("scan").description("Scan a skill directory for security issues"
2565
2625
  process.exit(1);
2566
2626
  }
2567
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
+ }
2568
2633
  if (opts.policy) {
2569
2634
  config.policy = opts.policy;
2570
2635
  }