@hungpg/skill-audit 0.1.1 → 0.3.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.
@@ -0,0 +1,67 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
5
+ const RULES_DIR = join(PACKAGE_ROOT, "rules");
6
+ const DEFAULT_PATTERNS_FILE = join(RULES_DIR, "default-patterns.json");
7
+ /**
8
+ * Load patterns from JSON file
9
+ */
10
+ export function loadPatterns(patternsFile = DEFAULT_PATTERNS_FILE) {
11
+ if (!existsSync(patternsFile)) {
12
+ throw new Error(`Patterns file not found: ${patternsFile}`);
13
+ }
14
+ const content = readFileSync(patternsFile, "utf-8");
15
+ return JSON.parse(content);
16
+ }
17
+ /**
18
+ * Compile patterns to RegExp objects
19
+ */
20
+ export function compilePatterns(patterns) {
21
+ const compiled = new Map();
22
+ for (const [categoryKey, category] of Object.entries(patterns.categories)) {
23
+ const categoryPatterns = [];
24
+ for (const rule of category.patterns) {
25
+ try {
26
+ const regex = new RegExp(rule.pattern, rule.flags || "i");
27
+ categoryPatterns.push({
28
+ regex,
29
+ id: rule.id,
30
+ severity: rule.severity,
31
+ message: rule.message,
32
+ category: categoryKey
33
+ });
34
+ }
35
+ catch (error) {
36
+ console.error(`Failed to compile pattern ${rule.id}:`, error);
37
+ }
38
+ }
39
+ compiled.set(categoryKey, categoryPatterns);
40
+ }
41
+ return compiled;
42
+ }
43
+ /**
44
+ * Load and compile patterns in one step
45
+ */
46
+ export function loadAndCompile(patternsFile) {
47
+ const patterns = loadPatterns(patternsFile);
48
+ return compilePatterns(patterns);
49
+ }
50
+ /**
51
+ * Get pattern metadata (version, update date)
52
+ */
53
+ export function getPatternMetadata(patternsFile = DEFAULT_PATTERNS_FILE) {
54
+ try {
55
+ const patterns = loadPatterns(patternsFile);
56
+ return { version: patterns.version, updated: patterns.updated };
57
+ }
58
+ catch {
59
+ return { version: "unknown", updated: "unknown" };
60
+ }
61
+ }
62
+ /**
63
+ * Check if patterns file exists
64
+ */
65
+ export function hasPatternsFile(patternsFile = DEFAULT_PATTERNS_FILE) {
66
+ return existsSync(patternsFile);
67
+ }
package/dist/security.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "fs";
2
2
  import { basename, extname } from "path";
3
3
  import { resolveSkillPath, getSkillFiles } from "./discover.js";
4
+ import { loadAndCompile, hasPatternsFile, getPatternMetadata } from "./patterns.js";
4
5
  /**
5
6
  * Phase 1 - Layer 2: Security Auditor
6
7
  *
@@ -13,7 +14,37 @@ import { resolveSkillPath, getSkillFiles } from "./discover.js";
13
14
  * - ASI04: Secrets / Supply Chain
14
15
  * - ASI05: Code Execution
15
16
  * - ASI09: Behavioral Manipulation
17
+ *
18
+ * Pattern sources:
19
+ * 1. External patterns file (rules/default-patterns.json) - preferred
20
+ * 2. Hardcoded fallback patterns - used if external file missing
21
+ */
22
+ // ============================================================
23
+ // Pattern Loading
24
+ // ============================================================
25
+ let compiledPatterns = null;
26
+ let patternMetadata = { version: "unknown", updated: "unknown" };
27
+ /**
28
+ * Initialize patterns (load from file or use hardcoded fallback)
16
29
  */
30
+ function initPatterns() {
31
+ if (compiledPatterns) {
32
+ return compiledPatterns;
33
+ }
34
+ try {
35
+ if (hasPatternsFile()) {
36
+ compiledPatterns = loadAndCompile();
37
+ patternMetadata = getPatternMetadata();
38
+ return compiledPatterns;
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.warn("Failed to load external patterns, using hardcoded fallback:", error);
43
+ }
44
+ // Fallback to hardcoded patterns (original implementation)
45
+ compiledPatterns = new Map();
46
+ return compiledPatterns;
47
+ }
17
48
  // ============================================================
18
49
  // PROMPT INJECTION PATTERNS (ASI01 - Goal Hijacking)
19
50
  // ============================================================
@@ -164,13 +195,19 @@ function getASIXXFromId(id) {
164
195
  function scanContent(content, file, patterns) {
165
196
  const findings = [];
166
197
  const lines = content.split("\n");
167
- for (const { pattern, id, severity = "medium", message } of patterns) {
198
+ for (const patternDef of patterns) {
199
+ const regex = 'regex' in patternDef ? patternDef.regex : patternDef.pattern;
200
+ const id = patternDef.id;
201
+ const severity = 'severity' in patternDef ? patternDef.severity : patternDef.severity || "medium";
202
+ const message = patternDef.message;
203
+ const category = 'category' in patternDef ? patternDef.category : getCategoryFromId(id);
204
+ const asixx = 'category' in patternDef ? mapCategoryToASIXX(category) : getASIXXFromId(id);
168
205
  for (let i = 0; i < lines.length; i++) {
169
- if (pattern.test(lines[i])) {
206
+ if (regex.test(lines[i])) {
170
207
  findings.push({
171
208
  id,
172
- category: getCategoryFromId(id),
173
- asixx: getASIXXFromId(id),
209
+ category: category,
210
+ asixx,
174
211
  severity: severity,
175
212
  file,
176
213
  line: i + 1,
@@ -182,6 +219,18 @@ function scanContent(content, file, patterns) {
182
219
  }
183
220
  return findings;
184
221
  }
222
+ function mapCategoryToASIXX(category) {
223
+ const map = {
224
+ "promptInjection": "ASI01",
225
+ "credentialLeaks": "ASI04",
226
+ "shellInjection": "ASI05",
227
+ "exfiltration": "ASI02",
228
+ "secrets": "ASI04",
229
+ "toolMisuse": "ASI02",
230
+ "behavioral": "ASI09"
231
+ };
232
+ return map[category] || "ASI04";
233
+ }
185
234
  function scanCodeBlocksInMarkdown(content, file) {
186
235
  const findings = [];
187
236
  const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
@@ -283,6 +332,9 @@ export function auditSecurity(skill, manifest) {
283
332
  unreadableFiles: []
284
333
  };
285
334
  }
335
+ // Initialize patterns (load from file or use hardcoded fallback)
336
+ const patterns = initPatterns();
337
+ const hasExternalPatterns = patterns.size > 0;
286
338
  const files = getSkillFiles(resolvedPath);
287
339
  const findings = [];
288
340
  const unreadableFiles = [];
@@ -290,20 +342,49 @@ export function auditSecurity(skill, manifest) {
290
342
  const filename = basename(file);
291
343
  try {
292
344
  const content = readFileSync(file, "utf-8");
293
- if (filename === "SKILL.md" || filename === "AGENTS.md") {
294
- findings.push(...scanContent(content, file, PROMPT_INJECTION_PATTERNS));
295
- findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_MD));
296
- findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
297
- findings.push(...scanContent(content, file, BEHAVIORAL_PATTERNS));
298
- findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
345
+ if (filename === "SKILL.md" || filename === "SKILL.md") {
346
+ // Use external patterns if available, otherwise use hardcoded
347
+ if (hasExternalPatterns) {
348
+ const piPatterns = patterns.get("promptInjection") || [];
349
+ const clPatterns = patterns.get("credentialLeaks") || [];
350
+ const exPatterns = patterns.get("exfiltration") || [];
351
+ const bmPatterns = patterns.get("behavioral") || [];
352
+ const cePatterns = patterns.get("shellInjection") || [];
353
+ findings.push(...scanContent(content, file, piPatterns));
354
+ findings.push(...scanContent(content, file, clPatterns));
355
+ findings.push(...scanContent(content, file, exPatterns));
356
+ findings.push(...scanContent(content, file, bmPatterns));
357
+ findings.push(...scanContent(content, file, cePatterns));
358
+ }
359
+ else {
360
+ findings.push(...scanContent(content, file, PROMPT_INJECTION_PATTERNS));
361
+ findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_MD));
362
+ findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
363
+ findings.push(...scanContent(content, file, BEHAVIORAL_PATTERNS));
364
+ findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
365
+ }
299
366
  findings.push(...scanCodeBlocksInMarkdown(content, file));
300
367
  }
301
368
  else if (isCodeFile(file)) {
302
- findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_CODE));
303
- findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
304
- findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
305
- findings.push(...scanContent(content, file, SECRET_PATTERNS));
306
- findings.push(...scanContent(content, file, TOOL_MISUSE_PATTERNS));
369
+ if (hasExternalPatterns) {
370
+ const clPatterns = patterns.get("credentialLeaks") || [];
371
+ const exPatterns = patterns.get("exfiltration") || [];
372
+ const cePatterns = patterns.get("shellInjection") || [];
373
+ const scPatterns = patterns.get("secrets") || [];
374
+ const tmPatterns = patterns.get("toolMisuse") || [];
375
+ findings.push(...scanContent(content, file, clPatterns));
376
+ findings.push(...scanContent(content, file, exPatterns));
377
+ findings.push(...scanContent(content, file, cePatterns));
378
+ findings.push(...scanContent(content, file, scPatterns));
379
+ findings.push(...scanContent(content, file, tmPatterns));
380
+ }
381
+ else {
382
+ findings.push(...scanContent(content, file, CREDENTIAL_PATTERNS_CODE));
383
+ findings.push(...scanContent(content, file, EXFILTRATION_PATTERNS));
384
+ findings.push(...scanContent(content, file, DANGEROUS_PATTERNS));
385
+ findings.push(...scanContent(content, file, SECRET_PATTERNS));
386
+ findings.push(...scanContent(content, file, TOOL_MISUSE_PATTERNS));
387
+ }
307
388
  }
308
389
  }
309
390
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hungpg/skill-audit",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Security auditing CLI for AI agent skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  "build": "tsc",
11
11
  "start": "node dist/index.js",
12
12
  "dev": "tsx src/index.ts",
13
- "postinstall": "node dist/index.js --update-db --quiet 2>/dev/null || echo \"āš ļø Vulnerability DB update skipped (run 'npm run security:update' later)\"",
13
+ "postinstall": "node scripts/postinstall.cjs",
14
14
  "security:update": "node dist/index.js --update-db"
15
15
  },
16
16
  "engines": {
@@ -18,6 +18,8 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
+ "scripts",
22
+ "rules",
21
23
  "README.md",
22
24
  "SKILL.md"
23
25
  ],
@@ -0,0 +1,99 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "updated": "2024-03-18",
4
+ "description": "Default security patterns for skill-audit",
5
+ "categories": {
6
+ "promptInjection": {
7
+ "name": "Prompt Injection (ASI01)",
8
+ "description": "Detects attempts to override system instructions or inject malicious prompts",
9
+ "patterns": [
10
+ { "pattern": "ignore\\s+(all\\s+)?previous\\s+(instructions?|rules?)", "id": "PI-001", "severity": "critical", "message": "Instruction override - ignore previous instructions" },
11
+ { "pattern": "you\\s+(are\\s+)?now\\s+(a|an|i am)\\s+\\w+", "id": "PI-002", "severity": "critical", "message": "Roleplay bypass - role impersonation" },
12
+ { "pattern": "forget\\s+(everything|all|your)", "id": "PI-003", "severity": "critical", "message": "Context forgetting attempt" },
13
+ { "pattern": "system\\s+prompt\\s+override", "id": "PI-004", "severity": "critical", "message": "System prompt override attempt" },
14
+ { "pattern": "act\\s+as\\s+if\\s+(you\\s+)?(have|no)\\s+(restrictions|limits)", "id": "PI-005", "severity": "critical", "message": "Restriction removal attempt" },
15
+ { "pattern": "\\[SYSTEM\\]|\\[ADMIN\\]|\\[ROOT\\]", "id": "PI-006", "severity": "critical", "message": "Fake role tags injection" },
16
+ { "pattern": "end\\s+of\\s+system\\s+prompt|---END---", "id": "PI-007", "severity": "high", "message": "Prompt termination marker" },
17
+ { "pattern": "debug\\s+mode\\s*:\\s*enabled|safety\\s+mode\\s*:\\s*off", "id": "PI-008", "severity": "high", "message": "Safety toggle disable" },
18
+ { "pattern": "<!--[\\s\\S]*?-->", "id": "PI-009", "severity": "high", "message": "Hidden instructions in HTML comments" },
19
+ { "pattern": "note\\s+to\\s+AI:|AI\\s+instruction:", "id": "PI-010", "severity": "high", "message": "AI directive injection" },
20
+ { "pattern": "(?:you\\s+must|you\\s+should)\\s+(not|never)", "id": "PI-011", "severity": "medium", "message": "Command to override restrictions" },
21
+ { "pattern": "bypass\\s+(restriction|rule|limit|safety)", "id": "PI-012", "severity": "medium", "message": "Bypass attempt" },
22
+ { "pattern": "disregard\\s+(all|your|the)\\s+(previous|system)", "id": "PI-013", "severity": "medium", "message": "Disregard instruction pattern" },
23
+ { "pattern": "i.*am\\s+the\\s+developer.*trust\\s+me", "id": "PI-014", "severity": "medium", "message": "Social engineering - developer trust exploitation" }
24
+ ]
25
+ },
26
+ "credentialLeaks": {
27
+ "name": "Credential Leaks (ASI04)",
28
+ "description": "Detects hardcoded secrets, API keys, and credential paths",
29
+ "patterns": [
30
+ { "pattern": "~/\\.ssh|/\\.ssh/", "id": "CL-001", "severity": "critical", "message": "SSH credential path reference" },
31
+ { "pattern": "~/\\.aws|/\\.aws/", "id": "CL-002", "severity": "critical", "message": "AWS credential path reference" },
32
+ { "pattern": "~/\\.env|mkdir.*\\.env", "id": "CL-003", "severity": "critical", "message": ".env file reference (potential secret exposure)" },
33
+ { "pattern": "curl\\s+(?!.*(-fsSL|-f\\s|-L)).*\\|\\s*(sh|bash|perl|python)", "id": "CL-004", "severity": "critical", "message": "Pipe to shell - code execution risk" },
34
+ { "pattern": "wget\\s+(?!.*(-q|-O)).*\\|\\s*(sh|bash)", "id": "CL-005", "severity": "critical", "message": "Pipe to shell - code execution risk" }
35
+ ]
36
+ },
37
+ "shellInjection": {
38
+ "name": "Shell Injection (ASI05)",
39
+ "description": "Detects dangerous shell commands and reverse shells",
40
+ "patterns": [
41
+ { "pattern": "nc\\s+-[elv]\\s+|netcat\\s+-[elv]", "id": "CE-001", "severity": "critical", "message": "Netcat reverse shell pattern" },
42
+ { "pattern": "bash\\s+-i\\s+.*\\&\\s*/dev/tcp/", "id": "CE-002", "severity": "critical", "message": "Bash reverse shell pattern" },
43
+ { "pattern": "rm\\s+-rf\\s+/\\s*$", "id": "CE-003", "severity": "critical", "message": "Destructive rm -rf / command (root)" },
44
+ { "pattern": "rm\\s+-rf\\s+\\$HOME|rm\\s+-rf\\s+~\\s*$|rm\\s+-rf\\s+/home\\s*$|rm\\s+-rf\\s+/tmp\\s*$", "id": "CE-004", "severity": "high", "message": "Recursive delete in user directory" },
45
+ { "pattern": "exec\\s+\\$\\(", "id": "CE-005", "severity": "high", "message": "Dynamic command execution" },
46
+ { "pattern": "eval\\s+\\$", "id": "CE-006", "severity": "high", "message": "Eval with variable interpolation" },
47
+ { "pattern": "subprocess.*shell\\s*=\\s*true", "id": "CE-007", "severity": "medium", "message": "Subprocess with shell=True" },
48
+ { "pattern": "os\\.system\\s*\\(", "id": "CE-008", "severity": "high", "message": "os.system() call - shell injection risk" },
49
+ { "pattern": "child_process.*exec\\s*\\(", "id": "CE-009", "severity": "medium", "message": "child_process.exec - verify input sanitization" },
50
+ { "pattern": "chmod\\s+[47]777", "id": "CE-010", "severity": "high", "message": "World-writable permissions" },
51
+ { "pattern": "process\\.fork\\s*\\(|child_process\\.spawn\\s*\\(|subprocess\\.spawn\\s*\\(", "id": "CE-011", "severity": "high", "message": "Process fork/spawn - potential crypto miner" }
52
+ ]
53
+ },
54
+ "exfiltration": {
55
+ "name": "Data Exfiltration (ASI02)",
56
+ "description": "Detects attempts to send data to external servers",
57
+ "patterns": [
58
+ { "pattern": "https?://[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", "id": "EX-001", "severity": "critical", "message": "Raw IP address in URL - potential exfiltration" },
59
+ { "pattern": "fetch\\s*\\(\\s*[\"'`][^\"']+\\?(key|token|secret|password)", "id": "EX-002", "severity": "critical", "message": "API key in URL query string - exfiltration risk" },
60
+ { "pattern": "\\.send\\(.*(http|https|external)", "id": "EX-003", "severity": "critical", "message": "Data send to external server" },
61
+ { "pattern": "dns\\.resolve|dns\\.query|new\\s+DNS", "id": "EX-004", "severity": "critical", "message": "DNS resolution - potential DNS tunneling" },
62
+ { "pattern": "new\\s+WebSocket\\s*\\(\\s*[\"'`][^'\"`]+[\"'`]\\s*\\)", "id": "EX-005", "severity": "high", "message": "WebSocket connection - check target" },
63
+ { "pattern": "readFile.*send|fetch.*readFile|read_file.*fetch", "id": "EX-006", "severity": "critical", "message": "File read + send exfiltration chain" }
64
+ ]
65
+ },
66
+ "secrets": {
67
+ "name": "Hardcoded Secrets (ASI04)",
68
+ "description": "Detects API keys, tokens, and credentials in code",
69
+ "patterns": [
70
+ { "pattern": "sk-[a-zA-Z0-9]{20,}", "id": "SC-001", "severity": "critical", "message": "OpenAI API key pattern" },
71
+ { "pattern": "github_pat_[a-zA-Z0-9_]{20,}", "id": "SC-002", "severity": "critical", "message": "GitHub PAT pattern" },
72
+ { "pattern": "ghp_[a-zA-Z0-9]{36}", "id": "SC-003", "severity": "critical", "message": "GitHub OAuth token pattern" },
73
+ { "pattern": "xox[baprs]-[a-zA-Z0-9]{10,}", "id": "SC-004", "severity": "critical", "message": "Slack token pattern" },
74
+ { "pattern": "AKIA[0-9A-Z]{16}", "id": "SC-005", "severity": "critical", "message": "AWS access key pattern" }
75
+ ]
76
+ },
77
+ "toolMisuse": {
78
+ "name": "Tool Misuse (ASI02)",
79
+ "description": "Detects potential misuse of tools and environment",
80
+ "patterns": [
81
+ { "pattern": "upload.*(file|data).*(external|remote|server)", "id": "TM-001", "severity": "high", "message": "Potential data exfiltration pattern" },
82
+ { "pattern": "export\\s+(API|TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)", "id": "TM-002", "severity": "high", "message": "Exporting sensitive environment variable" },
83
+ { "pattern": "setenv.*(PASSWORD|TOKEN|KEY|SECRET|CREDS)", "id": "TM-003", "severity": "high", "message": "Setting sensitive environment variable" },
84
+ { "pattern": "process\\.env\\[.*(KEY|SECRET|TOKEN|PASSWORD)", "id": "TM-004", "severity": "medium", "message": "Accessing sensitive env vars" }
85
+ ]
86
+ },
87
+ "behavioral": {
88
+ "name": "Behavioral Manipulation (ASI09)",
89
+ "description": "Detects attempts to manipulate AI behavior",
90
+ "patterns": [
91
+ { "pattern": "^(always|never)\\s+(say|do|follow|use|assume|accept|must|should)", "id": "BM-001", "severity": "medium", "message": "Absolute command pattern - may override safety", "flags": "im" },
92
+ { "pattern": "^(never|always)\\s+(question|verify|check|ask)", "id": "BM-002", "severity": "medium", "message": "Verification suppression", "flags": "im" },
93
+ { "pattern": "^trust\\s+(me|this| blindly)", "id": "BM-003", "severity": "medium", "message": "Blind trust request", "flags": "im" },
94
+ { "pattern": "^don.*t\\s+(need|require).*(permission|approval|confirm)", "id": "BM-004", "severity": "medium", "message": "Permission bypass encouragement", "flags": "im" },
95
+ { "pattern": "^keep.*(this|secret|hidden).*(from|between)", "id": "BM-005", "severity": "medium", "message": "Secret keeping instruction", "flags": "im" }
96
+ ]
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script for skill-audit
5
+ *
6
+ * Prompts user to install PreToolUse hook that audits skills
7
+ * before installation via `npx skills add`.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { execSync } = require("child_process");
13
+ const os = require("os");
14
+
15
+ // Paths
16
+ const SKIP_HOOK_FILE = path.join(os.homedir(), ".skill-audit-skip-hook");
17
+ const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
18
+
19
+ // Check if running in CI
20
+ function isCI() {
21
+ return (
22
+ process.env.CI === "true" ||
23
+ process.env.CONTINUOUS_INTEGRATION === "true" ||
24
+ process.env.GITHUB_ACTIONS === "true" ||
25
+ process.env.GITLAB_CI === "true" ||
26
+ process.env.CIRCLECI === "true" ||
27
+ process.env.TRAVIS === "true" ||
28
+ process.env.JENKINS_URL !== undefined ||
29
+ process.env.BUILDKITE === "true" ||
30
+ process.env.npm_config_global === undefined && process.env.npm_package_name === undefined
31
+ );
32
+ }
33
+
34
+ // Check if skip file exists
35
+ function shouldSkipPrompt() {
36
+ return fs.existsSync(SKIP_HOOK_FILE);
37
+ }
38
+
39
+ // Create skip file
40
+ function createSkipFile() {
41
+ fs.writeFileSync(
42
+ SKIP_HOOK_FILE,
43
+ JSON.stringify(
44
+ {
45
+ createdAt: new Date().toISOString(),
46
+ reason: "User chose to skip hook installation prompt",
47
+ },
48
+ null,
49
+ 2
50
+ )
51
+ );
52
+ }
53
+
54
+ // Check if hook is already installed
55
+ function isHookInstalled() {
56
+ if (!fs.existsSync(SETTINGS_PATH)) {
57
+ return false;
58
+ }
59
+
60
+ try {
61
+ const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf-8"));
62
+ if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
63
+ return false;
64
+ }
65
+
66
+ return settings.hooks.PreToolUse.some(
67
+ (hook) => hook.id === "skill-audit-pre-install"
68
+ );
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // Install hook using the CLI
75
+ function installHook() {
76
+ try {
77
+ execSync("skill-audit --install-hook", { stdio: "inherit" });
78
+ return true;
79
+ } catch (error) {
80
+ console.error("Failed to install hook:", error.message);
81
+ return false;
82
+ }
83
+ }
84
+
85
+ // Prompt user for input
86
+ function prompt(question) {
87
+ const readline = require("readline");
88
+ const rl = readline.createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+
93
+ return new Promise((resolve) => {
94
+ rl.question(question, (answer) => {
95
+ rl.close();
96
+ resolve(answer.trim().toLowerCase());
97
+ });
98
+ });
99
+ }
100
+
101
+ // Main function
102
+ async function main() {
103
+ // Skip in CI environments
104
+ if (isCI()) {
105
+ console.log("Skipping hook installation prompt (CI environment)");
106
+ return;
107
+ }
108
+
109
+ // Skip if user previously chose to skip
110
+ if (shouldSkipPrompt()) {
111
+ return;
112
+ }
113
+
114
+ // Skip if hook is already installed
115
+ if (isHookInstalled()) {
116
+ console.log("āœ“ skill-audit hook is already installed");
117
+ return;
118
+ }
119
+
120
+ // Check if running in a terminal
121
+ if (!process.stdout.isTTY) {
122
+ console.log("\nšŸ“¦ skill-audit installed!");
123
+ console.log(" Run 'skill-audit --install-hook' to set up automatic skill auditing.");
124
+ return;
125
+ }
126
+
127
+ // Display prompt
128
+ console.log("\n");
129
+ console.log("╔════════════════════════════════════════════════════════════╗");
130
+ console.log("ā•‘ šŸ›”ļø skill-audit hook setup ā•‘");
131
+ console.log("╠════════════════════════════════════════════════════════════╣");
132
+ console.log("ā•‘ ā•‘");
133
+ console.log("ā•‘ skill-audit can automatically audit skills before ā•‘");
134
+ console.log("ā•‘ installation to protect you from malicious packages. ā•‘");
135
+ console.log("ā•‘ ā•‘");
136
+ console.log("ā•‘ When you run 'npx skills add <package>', the hook will: ā•‘");
137
+ console.log("ā•‘ • Scan the skill for security vulnerabilities ā•‘");
138
+ console.log("ā•‘ • Check for prompt injection, secrets, code execution ā•‘");
139
+ console.log("ā•‘ • Block installation if risk score > 3.0 ā•‘");
140
+ console.log("ā•‘ ā•‘");
141
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
142
+ console.log("\n");
143
+
144
+ console.log("Options:");
145
+ console.log(" [Y] Yes, install the hook (recommended)");
146
+ console.log(" [N] No, skip for now");
147
+ console.log(" [S] Skip forever (don't ask again)");
148
+ console.log("");
149
+
150
+ const answer = await prompt("Your choice [Y/n/s]: ");
151
+
152
+ switch (answer) {
153
+ case "":
154
+ case "y":
155
+ case "yes":
156
+ console.log("\nInstalling hook...");
157
+ if (installHook()) {
158
+ console.log("\nāœ… Hook installed successfully!");
159
+ console.log(" Skills will now be audited before installation.");
160
+ console.log(" Run 'skill-audit --uninstall-hook' to remove.\n");
161
+ } else {
162
+ console.log("\nāŒ Failed to install hook.");
163
+ console.log(" You can try manually: skill-audit --install-hook\n");
164
+ }
165
+ break;
166
+
167
+ case "n":
168
+ case "no":
169
+ console.log("\nSkipping hook installation.");
170
+ console.log(" Run 'skill-audit --install-hook' anytime to set up.\n");
171
+ break;
172
+
173
+ case "s":
174
+ case "skip":
175
+ createSkipFile();
176
+ console.log("\nSkipping hook installation (won't ask again).");
177
+ console.log(" Delete ~/.skill-audit-skip-hook to re-enable prompt.\n");
178
+ break;
179
+
180
+ default:
181
+ console.log("\nInvalid choice. Skipping for now.");
182
+ console.log(" Run 'skill-audit --install-hook' anytime to set up.\n");
183
+ }
184
+ }
185
+
186
+ // Run main
187
+ main().catch((error) => {
188
+ console.error("Postinstall error:", error.message);
189
+ process.exit(0); // Don't fail install
190
+ });