skill-checker 0.1.16 → 0.5.0

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
@@ -14,7 +14,7 @@ Security checker for Claude Code skills — detect injection, malicious code, an
14
14
 
15
15
  ## Security Standard & Benchmark
16
16
 
17
- Skill Checker's 57 rules are aligned with established security frameworks including OWASP Top 10 for LLM Applications (2025), MITRE CWE, and MITRE ATT&CK. The tool ships with a reproducible benchmark dataset of six fixture skills covering all rule categories. This alignment is an internal mapping exercise — Skill Checker does not claim third-party certification or external audit status.
17
+ Skill Checker's 57 rules are aligned with established security frameworks including OWASP Top 10 for LLM Applications (2025), MITRE CWE, and MITRE ATT&CK. The tool ships with a reproducible benchmark dataset of nine fixture skills covering all rule categories. This alignment is an internal mapping exercise — Skill Checker does not claim third-party certification or external audit status.
18
18
 
19
19
  See [docs/SECURITY_BENCHMARK.md](docs/SECURITY_BENCHMARK.md) for the full rule mapping matrix, benchmark methodology, scoring model, and known limitations.
20
20
 
@@ -42,6 +42,7 @@ skill-checker scan <path> [options]
42
42
  | `-f, --format <format>` | Output format: `terminal` (default), `json`, `hook` |
43
43
  | `-p, --policy <policy>` | Approval policy: `strict`, `balanced` (default), `permissive` |
44
44
  | `-c, --config <path>` | Path to config file |
45
+ | `--no-ignore` | Disable inline suppression comments |
45
46
 
46
47
  ```bash
47
48
  # Colored terminal report
@@ -149,6 +150,28 @@ ignore:
149
150
 
150
151
  Config is resolved in order: CLI `--config` flag → project directory (walks up) → home directory → defaults.
151
152
 
153
+ ### Inline Suppression
154
+
155
+ Suppress specific findings directly in SKILL.md using comments:
156
+
157
+ ```markdown
158
+ <!-- skill-checker-ignore CODE-002 -->
159
+ Run `soffice --convert-to pdf` to convert documents.
160
+
161
+ <!-- skill-checker-ignore-file CONT-001 -->
162
+
163
+ subprocess.run("soffice") // skill-checker-ignore CODE-002
164
+ ```
165
+
166
+ - `<!-- skill-checker-ignore RULE -->` suppresses the finding on the next line only
167
+ - `<!-- skill-checker-ignore-file RULE -->` suppresses the finding for the entire file
168
+ - `// skill-checker-ignore RULE` as a trailing comment suppresses the finding on the same line
169
+ - Multiple rules can be space-separated: `<!-- skill-checker-ignore CODE-002 CONT-001 -->`
170
+ - **INJ rules cannot be suppressed** — attempts produce a warning
171
+ - Directives only affect findings from the same source file (no cross-file suppression)
172
+ - Suppressed findings are excluded from scoring but remain visible in the report
173
+ - Use `--no-ignore` to disable all inline suppression
174
+
152
175
  ### Policy Matrix
153
176
 
154
177
  | Severity | strict | balanced | permissive |
package/dist/cli.js CHANGED
@@ -568,7 +568,8 @@ var contentChecks = {
568
568
  /\bplaceholder\b.*\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)/i.test(line) || /\b(TITLE|SUBTITLE|BODY|OBJECT|SLIDE|layout|slide|shape|pptx|presentation)\b.*\bplaceholder\b/i.test(line) || // API/code context: placeholder as a noun in technical documentation
569
569
  /\bplaceholder\s+(areas?|locations?|counts?|slots?|elements?|fields?)\b/i.test(line) || /\b(replace|replacing|replacement)\b.*\bplaceholder\b/i.test(line)
570
570
  );
571
- if (!inCodeBlk && !hasInlineCode && !isTechnicalRef) {
571
+ const isInstructional = /\b(?:create|generate|add|write|insert|produce|include|use)\b.*\bplaceholder\b/i.test(line) || /\bplaceholder\b.*\b(?:for|in|to|as|with)\s+(?:all|each|every|the|missing)\b/i.test(line);
572
+ if (!inCodeBlk && !hasInlineCode && !isTechnicalRef && !isInstructional) {
572
573
  for (const pattern of CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS) {
573
574
  if (pattern.test(line)) {
574
575
  matched = true;
@@ -576,6 +577,26 @@ var contentChecks = {
576
577
  }
577
578
  }
578
579
  }
580
+ if (!matched && isInstructional && !inCodeBlk && !hasInlineCode && !isTechnicalRef) {
581
+ for (const pattern of CONTEXT_SENSITIVE_PLACEHOLDER_PATTERNS) {
582
+ if (pattern.test(line)) {
583
+ const reduction = reduceSeverity("HIGH", "instructional placeholder context");
584
+ results.push({
585
+ id: "CONT-001",
586
+ category: "CONT",
587
+ severity: reduction.severity,
588
+ title: "Placeholder content detected",
589
+ message: `Line ${skill.bodyStartLine + i}: Contains placeholder text. ${reduction.annotation}`,
590
+ line: skill.bodyStartLine + i,
591
+ snippet: line.trim().slice(0, 120),
592
+ reducedFrom: reduction.reducedFrom,
593
+ source: "SKILL.md"
594
+ });
595
+ break;
596
+ }
597
+ }
598
+ continue;
599
+ }
579
600
  }
580
601
  if (matched) {
581
602
  results.push({
@@ -1052,7 +1073,8 @@ var injectionChecks = {
1052
1073
  title: "System prompt override attempt",
1053
1074
  message: `Line ${lineNum}: Attempts to override system instructions.`,
1054
1075
  line: lineNum,
1055
- snippet: line.trim().slice(0, 120)
1076
+ snippet: line.trim().slice(0, 120),
1077
+ source: "SKILL.md"
1056
1078
  });
1057
1079
  break;
1058
1080
  }
@@ -1066,7 +1088,8 @@ var injectionChecks = {
1066
1088
  title: "Tool output manipulation",
1067
1089
  message: `Line ${lineNum}: Attempts to manipulate tool results.`,
1068
1090
  line: lineNum,
1069
- snippet: line.trim().slice(0, 120)
1091
+ snippet: line.trim().slice(0, 120),
1092
+ source: "SKILL.md"
1070
1093
  });
1071
1094
  break;
1072
1095
  }
@@ -1080,7 +1103,8 @@ var injectionChecks = {
1080
1103
  title: "Tag injection detected",
1081
1104
  message: `Line ${lineNum}: Contains special model/system tags.`,
1082
1105
  line: lineNum,
1083
- snippet: line.trim().slice(0, 120)
1106
+ snippet: line.trim().slice(0, 120),
1107
+ source: "SKILL.md"
1084
1108
  });
1085
1109
  break;
1086
1110
  }
@@ -1094,7 +1118,8 @@ var injectionChecks = {
1094
1118
  title: "Delimiter confusion pattern",
1095
1119
  message: `Line ${lineNum}: Uses patterns that could confuse model context boundaries.`,
1096
1120
  line: lineNum,
1097
- snippet: line.trim().slice(0, 120)
1121
+ snippet: line.trim().slice(0, 120),
1122
+ source: "SKILL.md"
1098
1123
  });
1099
1124
  break;
1100
1125
  }
@@ -1111,7 +1136,8 @@ var injectionChecks = {
1111
1136
  title: "Social engineering: identity hijacking",
1112
1137
  message: `Line ${lineNum}: Attempts to hijack the model's identity.`,
1113
1138
  line: lineNum,
1114
- snippet: line.trim().slice(0, 120)
1139
+ snippet: line.trim().slice(0, 120),
1140
+ source: "SKILL.md"
1115
1141
  });
1116
1142
  break;
1117
1143
  }
@@ -1125,7 +1151,8 @@ var injectionChecks = {
1125
1151
  title: "Social engineering: deception/secrecy",
1126
1152
  message: `Line ${lineNum}: Instructs the model to hide actions from the user.`,
1127
1153
  line: lineNum,
1128
- snippet: line.trim().slice(0, 120)
1154
+ snippet: line.trim().slice(0, 120),
1155
+ source: "SKILL.md"
1129
1156
  });
1130
1157
  break;
1131
1158
  }
@@ -1139,7 +1166,8 @@ var injectionChecks = {
1139
1166
  title: "Social engineering: configuration tampering",
1140
1167
  message: `Line ${lineNum}: Attempts to tamper with model configuration or memory.`,
1141
1168
  line: lineNum,
1142
- snippet: line.trim().slice(0, 120)
1169
+ snippet: line.trim().slice(0, 120),
1170
+ source: "SKILL.md"
1143
1171
  });
1144
1172
  break;
1145
1173
  }
@@ -1153,7 +1181,8 @@ var injectionChecks = {
1153
1181
  title: "Social engineering: verification bypass",
1154
1182
  message: `Line ${lineNum}: Attempts to bypass verification or validation.`,
1155
1183
  line: lineNum,
1156
- snippet: line.trim().slice(0, 120)
1184
+ snippet: line.trim().slice(0, 120),
1185
+ source: "SKILL.md"
1157
1186
  });
1158
1187
  break;
1159
1188
  }
@@ -1172,7 +1201,8 @@ var injectionChecks = {
1172
1201
  title: "Hidden instructions in HTML comment",
1173
1202
  message: `Line ${lineNum}: HTML comment contains instruction-like content.`,
1174
1203
  line: lineNum,
1175
- snippet: commentBody.trim().slice(0, 120)
1204
+ snippet: commentBody.trim().slice(0, 120),
1205
+ source: "SKILL.md"
1176
1206
  });
1177
1207
  }
1178
1208
  }
@@ -1191,7 +1221,8 @@ var injectionChecks = {
1191
1221
  title: "Encoded instructions detected",
1192
1222
  message: `Line ${lineNum}: Base64 string decodes to instruction-like content.`,
1193
1223
  line: lineNum,
1194
- snippet: decoded.slice(0, 120)
1224
+ snippet: decoded.slice(0, 120),
1225
+ source: "SKILL.md"
1195
1226
  });
1196
1227
  }
1197
1228
  }
@@ -2326,7 +2357,8 @@ var resourceChecks = {
2326
2357
  title: "Instruction amplification",
2327
2358
  message: `Line ${lineNum}: Contains recursive/repetitive task pattern.`,
2328
2359
  line: lineNum,
2329
- snippet: line.trim().slice(0, 120)
2360
+ snippet: line.trim().slice(0, 120),
2361
+ source: "SKILL.md"
2330
2362
  });
2331
2363
  break;
2332
2364
  }
@@ -2340,7 +2372,8 @@ var resourceChecks = {
2340
2372
  title: "Unrestricted tool access requested",
2341
2373
  message: `Line ${lineNum}: Requests broad/unrestricted tool access.`,
2342
2374
  line: lineNum,
2343
- snippet: line.trim().slice(0, 120)
2375
+ snippet: line.trim().slice(0, 120),
2376
+ source: "SKILL.md"
2344
2377
  });
2345
2378
  break;
2346
2379
  }
@@ -2354,7 +2387,8 @@ var resourceChecks = {
2354
2387
  title: "Attempts to disable safety checks",
2355
2388
  message: `Line ${lineNum}: Instructs disabling of safety mechanisms.`,
2356
2389
  line: lineNum,
2357
- snippet: line.trim().slice(0, 120)
2390
+ snippet: line.trim().slice(0, 120),
2391
+ source: "SKILL.md"
2358
2392
  });
2359
2393
  break;
2360
2394
  }
@@ -2368,7 +2402,8 @@ var resourceChecks = {
2368
2402
  title: "Token waste pattern",
2369
2403
  message: `Line ${lineNum}: Contains instructions that waste tokens.`,
2370
2404
  line: lineNum,
2371
- snippet: line.trim().slice(0, 120)
2405
+ snippet: line.trim().slice(0, 120),
2406
+ source: "SKILL.md"
2372
2407
  });
2373
2408
  break;
2374
2409
  }
@@ -2382,7 +2417,8 @@ var resourceChecks = {
2382
2417
  title: "Attempts to ignore project rules",
2383
2418
  message: `Line ${lineNum}: Instructs ignoring CLAUDE.md or project configuration.`,
2384
2419
  line: lineNum,
2385
- snippet: line.trim().slice(0, 120)
2420
+ snippet: line.trim().slice(0, 120),
2421
+ source: "SKILL.md"
2386
2422
  });
2387
2423
  break;
2388
2424
  }
@@ -2648,6 +2684,344 @@ function runAllChecks(skill) {
2648
2684
  return results;
2649
2685
  }
2650
2686
 
2687
+ // src/suppression.ts
2688
+ var RULE_ID_RE = /^[A-Z]+-\d{3}$/;
2689
+ var NEXT_LINE_RE = /^<!--\s*skill-checker-ignore\s+([\w-]+(?:\s+[\w-]+)*)\s*-->$/;
2690
+ var FILE_LEVEL_RE = /^<!--\s*skill-checker-ignore-file\s+([\w-]+(?:\s+[\w-]+)*)\s*-->$/;
2691
+ var INLINE_COMMENT_RE = /\/\/\s*skill-checker-ignore\s+([\w-]+(?:\s+[\w-]+)*)\s*$/;
2692
+ function parseSuppressionDirectives(rawLines, source = "SKILL.md") {
2693
+ const directives = [];
2694
+ for (let i = 0; i < rawLines.length; i++) {
2695
+ const trimmed = rawLines[i].trim();
2696
+ const lineNum = i + 1;
2697
+ const nextMatch = NEXT_LINE_RE.exec(trimmed);
2698
+ if (nextMatch) {
2699
+ const ruleIds = nextMatch[1].split(/\s+/);
2700
+ directives.push({ ruleIds, scope: "next-line", line: lineNum, source });
2701
+ continue;
2702
+ }
2703
+ const fileMatch = FILE_LEVEL_RE.exec(trimmed);
2704
+ if (fileMatch) {
2705
+ const ruleIds = fileMatch[1].split(/\s+/);
2706
+ directives.push({ ruleIds, scope: "file", line: lineNum, source });
2707
+ continue;
2708
+ }
2709
+ const inlineMatch = INLINE_COMMENT_RE.exec(rawLines[i]);
2710
+ if (inlineMatch) {
2711
+ const ruleIds = inlineMatch[1].split(/\s+/);
2712
+ directives.push({ ruleIds, scope: "same-line", line: lineNum, source });
2713
+ }
2714
+ }
2715
+ return directives;
2716
+ }
2717
+ function isInjRule(ruleId) {
2718
+ return ruleId.startsWith("INJ-");
2719
+ }
2720
+ function applySuppressions(results, directives, bodyStartLine) {
2721
+ const warnings = [];
2722
+ const usedDirectiveRules = /* @__PURE__ */ new Set();
2723
+ for (const d of directives) {
2724
+ for (const ruleId of d.ruleIds) {
2725
+ if (!RULE_ID_RE.test(ruleId)) {
2726
+ warnings.push(`Invalid suppression: ${ruleId} at line ${d.line}`);
2727
+ } else if (isInjRule(ruleId)) {
2728
+ warnings.push(`Cannot suppress INJ rule: ${ruleId} at line ${d.line} (security policy)`);
2729
+ }
2730
+ }
2731
+ }
2732
+ const active = [];
2733
+ const suppressed = [];
2734
+ for (const result of results) {
2735
+ let isSuppressed = false;
2736
+ if (!isInjRule(result.id)) {
2737
+ for (const d of directives) {
2738
+ if (!d.ruleIds.includes(result.id)) continue;
2739
+ if (!RULE_ID_RE.test(result.id)) continue;
2740
+ if (result.source !== void 0 && result.source !== d.source) continue;
2741
+ if (result.source === void 0 && d.source !== "SKILL.md") continue;
2742
+ if (d.scope === "next-line") {
2743
+ const rawTarget = d.line + 1;
2744
+ const bodyTarget = bodyStartLine !== void 0 ? rawTarget - bodyStartLine + 1 : void 0;
2745
+ if (result.line !== void 0 && (result.line === rawTarget || bodyTarget !== void 0 && result.line === bodyTarget)) {
2746
+ isSuppressed = true;
2747
+ usedDirectiveRules.add(`${d.line}:${result.id}`);
2748
+ break;
2749
+ }
2750
+ } else if (d.scope === "same-line") {
2751
+ const rawTarget = d.line;
2752
+ const bodyTarget = bodyStartLine !== void 0 ? rawTarget - bodyStartLine + 1 : void 0;
2753
+ if (result.line !== void 0 && (result.line === rawTarget || bodyTarget !== void 0 && result.line === bodyTarget)) {
2754
+ isSuppressed = true;
2755
+ usedDirectiveRules.add(`${d.line}:${result.id}`);
2756
+ break;
2757
+ }
2758
+ } else if (d.scope === "file") {
2759
+ isSuppressed = true;
2760
+ usedDirectiveRules.add(`${d.line}:${result.id}`);
2761
+ break;
2762
+ }
2763
+ }
2764
+ }
2765
+ if (isSuppressed) {
2766
+ suppressed.push({ ...result, suppressed: true });
2767
+ } else {
2768
+ active.push(result);
2769
+ }
2770
+ }
2771
+ for (const d of directives) {
2772
+ for (const ruleId of d.ruleIds) {
2773
+ if (!RULE_ID_RE.test(ruleId)) continue;
2774
+ if (isInjRule(ruleId)) continue;
2775
+ const key = `${d.line}:${ruleId}`;
2776
+ if (!usedDirectiveRules.has(key)) {
2777
+ warnings.push(`Unused suppression: ${ruleId} at line ${d.line}`);
2778
+ }
2779
+ }
2780
+ }
2781
+ return { active, suppressed, warnings };
2782
+ }
2783
+
2784
+ // src/remediation.ts
2785
+ var REMEDIATION_MAP = {
2786
+ // ===== STRUCT =====
2787
+ "STRUCT-001": {
2788
+ guidance: "Create a SKILL.md file in the skill directory with valid YAML frontmatter and a body section.",
2789
+ effort: "low"
2790
+ },
2791
+ "STRUCT-002": {
2792
+ guidance: "Fix the YAML frontmatter syntax. Ensure it is enclosed between --- delimiters and contains valid YAML.",
2793
+ effort: "low"
2794
+ },
2795
+ "STRUCT-003": {
2796
+ guidance: "Add a `name` field to the frontmatter, e.g. `name: my-skill`.",
2797
+ effort: "low"
2798
+ },
2799
+ "STRUCT-004": {
2800
+ guidance: "Add a `description` field to the frontmatter explaining what the skill does.",
2801
+ effort: "low"
2802
+ },
2803
+ "STRUCT-005": {
2804
+ guidance: "Expand the body section with meaningful instructions. A skill body should describe the workflow and expected behavior.",
2805
+ effort: "medium"
2806
+ },
2807
+ "STRUCT-006": {
2808
+ guidance: "Remove binary, executable, or script files from the skill directory. Skills should contain only text-based configuration and documentation.",
2809
+ effort: "low"
2810
+ },
2811
+ "STRUCT-007": {
2812
+ guidance: "Use lowercase kebab-case for the skill name (e.g. `my-skill-name`). Avoid special characters or spaces.",
2813
+ effort: "low"
2814
+ },
2815
+ "STRUCT-008": {
2816
+ guidance: "Ensure the skill directory structure is standard. This warning indicates some files could not be scanned.",
2817
+ effort: "low"
2818
+ },
2819
+ // ===== CONT =====
2820
+ "CONT-001": {
2821
+ guidance: "Replace TODO/FIXME/placeholder text with actual content. If the skill instructs creating placeholders, use `<!-- skill-checker-ignore CONT-001 -->` before the line.",
2822
+ effort: "low"
2823
+ },
2824
+ "CONT-002": {
2825
+ guidance: "Remove lorem ipsum filler text and replace with real skill instructions.",
2826
+ effort: "low"
2827
+ },
2828
+ "CONT-003": {
2829
+ guidance: "Reduce duplicated lines. Consolidate repeated instructions into a single clear statement.",
2830
+ effort: "medium"
2831
+ },
2832
+ "CONT-004": {
2833
+ guidance: "Align the frontmatter description with the body content. The description should accurately summarize what the skill does.",
2834
+ effort: "medium"
2835
+ },
2836
+ "CONT-005": {
2837
+ guidance: "Remove promotional or advertising content. If describing pricing/marketing concepts for educational purposes, use `<!-- skill-checker-ignore CONT-005 -->` before the line.",
2838
+ effort: "low"
2839
+ },
2840
+ "CONT-006": {
2841
+ guidance: "Add more instructional prose around code examples. A skill should explain when and how to use the code, not just provide raw snippets.",
2842
+ effort: "medium"
2843
+ },
2844
+ "CONT-007": {
2845
+ guidance: "Ensure the skill body references the capabilities implied by the skill name, or rename the skill to match its actual functionality.",
2846
+ effort: "medium"
2847
+ },
2848
+ // ===== INJ =====
2849
+ "INJ-001": {
2850
+ guidance: 'Remove zero-width Unicode characters from the file. These can hide malicious content. Use a hex editor or run: `sed -i "" "s/[\\x200B\\x200C\\x200D\\xFEFF]//g" SKILL.md`.',
2851
+ effort: "low"
2852
+ },
2853
+ "INJ-002": {
2854
+ guidance: "Replace homoglyph characters (e.g. Cyrillic/Greek lookalikes) with their ASCII equivalents to prevent spoofing.",
2855
+ effort: "low"
2856
+ },
2857
+ "INJ-003": {
2858
+ guidance: "Remove RTL/bidirectional override characters. These manipulate text display and can disguise malicious content.",
2859
+ effort: "low"
2860
+ },
2861
+ "INJ-004": {
2862
+ guidance: 'Remove instructions that attempt to override the system prompt. A skill must not contain "ignore previous instructions" or similar patterns.',
2863
+ effort: "low"
2864
+ },
2865
+ "INJ-005": {
2866
+ guidance: "Remove patterns that simulate or manipulate tool output (e.g. `permissionDecision`, `tool_result`). Skills must not forge tool responses.",
2867
+ effort: "low"
2868
+ },
2869
+ "INJ-006": {
2870
+ guidance: "Remove instruction-like content from HTML comments. If comments are needed for documentation, ensure they do not contain executable directives.",
2871
+ effort: "low"
2872
+ },
2873
+ "INJ-007": {
2874
+ guidance: "Remove special model/system tags (e.g. `<|im_start|>`, `<system>`). These attempt to inject into the model context boundary.",
2875
+ effort: "low"
2876
+ },
2877
+ "INJ-008": {
2878
+ guidance: "Remove base64-encoded strings that decode to instructions. Legitimate data should be stored in plain text or referenced via file paths.",
2879
+ effort: "low"
2880
+ },
2881
+ "INJ-009": {
2882
+ guidance: "Avoid delimiter patterns that mimic system/instruction boundaries (e.g. `[SYSTEM]`, `[INST]`). Use standard Markdown headers instead.",
2883
+ effort: "low"
2884
+ },
2885
+ "INJ-010": {
2886
+ guidance: "Remove social engineering patterns (identity hijacking, deception, config tampering, verification bypass). Skills must not attempt to alter model identity or hide actions from users.",
2887
+ effort: "low"
2888
+ },
2889
+ // ===== CODE =====
2890
+ "CODE-001": {
2891
+ guidance: "Remove eval(), Function(), or dynamic code execution. Use explicit function calls or tool APIs instead.",
2892
+ effort: "medium"
2893
+ },
2894
+ "CODE-002": {
2895
+ guidance: "Remove shell/subprocess calls. Use tool APIs instead. If shell access is essential (e.g. calling soffice), use `<!-- skill-checker-ignore CODE-002 -->` and explain the necessity.",
2896
+ effort: "medium"
2897
+ },
2898
+ "CODE-003": {
2899
+ guidance: "Remove destructive file operations (rm -rf, rmdir, unlink on broad paths). If file cleanup is needed, target specific files with explicit paths.",
2900
+ effort: "medium"
2901
+ },
2902
+ "CODE-004": {
2903
+ guidance: "Review hardcoded URLs. Ensure all external URLs use HTTPS and point to trusted domains. If URLs are necessary API endpoints, use `<!-- skill-checker-ignore CODE-004 -->`.",
2904
+ effort: "medium"
2905
+ },
2906
+ "CODE-005": {
2907
+ guidance: "Restrict file writes to the skill working directory. Avoid writing to system paths, home directories, or parent directories.",
2908
+ effort: "medium"
2909
+ },
2910
+ "CODE-006": {
2911
+ guidance: "Avoid direct environment variable access for sensitive data. If env vars are needed for configuration, document which variables and why.",
2912
+ effort: "medium"
2913
+ },
2914
+ "CODE-007": {
2915
+ guidance: "Review long encoded strings. If they contain legitimate data (e.g. embedded assets), add a comment explaining their purpose.",
2916
+ effort: "low"
2917
+ },
2918
+ "CODE-008": {
2919
+ guidance: "Review high-entropy strings. These may be embedded secrets or obfuscated code. Replace with references to external configuration.",
2920
+ effort: "medium"
2921
+ },
2922
+ "CODE-009": {
2923
+ guidance: "Remove multi-layer encoding (e.g. base64 within hex). Legitimate content should use a single encoding layer at most.",
2924
+ effort: "low"
2925
+ },
2926
+ "CODE-010": {
2927
+ guidance: "Remove dynamic code generation patterns. Use static code with explicit function calls instead of constructing code at runtime.",
2928
+ effort: "medium"
2929
+ },
2930
+ "CODE-011": {
2931
+ guidance: "Use descriptive variable names instead of obfuscated single-character or hex-named variables.",
2932
+ effort: "low"
2933
+ },
2934
+ "CODE-012": {
2935
+ guidance: "Remove permission escalation commands (sudo, chmod 777, setuid). Skills should operate within normal user permissions.",
2936
+ effort: "low"
2937
+ },
2938
+ "CODE-013": {
2939
+ guidance: "Remove hardcoded API keys, tokens, or credentials. Use environment variables or a secrets manager, and add patterns to .gitignore.",
2940
+ effort: "medium"
2941
+ },
2942
+ "CODE-014": {
2943
+ guidance: "Remove reverse shell patterns. These are indicators of malicious intent and have no legitimate use in skills.",
2944
+ effort: "low"
2945
+ },
2946
+ "CODE-015": {
2947
+ guidance: "Remove remote pipeline execution or data exfiltration patterns (e.g. curl | sh, piping sensitive data to external endpoints).",
2948
+ effort: "low"
2949
+ },
2950
+ "CODE-016": {
2951
+ guidance: "Remove persistence mechanism patterns (cron jobs, launchd plists, shell profile modifications, autostart entries). Skills should not install persistent system services.",
2952
+ effort: "medium"
2953
+ },
2954
+ // ===== SUPPLY =====
2955
+ "SUPPLY-001": {
2956
+ guidance: "Review the MCP server reference. Ensure it points to a trusted, well-known server. Document why external MCP access is needed.",
2957
+ effort: "medium"
2958
+ },
2959
+ "SUPPLY-002": {
2960
+ guidance: "Avoid `npx -y` auto-install. Pin the package version and verify the package before use. Use `npx package@version` instead.",
2961
+ effort: "low"
2962
+ },
2963
+ "SUPPLY-003": {
2964
+ guidance: "Review package installation commands. Pin exact versions and verify package authenticity. Avoid installing packages from untrusted sources.",
2965
+ effort: "medium"
2966
+ },
2967
+ "SUPPLY-004": {
2968
+ guidance: "Use HTTPS instead of HTTP for all URLs to prevent man-in-the-middle attacks.",
2969
+ effort: "low"
2970
+ },
2971
+ "SUPPLY-005": {
2972
+ guidance: "Replace raw IP addresses with domain names. IP-based URLs bypass DNS security controls and are harder to audit.",
2973
+ effort: "low"
2974
+ },
2975
+ "SUPPLY-006": {
2976
+ guidance: "Review git clone commands. Ensure repositories are from trusted sources. Pin to a specific commit hash or tag for reproducibility.",
2977
+ effort: "medium"
2978
+ },
2979
+ "SUPPLY-007": {
2980
+ guidance: "Remove references to known suspicious domains. These domains have been flagged for malicious activity.",
2981
+ effort: "low"
2982
+ },
2983
+ "SUPPLY-008": {
2984
+ guidance: "Remove files matching known malicious hashes. These files have been identified as malware or exploits.",
2985
+ effort: "low"
2986
+ },
2987
+ "SUPPLY-009": {
2988
+ guidance: "Remove references to known command-and-control IP addresses. These IPs are associated with malware infrastructure.",
2989
+ effort: "low"
2990
+ },
2991
+ "SUPPLY-010": {
2992
+ guidance: "Verify the package name is correct and not a typosquat of a popular package. Check the official registry for the canonical name.",
2993
+ effort: "low"
2994
+ },
2995
+ // ===== RES =====
2996
+ "RES-001": {
2997
+ guidance: 'Remove recursive or repetitive task patterns (e.g. "repeat 1000 times"). Use bounded iteration with a clear termination condition.',
2998
+ effort: "low"
2999
+ },
3000
+ "RES-002": {
3001
+ guidance: "Request only the specific tools needed instead of unrestricted access. List individual tools in allowed-tools.",
3002
+ effort: "medium"
3003
+ },
3004
+ "RES-003": {
3005
+ guidance: "Reduce the number of allowed tools. Review if all listed tools are actually needed. Consider splitting into multiple focused skills.",
3006
+ effort: "medium"
3007
+ },
3008
+ "RES-004": {
3009
+ guidance: "Remove instructions that disable safety checks, hooks, or verification. Skills must operate within the security framework.",
3010
+ effort: "low"
3011
+ },
3012
+ "RES-005": {
3013
+ guidance: 'Remove token-wasting patterns (e.g. "repeat in every response"). Keep instructions concise and avoid mandating boilerplate in every output.',
3014
+ effort: "low"
3015
+ },
3016
+ "RES-006": {
3017
+ guidance: "Remove instructions to ignore CLAUDE.md or project rules. Skills must respect the project configuration set by the user.",
3018
+ effort: "low"
3019
+ }
3020
+ };
3021
+ function getRemediation(ruleId) {
3022
+ return REMEDIATION_MAP[ruleId];
3023
+ }
3024
+
2651
3025
  // src/scanner.ts
2652
3026
  function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
2653
3027
  const skill = parseSkill(dirPath);
@@ -2655,6 +3029,15 @@ function scanSkillDirectory(dirPath, config = DEFAULT_CONFIG) {
2655
3029
  }
2656
3030
  function buildReport(skill, config) {
2657
3031
  let results = runAllChecks(skill);
3032
+ let suppressedResults = [];
3033
+ let suppressionWarnings = [];
3034
+ if (!config.noIgnoreInline) {
3035
+ const directives = parseSuppressionDirectives(skill.raw.split("\n"));
3036
+ const sr = applySuppressions(results, directives, skill.bodyStartLine);
3037
+ results = sr.active;
3038
+ suppressedResults = sr.suppressed;
3039
+ suppressionWarnings = sr.warnings;
3040
+ }
2658
3041
  results = results.map((r) => {
2659
3042
  if (config.overrides[r.id]) {
2660
3043
  return { ...r, severity: config.overrides[r.id] };
@@ -2663,6 +3046,10 @@ function buildReport(skill, config) {
2663
3046
  });
2664
3047
  results = results.filter((r) => !config.ignore.includes(r.id));
2665
3048
  results = deduplicateResults(results);
3049
+ results = results.map((r) => {
3050
+ const rem = getRemediation(r.id);
3051
+ return rem ? { ...r, remediation: rem.guidance } : r;
3052
+ });
2666
3053
  const score = calculateScore(results);
2667
3054
  const grade = computeGrade(score);
2668
3055
  const summary = {
@@ -2679,7 +3066,9 @@ function buildReport(skill, config) {
2679
3066
  results,
2680
3067
  score,
2681
3068
  grade,
2682
- summary
3069
+ summary,
3070
+ suppressedResults,
3071
+ suppressionWarnings
2683
3072
  };
2684
3073
  }
2685
3074
  function deduplicateResults(results) {
@@ -2692,7 +3081,9 @@ function deduplicateResults(results) {
2692
3081
  };
2693
3082
  for (const r of results) {
2694
3083
  const sourceKey = r.source ?? `_no_source_:${r.category}:${r.line ?? ""}`;
2695
- const key = `${r.id}::${r.title}::${sourceKey}`;
3084
+ const isSecurityLineLevel = r.source === "SKILL.md" && r.line !== void 0 && (r.category === "INJ" || r.category === "RES");
3085
+ const lineKey = isSecurityLineLevel ? `:${r.line}` : "";
3086
+ const key = `${r.id}::${r.title}::${sourceKey}${lineKey}`;
2696
3087
  const group = groups.get(key);
2697
3088
  if (group) {
2698
3089
  group.push(r);
@@ -2803,10 +3194,29 @@ function formatTerminalReport(report) {
2803
3194
  if (f.snippet) {
2804
3195
  lines.push(` ${chalk.dim(f.snippet)}`);
2805
3196
  }
3197
+ if (f.remediation) {
3198
+ lines.push(` ${chalk.dim("Fix: " + f.remediation)}`);
3199
+ }
2806
3200
  }
2807
3201
  lines.push("");
2808
3202
  }
2809
3203
  }
3204
+ if (report.suppressedResults && report.suppressedResults.length > 0) {
3205
+ const counts = /* @__PURE__ */ new Map();
3206
+ for (const s of report.suppressedResults) {
3207
+ counts.set(s.id, (counts.get(s.id) ?? 0) + 1);
3208
+ }
3209
+ const detail = Array.from(counts.entries()).map(([id, n]) => `${id}: ${n}`).join(", ");
3210
+ lines.push(chalk.dim(`Suppressed: ${report.suppressedResults.length} finding(s) (${detail})`));
3211
+ lines.push("");
3212
+ }
3213
+ if (report.suppressionWarnings && report.suppressionWarnings.length > 0) {
3214
+ lines.push(chalk.yellow("Suppression warnings:"));
3215
+ for (const w of report.suppressionWarnings) {
3216
+ lines.push(chalk.yellow(` - ${w}`));
3217
+ }
3218
+ lines.push("");
3219
+ }
2810
3220
  lines.push(chalk.bold("Recommendation:"));
2811
3221
  switch (report.grade) {
2812
3222
  case "A":
@@ -2841,10 +3251,15 @@ function formatJsonReport(report) {
2841
3251
  }
2842
3252
  function generateHookResponse(report, config = DEFAULT_CONFIG) {
2843
3253
  const worst = worstSeverity(report.results);
2844
- if (!worst) {
3254
+ const suppressedWorst = report.suppressedResults ? worstSeverity(report.suppressedResults) : null;
3255
+ const hasSuppressedCriticalOrHigh = suppressedWorst === "CRITICAL" || suppressedWorst === "HIGH";
3256
+ if (!worst && !hasSuppressedCriticalOrHigh) {
2845
3257
  return { permissionDecision: "allow" };
2846
3258
  }
2847
- const action = getHookAction(config.policy, worst);
3259
+ let action = worst ? getHookAction(config.policy, worst) : "report";
3260
+ if (hasSuppressedCriticalOrHigh && (action === "report" || !worst)) {
3261
+ action = "ask";
3262
+ }
2848
3263
  switch (action) {
2849
3264
  case "deny":
2850
3265
  return {
@@ -2988,7 +3403,7 @@ program.name("skill-checker").description(
2988
3403
  "Security checker for Claude Code skills - detect injection, malicious code, and supply chain risks"
2989
3404
  ).version(pkg.version);
2990
3405
  var VALID_POLICIES = ["strict", "balanced", "permissive"];
2991
- program.command("scan").description("Scan a skill directory for security issues").argument("<path>", "Path to the skill directory").option("-f, --format <format>", "Output format: terminal, json, hook", "terminal").option("-p, --policy <policy>", "Policy: strict, balanced, permissive").option("-c, --config <path>", "Path to config file").action(
3406
+ program.command("scan").description("Scan a skill directory for security issues").argument("<path>", "Path to the skill directory").option("-f, --format <format>", "Output format: terminal, json, hook", "terminal").option("-p, --policy <policy>", "Policy: strict, balanced, permissive").option("-c, --config <path>", "Path to config file").option("--no-ignore", "Disable inline suppression comments").action(
2992
3407
  (path, opts) => {
2993
3408
  if (opts.policy && !VALID_POLICIES.includes(opts.policy)) {
2994
3409
  console.error(`Error: invalid policy "${opts.policy}". Valid values: ${VALID_POLICIES.join(", ")}`);
@@ -3003,6 +3418,9 @@ program.command("scan").description("Scan a skill directory for security issues"
3003
3418
  if (opts.policy) {
3004
3419
  config.policy = opts.policy;
3005
3420
  }
3421
+ if (!opts.ignore) {
3422
+ config.noIgnoreInline = true;
3423
+ }
3006
3424
  const report = scanSkillDirectory(path, config);
3007
3425
  switch (opts.format) {
3008
3426
  case "json":