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 +67 -16
- package/bin/scan.js +87 -4
- package/commands/goodvibesonly.md +33 -0
- package/package.json +1 -1
- package/skills/goodvibesonly/SKILL.md +50 -0
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
|
|
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. **
|
|
25
|
-
2. **
|
|
26
|
-
3. **Blocks on critical issues** - Prevents
|
|
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
|
|
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
|
|
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
|
-
|
|
97
|
+
When working inside Claude Code, GoodVibesOnly runs automatically whenever Claude executes a git commit or push:
|
|
96
98
|
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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
|
```
|