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 +12 -22
- package/dist/cli.js +96 -31
- package/dist/cli.js.map +1 -1
- package/dist/index.js +89 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
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
|
|
155
|
+
} else {
|
|
154
156
|
let fd;
|
|
155
157
|
try {
|
|
156
158
|
fd = openSync(fullPath, "r");
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
193
|
-
".exe",
|
|
194
|
-
".bat",
|
|
195
|
-
".cmd",
|
|
201
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
196
202
|
".sh",
|
|
197
203
|
".bash",
|
|
198
204
|
".ps1",
|
|
199
|
-
".
|
|
200
|
-
".
|
|
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) ||
|
|
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
|
-
|
|
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-
|
|
1787
|
+
id: "SUPPLY-006",
|
|
1716
1788
|
category: "SUPPLY",
|
|
1717
1789
|
severity,
|
|
1718
|
-
title: "
|
|
1719
|
-
message: `${source}:${lineNum}:
|
|
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
|
}
|