goodvibesonly-cc 0.3.1 → 0.4.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
@@ -15,19 +15,21 @@
15
15
 
16
16
  # GoodVibesOnly
17
17
 
18
- **Security scanner for vibe-coded projects.** A Claude Code extension that automatically scans for vulnerabilities before you commit.
18
+ **Security scanner for vibe-coded projects.** A Claude Code extension that automatically scans for vulnerabilities when Claude Code commits on your behalf.
19
19
 
20
20
  ## How It Works
21
21
 
22
- GoodVibesOnly uses Claude Code's hooks system to intercept git commands:
22
+ GoodVibesOnly uses Claude Code's [hooks system](https://docs.anthropic.com/en/docs/claude-code/hooks) to intercept git commands **within Claude Code sessions**. It does not hook into git directly — it only triggers when Claude Code itself runs a Bash command.
23
23
 
24
- 1. **Hooks into git commit/push** - Automatically runs before any `git commit` or `git push`
25
- 2. **Scans changed files** - Checks for hardcoded secrets, injection vulnerabilities, XSS, and more
26
- 3. **Blocks on critical issues** - Prevents commits with critical vulnerabilities (exit code 2)
24
+ 1. **Intercepts Claude Code's Bash calls** - A `PreToolUse` hook runs the scanner whenever Claude Code is about to execute a Bash command
25
+ 2. **Checks for git commit/push** - If the command is a `git commit` or `git push`, it scans staged files for hardcoded secrets, injection vulnerabilities, XSS, and more
26
+ 3. **Blocks on critical issues** - Prevents Claude Code from executing the commit by exiting with code 2
27
27
  4. **Allows warnings through** - High/medium issues are reported but don't block
28
28
 
29
+ > **Note:** This only works when committing through Claude Code. Running `git commit` directly in your terminal will not trigger the scan. For terminal-level git hooks, consider a traditional pre-commit hook tool.
30
+
29
31
  ```
30
- You: git commit -m "add user api"
32
+ You (in Claude Code): commit my changes
31
33
 
32
34
  🛡️ GoodVibesOnly Security Scan
33
35
 
@@ -42,7 +44,7 @@ You: git commit -m "add user api"
42
44
  db.query("SELECT * FROM users WHERE id = " + id)
43
45
 
44
46
  Found 2 critical, 0 high, 0 medium issues.
45
- Commit blocked. Fix critical issues or use --no-verify to bypass.
47
+ Commit blocked fix critical issues before committing.
46
48
  ```
47
49
 
48
50
  ## Installation
@@ -92,11 +94,11 @@ node bin/install.js --uninstall # Remove GoodVibesOnly
92
94
 
93
95
  ### Automatic (via hooks)
94
96
 
95
- Just use git normally. GoodVibesOnly runs automatically:
97
+ When working inside Claude Code, GoodVibesOnly runs automatically whenever Claude executes a git commit or push:
96
98
 
97
- ```bash
98
- git commit -m "message" # Scans before commit
99
- git push # Scans before push
99
+ ```
100
+ You: commit my changes # Scans before Claude runs git commit
101
+ You: push to origin # Scans before Claude runs git push
100
102
  ```
101
103
 
102
104
  ### Manual Scan
@@ -158,23 +160,72 @@ goodvibesonly/
158
160
  └── README.md
159
161
  ```
160
162
 
163
+ ## Allowlist
164
+
165
+ Suppress specific findings by adding a `.goodvibesonly.json` file to your project root:
166
+
167
+ ```json
168
+ {
169
+ "allow": [
170
+ { "pattern": "XSS via dangerouslySetInnerHTML", "reason": "Sanitized with DOMPurify" },
171
+ { "path": "test/**", "reason": "Test files contain intentional patterns" },
172
+ { "pattern": "SQL Injection", "path": "src/db/raw.js", "reason": "Parameterized at call site" }
173
+ ]
174
+ }
175
+ ```
176
+
177
+ Each entry in the `allow` array supports:
178
+
179
+ | Fields | Effect |
180
+ |--------|--------|
181
+ | `pattern` only | Suppress that pattern in all files |
182
+ | `path` only | Suppress all patterns in matching files |
183
+ | `pattern` + `path` | Suppress specific pattern in specific files |
184
+
185
+ - `reason` is expected on every entry (warns if missing)
186
+ - Pattern names must match exactly — run `node bin/scan.js --list-patterns` to see all names
187
+ - `path` supports glob patterns (`*` for single directory, `**` for recursive)
188
+
189
+ ### Conversational Flow
190
+
191
+ When GoodVibesOnly flags a finding in Claude Code, you can tell Claude to allow it:
192
+
193
+ ```
194
+ You: allow the dangerouslySetInnerHTML one
195
+ Claude: One-time (this commit only) or permanent?
196
+ You: permanent
197
+ Claude: What's the reason?
198
+ You: sanitized with DOMPurify
199
+ ```
200
+
201
+ - **One-time**: temporarily adds the entry, commits, then removes it
202
+ - **Permanent**: adds the entry to `.goodvibesonly.json` for you to commit later
203
+
204
+ ### List All Patterns
205
+
206
+ ```bash
207
+ node bin/scan.js --list-patterns
208
+ ```
209
+
161
210
  ## How It's Different
162
211
 
163
- - **Actually enforces** - Uses Claude Code hooks to block commits, not just advisory
212
+ - **Actually enforces** - Uses Claude Code's PreToolUse hooks to block commits, not just advisory
164
213
  - **Real scanning** - Node.js script with regex patterns, not just instructions for Claude
165
- - **Zero config** - Installs hooks automatically
214
+ - **Zero config** - Installs hooks automatically into Claude Code's settings
166
215
  - **Uninstall support** - Clean removal with `--uninstall`
167
216
 
168
217
  ## Technical Details
169
218
 
170
- GoodVibesOnly installs a `PreToolUse` hook that intercepts Bash commands. When it detects `git commit` or `git push`:
219
+ GoodVibesOnly installs a `PreToolUse` hook in Claude Code's settings. This hook runs before every Bash tool call that Claude Code makes. When the scanner detects the command is a `git commit` or `git push`:
171
220
 
172
221
  1. Reads staged files via `git diff --cached --name-only`
173
222
  2. Scans each file against vulnerability patterns
174
223
  3. Outputs findings to stderr
175
- 4. Exits with code 2 to block (critical issues) or 0 to allow
224
+ 4. Exits with code 2 to block Claude Code from running the command (critical issues) or 0 to allow it
225
+
226
+ For non-git commands, the scanner exits immediately with code 0 (allow).
176
227
 
177
- The hook is configured in `~/.claude/settings.json`:
228
+ The hook is configured in Claude Code's `settings.json`:
178
229
 
179
230
  ```json
180
231
  {
package/bin/scan.js CHANGED
@@ -95,6 +95,58 @@ const SCANNABLE_EXTENSIONS = new Set([
95
95
  '.config', '.conf', '.cfg', '.ini'
96
96
  ]);
97
97
 
98
+ function matchGlob(filePath, globPattern) {
99
+ // Convert glob pattern to regex
100
+ let regex = globPattern
101
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars (not * and ?)
102
+ .replace(/\*\*/g, '\0') // Temp placeholder for **
103
+ .replace(/\*/g, '[^/]*') // * matches anything except /
104
+ .replace(/\0/g, '.*') // ** matches anything including /
105
+ .replace(/\?/g, '[^/]'); // ? matches single non-/ char
106
+ return new RegExp(`^${regex}$`).test(filePath);
107
+ }
108
+
109
+ function loadAllowlist() {
110
+ const configPath = path.join(process.cwd(), '.goodvibesonly.json');
111
+ try {
112
+ if (!fs.existsSync(configPath)) return { allow: [] };
113
+ const raw = fs.readFileSync(configPath, 'utf8');
114
+ const config = JSON.parse(raw);
115
+ if (!Array.isArray(config.allow)) {
116
+ console.error('Warning: .goodvibesonly.json "allow" is not an array, ignoring');
117
+ return { allow: [] };
118
+ }
119
+ for (const entry of config.allow) {
120
+ if (!entry.reason) {
121
+ console.error(`Warning: allowlist entry missing "reason": ${JSON.stringify(entry)}`);
122
+ }
123
+ }
124
+ return config;
125
+ } catch (err) {
126
+ if (err instanceof SyntaxError) {
127
+ console.error(`Warning: .goodvibesonly.json has invalid JSON, ignoring: ${err.message}`);
128
+ }
129
+ return { allow: [] };
130
+ }
131
+ }
132
+
133
+ function isAllowed(patternName, filePath, allowlist) {
134
+ if (!allowlist || !allowlist.allow) return false;
135
+ for (const entry of allowlist.allow) {
136
+ const hasPattern = 'pattern' in entry;
137
+ const hasPath = 'path' in entry;
138
+
139
+ if (hasPattern && hasPath) {
140
+ if (entry.pattern === patternName && matchGlob(filePath, entry.path)) return true;
141
+ } else if (hasPattern) {
142
+ if (entry.pattern === patternName) return true;
143
+ } else if (hasPath) {
144
+ if (matchGlob(filePath, entry.path)) return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+
98
150
  function shouldScanFile(filePath) {
99
151
  const ext = path.extname(filePath).toLowerCase();
100
152
  const basename = path.basename(filePath).toLowerCase();
@@ -108,7 +160,7 @@ function shouldScanFile(filePath) {
108
160
  return SCANNABLE_EXTENSIONS.has(ext);
109
161
  }
110
162
 
111
- function scanFile(filePath) {
163
+ function scanFile(filePath, allowlist) {
112
164
  const findings = [];
113
165
 
114
166
  if (!fs.existsSync(filePath)) return findings;
@@ -137,6 +189,8 @@ function scanFile(filePath) {
137
189
  const matches = line.match(regex);
138
190
 
139
191
  if (matches) {
192
+ if (isAllowed(name, filePath, allowlist)) return;
193
+
140
194
  findings.push({
141
195
  severity,
142
196
  type: name,
@@ -165,7 +219,7 @@ function getChangedFiles(staged = true) {
165
219
  }
166
220
  }
167
221
 
168
- function formatFindings(findings) {
222
+ function formatFindings(findings, allowlist) {
169
223
  const bySeverity = { critical: [], high: [], medium: [] };
170
224
 
171
225
  for (const f of findings) {
@@ -177,6 +231,9 @@ function formatFindings(findings) {
177
231
  const total = findings.length;
178
232
  if (total === 0) {
179
233
  output += '✓ No security issues found\n';
234
+ if (allowlist && allowlist.allow.length > 0) {
235
+ output += ` (${allowlist.allow.length} allowlist rule${allowlist.allow.length === 1 ? '' : 's'} active)\n`;
236
+ }
180
237
  return output;
181
238
  }
182
239
 
@@ -208,11 +265,34 @@ function formatFindings(findings) {
208
265
 
209
266
  output += `Found ${bySeverity.critical.length} critical, ${bySeverity.high.length} high, ${bySeverity.medium.length} medium issues.\n`;
210
267
 
268
+ if (allowlist && allowlist.allow.length > 0) {
269
+ output += `(${allowlist.allow.length} allowlist rule${allowlist.allow.length === 1 ? '' : 's'} active — some findings may be suppressed)\n`;
270
+ }
271
+
211
272
  return output;
212
273
  }
213
274
 
275
+ function listPatterns() {
276
+ let output = 'GoodVibesOnly Scanner — All Pattern Names\n\n';
277
+ for (const [severity, patterns] of Object.entries(PATTERNS)) {
278
+ output += `${severity.toUpperCase()}:\n`;
279
+ for (const { name } of patterns) {
280
+ output += ` - ${name}\n`;
281
+ }
282
+ output += '\n';
283
+ }
284
+ output += `Total: ${Object.values(PATTERNS).reduce((sum, p) => sum + p.length, 0)} patterns\n`;
285
+ console.log(output);
286
+ }
287
+
214
288
  async function main() {
215
289
  const args = process.argv.slice(2);
290
+
291
+ if (args.includes('--list-patterns')) {
292
+ listPatterns();
293
+ process.exit(0);
294
+ }
295
+
216
296
  let files = [];
217
297
  let isHook = false;
218
298
 
@@ -278,15 +358,18 @@ async function main() {
278
358
  process.exit(0);
279
359
  }
280
360
 
361
+ // Load allowlist
362
+ const allowlist = loadAllowlist();
363
+
281
364
  // Scan all files
282
365
  const allFindings = [];
283
366
  for (const file of files) {
284
- const findings = scanFile(file);
367
+ const findings = scanFile(file, allowlist);
285
368
  allFindings.push(...findings);
286
369
  }
287
370
 
288
371
  // Output results
289
- const output = formatFindings(allFindings);
372
+ const output = formatFindings(allFindings, allowlist);
290
373
 
291
374
  const criticalCount = allFindings.filter(f => f.severity === 'critical').length;
292
375
 
@@ -81,6 +81,39 @@ If clean:
81
81
  ✓ GoodVibesOnly passed - no security issues found in [N] files
82
82
  ```
83
83
 
84
+ ## Allowlist: `.goodvibesonly.json`
85
+
86
+ Users can suppress specific findings by adding entries to `.goodvibesonly.json` in the project root:
87
+
88
+ ```json
89
+ {
90
+ "allow": [
91
+ { "pattern": "XSS via dangerouslySetInnerHTML", "reason": "Sanitized with DOMPurify" },
92
+ { "path": "test/**", "reason": "Test files contain intentional patterns" },
93
+ { "pattern": "SQL Injection", "path": "src/db/raw.js", "reason": "Parameterized at call site" }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ - `pattern` only: suppress that pattern in all files
99
+ - `path` only: suppress all patterns in matching files (supports `*` and `**` globs)
100
+ - `pattern` + `path`: suppress specific pattern in specific files
101
+ - Every entry should have a `reason`
102
+ - Pattern names must match exactly — run `node bin/scan.js --list-patterns` to see all names
103
+
104
+ ### Allowing Findings in Conversation
105
+
106
+ When a user wants to allow a finding:
107
+
108
+ 1. Ask: **one-time** (this commit only) or **permanent**?
109
+ 2. Ask for a reason
110
+
111
+ **One-time**: Write entry to `.goodvibesonly.json` (don't stage it), re-run commit, then remove the entry afterward.
112
+
113
+ **Permanent**: Write entry to `.goodvibesonly.json`, re-run commit, leave the file for user to commit later.
114
+
115
+ Always show the user exactly what was added to the config.
116
+
84
117
  ## After Scanning
85
118
 
86
119
  - If CRITICAL issues: Offer to fix them automatically
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goodvibesonly-cc",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Security scanner for vibe-coded projects - Claude Code extension",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,6 +91,56 @@ http://(?!localhost) # Non-HTTPS
91
91
  1. Brief summary
92
92
  2. Proceed with the requested action
93
93
 
94
+ ## Allowlist Flow
95
+
96
+ When a user wants to suppress a specific finding, follow this flow:
97
+
98
+ 1. **User says** something like "allow the dangerouslySetInnerHTML one" or "ignore the XSS finding"
99
+ 2. **Ask**: "One-time (this commit only) or permanent?"
100
+ 3. **Ask for reason**: "What's the reason for allowing this?" (e.g., "Sanitized with DOMPurify")
101
+
102
+ ### One-Time Allow
103
+
104
+ 1. Read existing `.goodvibesonly.json` (or create `{ "allow": [] }` if missing)
105
+ 2. Add the temporary entry to the `allow` array
106
+ 3. Write the file (**do not** stage it with `git add`)
107
+ 4. Re-run the commit command
108
+ 5. After commit completes, remove the temporary entry from `.goodvibesonly.json`
109
+ 6. If the file is now empty (`{ "allow": [] }`), delete it
110
+
111
+ ### Permanent Allow
112
+
113
+ 1. Read existing `.goodvibesonly.json` (or create `{ "allow": [] }` if missing)
114
+ 2. Add the entry to the `allow` array with the user's reason
115
+ 3. Write the file (leave it for the user to commit when ready)
116
+ 4. Re-run the commit command
117
+ 5. Tell the user: "Added permanent allowlist rule. You can commit `.goodvibesonly.json` when ready."
118
+
119
+ ### Config Format: `.goodvibesonly.json`
120
+
121
+ ```json
122
+ {
123
+ "allow": [
124
+ { "pattern": "XSS via dangerouslySetInnerHTML", "reason": "Sanitized with DOMPurify" },
125
+ { "path": "test/**", "reason": "Test files contain intentional patterns" },
126
+ { "pattern": "SQL Injection", "path": "src/db/raw.js", "reason": "Parameterized at call site" }
127
+ ]
128
+ }
129
+ ```
130
+
131
+ - `pattern` only: suppress that pattern in all files
132
+ - `path` only: suppress all patterns in matching files (supports `*` and `**` globs)
133
+ - `pattern` + `path`: suppress specific pattern in specific files
134
+ - Pattern names must match exactly — run `node bin/scan.js --list-patterns` to see all names
135
+
136
+ ### Show the User What Changed
137
+
138
+ After adding an entry, show the user what was added:
139
+ ```
140
+ Added to .goodvibesonly.json:
141
+ { "pattern": "XSS via dangerouslySetInnerHTML", "reason": "Sanitized with DOMPurify" }
142
+ ```
143
+
94
144
  ## Example Output
95
145
 
96
146
  ```