@hungpg/skill-audit 0.2.0 → 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.
package/README.md CHANGED
@@ -12,10 +12,109 @@ Security auditing CLI for AI agent skills.
12
12
 
13
13
  ## Installation
14
14
 
15
+ ### Option 1: Install via npm (Recommended for CLI)
16
+
15
17
  ```bash
16
18
  npm install -g @hungpg/skill-audit
17
19
  ```
18
20
 
21
+ This installs the CLI globally and triggers the postinstall hook prompt.
22
+
23
+ ### Option 2: Install via bun (Fast Alternative)
24
+
25
+ ```bash
26
+ bun install -g @hungpg/skill-audit
27
+ ```
28
+
29
+ Bun is significantly faster than npm for installation.
30
+
31
+ ### Option 3: Install as a Skill (For Claude Code)
32
+
33
+ ```bash
34
+ # Install from GitHub repo (not npm package name)
35
+ npx skills add harrypham2000/skill-audit -g -y
36
+ ```
37
+
38
+ > ⚠️ **Important**: The skills CLI expects `owner/repo` format, not npm scoped packages.
39
+ > - ✅ Correct: `harrypham2000/skill-audit`
40
+ > - ❌ Incorrect: `@hungpg/skill-audit`
41
+
42
+ ### Option 4: Install for Qwen Code
43
+
44
+ ```bash
45
+ # Clone to Qwen skills directory
46
+ mkdir -p ~/.qwen/skills
47
+ git clone https://github.com/harrypham2000/skill-audit.git ~/.qwen/skills/skill-audit
48
+ cd ~/.qwen/skills/skill-audit/skill-audit
49
+ npm install && npm run build
50
+
51
+ # Or with bun (faster)
52
+ bun install && bun run build
53
+ ```
54
+
55
+ ### Option 5: Install for Gemini CLI
56
+
57
+ ```bash
58
+ # Clone to Gemini skills directory
59
+ mkdir -p ~/.gemini/skills
60
+ git clone https://github.com/harrypham2000/skill-audit.git ~/.gemini/skills/skill-audit
61
+ cd ~/.gemini/skills/skill-audit/skill-audit
62
+ npm install && npm run build
63
+
64
+ # Or with bun (faster)
65
+ bun install && bun run build
66
+ ```
67
+
68
+ ## Automatic Hook Setup
69
+
70
+ During npm installation, you'll be prompted to set up a **PreToolUse hook** that automatically audits skills before installation:
71
+
72
+ ```
73
+ ╔════════════════════════════════════════════════════════════╗
74
+ ║ 🛡️ skill-audit hook setup ║
75
+ ╠════════════════════════════════════════════════════════════╣
76
+ ║ ║
77
+ ║ skill-audit can automatically audit skills before ║
78
+ ║ installation to protect you from malicious packages. ║
79
+ ║ ║
80
+ ║ When you run 'npx skills add <package>', the hook will: ║
81
+ ║ • Scan the skill for security vulnerabilities ║
82
+ ║ • Check for prompt injection, secrets, code execution ║
83
+ ║ • Block installation if risk score > 3.0 ║
84
+ ║ ║
85
+ ╚════════════════════════════════════════════════════════════╝
86
+
87
+ Options:
88
+ [Y] Yes, install the hook (recommended)
89
+ [N] No, skip for now
90
+ [S] Skip forever (don't ask again)
91
+ ```
92
+
93
+ ### Manual Hook Management
94
+
95
+ ```bash
96
+ # Install hook manually
97
+ skill-audit --install-hook
98
+
99
+ # Install with custom threshold
100
+ skill-audit --install-hook --hook-threshold 5.0
101
+
102
+ # Check hook status
103
+ skill-audit --hook-status
104
+
105
+ # Remove hook
106
+ skill-audit --uninstall-hook
107
+ ```
108
+
109
+ ### How the Hook Works
110
+
111
+ 1. **Trigger**: When you run `npx skills add <package>`
112
+ 2. **Scan**: skill-audit analyzes the skill before installation
113
+ 3. **Decision**:
114
+ - Risk score ≤ 3.0 → Installation proceeds
115
+ - Risk score > 3.0 → Installation blocked
116
+ 4. **Report**: Detailed findings shown in terminal
117
+
19
118
  ## Usage
20
119
 
21
120
  ```bash
@@ -59,6 +158,11 @@ skill-audit --update-db
59
158
  | `-o, --output <file>` | Save to file | |
60
159
  | `--no-deps` | Skip dependency scan | |
61
160
  | `-v, --verbose` | Verbose output | |
161
+ | `--install-hook` | Install PreToolUse hook | |
162
+ | `--uninstall-hook` | Remove PreToolUse hook | |
163
+ | `--hook-threshold <score>` | Hook risk threshold | 3.0 |
164
+ | `--hook-status` | Show hook status | |
165
+ | `--block` | Exit 1 if threshold exceeded | |
62
166
 
63
167
  ## Exit Codes
64
168
 
package/SKILL.md CHANGED
@@ -4,8 +4,8 @@ description: This skill should be used when the user asks to "audit AI agent ski
4
4
  license: MIT
5
5
  compatibility: Node.js 18+ with npm or yarn
6
6
  metadata:
7
- repo: https://github.com/vercel/skill-audit
8
- version: 0.2.0
7
+ repo: https://github.com/harrypham2000/skill-audit
8
+ version: 0.3.0
9
9
  allowed-tools:
10
10
  - skill:exec
11
11
  - skill:read
@@ -14,7 +14,56 @@ allowed-tools:
14
14
 
15
15
  # skill-audit
16
16
 
17
- Security auditing CLI for AI agent skills in the Vercel ecosystem.
17
+ Security auditing CLI for AI agent skills.
18
+
19
+ ## Installation for Agents
20
+
21
+ ### Claude Code
22
+
23
+ ```bash
24
+ # Option 1: Install as skill from GitHub
25
+ npx skills add harrypham2000/skill-audit -g -y
26
+
27
+ # Option 2: Install CLI via npm
28
+ npm install -g @hungpg/skill-audit
29
+
30
+ # Option 3: Install CLI via bun (faster)
31
+ bun install -g @hungpg/skill-audit
32
+ ```
33
+
34
+ ### Qwen Code
35
+
36
+ ```bash
37
+ # Clone to Qwen skills directory
38
+ mkdir -p ~/.qwen/skills
39
+ git clone https://github.com/harrypham2000/skill-audit.git ~/.qwen/skills/skill-audit
40
+ cd ~/.qwen/skills/skill-audit/skill-audit
41
+
42
+ # Install with npm
43
+ npm install && npm run build
44
+
45
+ # Or with bun (faster)
46
+ bun install && bun run build
47
+ ```
48
+
49
+ ### Gemini CLI
50
+
51
+ ```bash
52
+ # Clone to Gemini skills directory
53
+ mkdir -p ~/.gemini/skills
54
+ git clone https://github.com/harrypham2000/skill-audit.git ~/.gemini/skills/skill-audit
55
+ cd ~/.gemini/skills/skill-audit/skill-audit
56
+
57
+ # Install with npm
58
+ npm install && npm run build
59
+
60
+ # Or with bun (faster)
61
+ bun install && bun run build
62
+ ```
63
+
64
+ > ⚠️ **Important for Skills CLI**: Use `owner/repo` format, not npm scoped names.
65
+ > - ✅ Correct: `harrypham2000/skill-audit`
66
+ > - ❌ Incorrect: `@hungpg/skill-audit`
18
67
 
19
68
  ## When to Use
20
69
 
package/dist/hooks.js ADDED
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Hook Configuration for Claude Code
3
+ *
4
+ * Provides PreToolUse hook that audits skills before installation.
5
+ * Hook is triggered when user runs `npx skills add <package>`.
6
+ */
7
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { homedir } from "os";
10
+ // Default settings path for Claude Code
11
+ const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
12
+ const CLAUDE_SETTINGS_BACKUP = join(homedir(), ".claude", "settings.json.backup");
13
+ const SKIP_HOOK_FILE = join(homedir(), ".skill-audit-skip-hook");
14
+ // Hook identifier for skill-audit
15
+ const HOOK_ID = "skill-audit-pre-install";
16
+ /**
17
+ * Get the default hook configuration
18
+ */
19
+ export function getDefaultHookConfig() {
20
+ return {
21
+ threshold: 3.0,
22
+ blockOnFailure: true
23
+ };
24
+ }
25
+ /**
26
+ * Generate the PreToolUse hook configuration
27
+ */
28
+ export function generateHookConfig(config = getDefaultHookConfig()) {
29
+ return {
30
+ hooks: {
31
+ PreToolUse: [
32
+ {
33
+ id: HOOK_ID,
34
+ matcher: {
35
+ toolName: "run_shell_command",
36
+ input: "npx skills add"
37
+ },
38
+ hooks: [
39
+ {
40
+ type: "command",
41
+ command: `skill-audit --mode audit --threshold ${config.threshold}${config.blockOnFailure ? " --block" : ""}`
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Check if the skip hook file exists
51
+ */
52
+ export function shouldSkipHookPrompt() {
53
+ return existsSync(SKIP_HOOK_FILE);
54
+ }
55
+ /**
56
+ * Create the skip hook file
57
+ */
58
+ export function createSkipHookFile() {
59
+ writeFileSync(SKIP_HOOK_FILE, JSON.stringify({
60
+ createdAt: new Date().toISOString(),
61
+ reason: "User chose to skip hook installation prompt"
62
+ }, null, 2));
63
+ }
64
+ /**
65
+ * Remove the skip hook file
66
+ */
67
+ export function removeSkipHookFile() {
68
+ if (existsSync(SKIP_HOOK_FILE)) {
69
+ const fs = require("fs");
70
+ fs.unlinkSync(SKIP_HOOK_FILE);
71
+ }
72
+ }
73
+ /**
74
+ * Load existing settings.json
75
+ */
76
+ function loadSettings() {
77
+ if (!existsSync(CLAUDE_SETTINGS_PATH)) {
78
+ return {};
79
+ }
80
+ try {
81
+ const content = readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
82
+ return JSON.parse(content);
83
+ }
84
+ catch (e) {
85
+ console.error("Failed to parse existing settings.json:", e);
86
+ return {};
87
+ }
88
+ }
89
+ /**
90
+ * Backup existing settings.json
91
+ */
92
+ function backupSettings() {
93
+ if (!existsSync(CLAUDE_SETTINGS_PATH)) {
94
+ return true; // Nothing to backup
95
+ }
96
+ try {
97
+ // Ensure directory exists
98
+ const settingsDir = dirname(CLAUDE_SETTINGS_BACKUP);
99
+ if (!existsSync(settingsDir)) {
100
+ mkdirSync(settingsDir, { recursive: true });
101
+ }
102
+ copyFileSync(CLAUDE_SETTINGS_PATH, CLAUDE_SETTINGS_BACKUP);
103
+ return true;
104
+ }
105
+ catch (e) {
106
+ console.error("Failed to backup settings.json:", e);
107
+ return false;
108
+ }
109
+ }
110
+ /**
111
+ * Check if hook is already installed
112
+ */
113
+ export function isHookInstalled() {
114
+ const settings = loadSettings();
115
+ if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
116
+ return false;
117
+ }
118
+ // PreToolUse can be an array of arrays or array of objects
119
+ const preToolUseHooks = settings.hooks.PreToolUse;
120
+ for (const item of preToolUseHooks) {
121
+ // Handle nested array structure
122
+ if (Array.isArray(item)) {
123
+ if (item.some((h) => h.id === HOOK_ID)) {
124
+ return true;
125
+ }
126
+ }
127
+ else if (typeof item === "object" && item !== null) {
128
+ if (item.id === HOOK_ID) {
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+ return false;
134
+ }
135
+ /**
136
+ * Install the PreToolUse hook
137
+ */
138
+ export function installHook(config = getDefaultHookConfig()) {
139
+ // Check if already installed
140
+ if (isHookInstalled()) {
141
+ return { success: true, message: "Hook is already installed" };
142
+ }
143
+ // Backup existing settings
144
+ if (!backupSettings()) {
145
+ return { success: false, message: "Failed to backup settings.json" };
146
+ }
147
+ // Load existing settings
148
+ const settings = loadSettings();
149
+ // Initialize hooks structure if not present
150
+ if (!settings.hooks) {
151
+ settings.hooks = {};
152
+ }
153
+ if (!settings.hooks.PreToolUse) {
154
+ settings.hooks.PreToolUse = [];
155
+ }
156
+ // Create the new hook object
157
+ const newHook = {
158
+ id: HOOK_ID,
159
+ matcher: {
160
+ toolName: "run_shell_command",
161
+ input: "npx skills add"
162
+ },
163
+ hooks: [
164
+ {
165
+ type: "command",
166
+ command: `skill-audit --mode audit --threshold ${config.threshold}${config.blockOnFailure ? " --block" : ""}`
167
+ }
168
+ ]
169
+ };
170
+ // Add the hook - wrap in array to match existing structure
171
+ const preToolUseHooks = settings.hooks.PreToolUse;
172
+ preToolUseHooks.push([newHook]);
173
+ // Ensure directory exists
174
+ const settingsDir = dirname(CLAUDE_SETTINGS_PATH);
175
+ if (!existsSync(settingsDir)) {
176
+ mkdirSync(settingsDir, { recursive: true });
177
+ }
178
+ // Write updated settings
179
+ try {
180
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
181
+ return { success: true, message: `Hook installed successfully (threshold: ${config.threshold})` };
182
+ }
183
+ catch (e) {
184
+ // Restore backup on failure
185
+ if (existsSync(CLAUDE_SETTINGS_BACKUP)) {
186
+ copyFileSync(CLAUDE_SETTINGS_BACKUP, CLAUDE_SETTINGS_PATH);
187
+ }
188
+ return { success: false, message: `Failed to write settings: ${e}` };
189
+ }
190
+ }
191
+ /**
192
+ * Uninstall the PreToolUse hook
193
+ */
194
+ export function uninstallHook() {
195
+ const settings = loadSettings();
196
+ if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
197
+ return { success: true, message: "No hooks to remove" };
198
+ }
199
+ const preToolUseHooks = settings.hooks.PreToolUse;
200
+ const initialLength = preToolUseHooks.length;
201
+ // Filter out our hook (handles nested array structure)
202
+ const filteredHooks = preToolUseHooks.filter((item) => {
203
+ if (Array.isArray(item)) {
204
+ return !item.some((h) => h.id === HOOK_ID);
205
+ }
206
+ else if (typeof item === "object" && item !== null) {
207
+ return item.id !== HOOK_ID;
208
+ }
209
+ return true;
210
+ });
211
+ if (filteredHooks.length === initialLength) {
212
+ return { success: true, message: "Hook was not installed" };
213
+ }
214
+ // Backup before modification
215
+ if (!backupSettings()) {
216
+ return { success: false, message: "Failed to backup settings.json" };
217
+ }
218
+ // Update settings
219
+ settings.hooks.PreToolUse = filteredHooks;
220
+ // Remove hooks object if empty
221
+ if (filteredHooks.length === 0) {
222
+ delete settings.hooks.PreToolUse;
223
+ }
224
+ if (Object.keys(settings.hooks).length === 0) {
225
+ delete settings.hooks;
226
+ }
227
+ // Write updated settings
228
+ try {
229
+ writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
230
+ return { success: true, message: "Hook uninstalled successfully" };
231
+ }
232
+ catch (e) {
233
+ // Restore backup on failure
234
+ if (existsSync(CLAUDE_SETTINGS_BACKUP)) {
235
+ copyFileSync(CLAUDE_SETTINGS_BACKUP, CLAUDE_SETTINGS_PATH);
236
+ }
237
+ return { success: false, message: `Failed to write settings: ${e}` };
238
+ }
239
+ }
240
+ /**
241
+ * Get hook status
242
+ */
243
+ export function getHookStatus() {
244
+ const settings = loadSettings();
245
+ if (!settings.hooks || !Array.isArray(settings.hooks.PreToolUse)) {
246
+ return { installed: false, settingsPath: CLAUDE_SETTINGS_PATH };
247
+ }
248
+ const preToolUseHooks = settings.hooks.PreToolUse;
249
+ // Find the hook in nested array structure
250
+ let hook;
251
+ for (const item of preToolUseHooks) {
252
+ if (Array.isArray(item)) {
253
+ hook = item.find((h) => h.id === HOOK_ID);
254
+ if (hook)
255
+ break;
256
+ }
257
+ else if (typeof item === "object" && item !== null) {
258
+ if (item.id === HOOK_ID) {
259
+ hook = item;
260
+ break;
261
+ }
262
+ }
263
+ }
264
+ if (!hook) {
265
+ return { installed: false, settingsPath: CLAUDE_SETTINGS_PATH };
266
+ }
267
+ // Extract config from hook command
268
+ const hookHooks = hook.hooks;
269
+ const command = hookHooks[0].command;
270
+ const thresholdMatch = command.match(/--threshold\s+([\d.]+)/);
271
+ const threshold = thresholdMatch ? parseFloat(thresholdMatch[1]) : 3.0;
272
+ const blockOnFailure = command.includes("--block");
273
+ return {
274
+ installed: true,
275
+ config: { threshold, blockOnFailure },
276
+ settingsPath: CLAUDE_SETTINGS_PATH
277
+ };
278
+ }
package/dist/index.js CHANGED
@@ -6,13 +6,14 @@ import { validateSkillSpec } from "./spec.js";
6
6
  import { createGroupedAuditResult } from "./scoring.js";
7
7
  import { scanDependencies } from "./deps.js";
8
8
  import { getKEV, getEPSS, getNVD, isCacheStale, downloadOfflineDB } from "./intel.js";
9
+ import { installHook, uninstallHook, getHookStatus, getDefaultHookConfig } from "./hooks.js";
9
10
  import { writeFileSync } from "fs";
10
11
  // Build CLI - no subcommands, just options + action
11
12
  const program = new Command();
12
13
  program
13
14
  .name("skill-audit")
14
15
  .description("Security auditing CLI for AI agent skills")
15
- .version("0.1.0")
16
+ .version("0.3.0")
16
17
  .option("-g, --global", "Audit global skills only (default: true)")
17
18
  .option("-p, --project", "Audit project-level skills only")
18
19
  .option("-a, --agent <agents...>", "Filter by specific agents")
@@ -26,7 +27,12 @@ program
26
27
  .option("--source <sources...>", "Sources for update-db: kev, epss, nvd, all", ["all"])
27
28
  .option("--strict", "Fail if feeds are stale")
28
29
  .option("--quiet", "Suppress non-error output")
29
- .option("--download-offline-db <dir>", "Download offline vulnerability databases to directory");
30
+ .option("--download-offline-db <dir>", "Download offline vulnerability databases to directory")
31
+ .option("--install-hook", "Install PreToolUse hook for automatic skill auditing")
32
+ .option("--uninstall-hook", "Remove the PreToolUse hook")
33
+ .option("--hook-threshold <score>", "Risk threshold for hook (default: 3.0)", parseFloat)
34
+ .option("--hook-status", "Show current hook status")
35
+ .option("--block", "Exit with code 1 if threshold exceeded (for hooks)");
30
36
  program.parse(process.argv);
31
37
  const options = program.opts();
32
38
  // Handle download-offline-db action
@@ -39,6 +45,52 @@ if (options.updateDb) {
39
45
  await updateAdvisoryDB({ source: options.source, strict: options.strict });
40
46
  process.exit(0);
41
47
  }
48
+ // Handle hook-status action
49
+ if (options.hookStatus) {
50
+ const status = getHookStatus();
51
+ console.log("\n🪝 skill-audit Hook Status\n");
52
+ console.log(` Installed: ${status.installed ? "✅ Yes" : "❌ No"}`);
53
+ if (status.installed && status.config) {
54
+ console.log(` Threshold: ${status.config.threshold}`);
55
+ console.log(` Block on failure: ${status.config.blockOnFailure ? "Yes" : "No"}`);
56
+ }
57
+ console.log(` Settings file: ${status.settingsPath}\n`);
58
+ process.exit(0);
59
+ }
60
+ // Handle install-hook action
61
+ if (options.installHook) {
62
+ const config = getDefaultHookConfig();
63
+ if (options.hookThreshold) {
64
+ config.threshold = options.hookThreshold;
65
+ }
66
+ config.blockOnFailure = true;
67
+ console.log("\n🪝 Installing skill-audit hook...\n");
68
+ const result = installHook(config);
69
+ if (result.success) {
70
+ console.log(`✅ ${result.message}`);
71
+ console.log(` Settings file: ${getHookStatus().settingsPath}`);
72
+ console.log("\n Skills will now be audited before installation.");
73
+ console.log(" Run 'skill-audit --uninstall-hook' to remove.\n");
74
+ }
75
+ else {
76
+ console.error(`❌ ${result.message}`);
77
+ process.exit(1);
78
+ }
79
+ process.exit(0);
80
+ }
81
+ // Handle uninstall-hook action
82
+ if (options.uninstallHook) {
83
+ console.log("\n🪝 Removing skill-audit hook...\n");
84
+ const result = uninstallHook();
85
+ if (result.success) {
86
+ console.log(`✅ ${result.message}\n`);
87
+ }
88
+ else {
89
+ console.error(`❌ ${result.message}`);
90
+ process.exit(1);
91
+ }
92
+ process.exit(0);
93
+ }
42
94
  // Default to global skills
43
95
  const scope = options.project ? "project" : "global";
44
96
  const mode = options.mode || "audit";
@@ -78,7 +130,8 @@ reportGroupedResults(results, {
78
130
  output: options.output,
79
131
  verbose: options.verbose,
80
132
  threshold: options.threshold,
81
- mode
133
+ mode,
134
+ block: options.block
82
135
  });
83
136
  async function updateAdvisoryDB(opts) {
84
137
  const sources = opts.source.includes("all") ? ["kev", "epss", "nvd"] : opts.source;
@@ -124,7 +177,7 @@ async function updateAdvisoryDB(opts) {
124
177
  }
125
178
  }
126
179
  function reportGroupedResults(results, options) {
127
- const { json, output, verbose, threshold, mode } = options;
180
+ const { json, output, verbose, threshold, mode, block } = options;
128
181
  // Export to file if specified
129
182
  if (output) {
130
183
  const report = {
@@ -190,6 +243,10 @@ function reportGroupedResults(results, options) {
190
243
  for (const f of failing) {
191
244
  console.log(` - ${f.skill.name}: ${f.riskScore}`);
192
245
  }
246
+ // Exit with error code if block flag is set
247
+ if (block) {
248
+ process.exit(1);
249
+ }
193
250
  }
194
251
  else {
195
252
  console.log(`\n✅ All skills pass threshold ${threshold}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hungpg/skill-audit",
3
- "version": "0.2.0",
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
+ });