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 +12 -22
- package/dist/cli.js +82 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +75 -24
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
200
|
-
".exe",
|
|
201
|
-
".bat",
|
|
202
|
-
".cmd",
|
|
201
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
203
202
|
".sh",
|
|
204
203
|
".bash",
|
|
205
204
|
".ps1",
|
|
206
|
-
".
|
|
207
|
-
".
|
|
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) ||
|
|
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
|
-
|
|
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-
|
|
1787
|
+
id: "SUPPLY-006",
|
|
1723
1788
|
category: "SUPPLY",
|
|
1724
1789
|
severity,
|
|
1725
|
-
title: "
|
|
1726
|
-
message: `${source}:${lineNum}:
|
|
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
|
}
|