| name | vibe-safe |
| description | Use when a non-technical contributor (PM, designer, researcher) is about to write, commit, or merge AI-assisted code in a shared codebase and wants to catch the specific ways vibecoding goes wrong before they become incidents. |
vibe-safe
Overview
Active safety session for non-technical contributors. Claude reads your actual git state and scans real files — you don't self-report anything. Every flag cites the line and file.
Core rule: "Claude said it's fine" is not a safety check. This skill is.
Auto-routing
You don't need to pick a mode. Just say vibe-safe and Claude will detect the right one:
git status shows no staged changes, you're describing a task → BEFORE mode
git diff --staged has content → COMMIT mode
git diff HEAD has unstaged changes → REVIEW mode
"did Claude change more than I asked?" → SCOPE mode
commits ahead of main, ready to open PR → PR mode
conflict markers in any file → CONFLICT mode
Claude just proposed something that feels big → ALARM mode
"what does this flag mean?" / need deeper explanation → EXPLAIN mode
developer reviewing a vibe-coded PR → REVIEWER mode
checking whether past commits contain mistakes → HISTORY mode
`.vibesafe` exists and repo has changed significantly → REFRESH mode
Or invoke directly: vibe-safe before / review / scope / commit / pr / conflict / alarm / history / explain / reviewer / refresh
Repo Configuration: .vibesafe
Two purposes: (1) custom danger/safe zones, (2) developer-defined contracts that vibe-safe enforces for every PM contribution.
# .vibesafe
# ── Zones ────────────────────────────────────────────
danger_zone: src/payments/
danger_zone: infrastructure/
safe_zone: src/components/marketing/
safe_zone: public/images/
# ── Developer contracts ──────────────────────────────
# Set these once. vibe-safe enforces them for every commit from this repo.
require_tests: true # New source files must have test file changes
max_changed_files: 15 # Warn + SCOPE audit if commit exceeds this
require_migration_rollback: true # Migrations must include a down/downgrade/rollback
block_pattern: TODO # Hard stop if TODO appears in added implementation lines
block_pattern: FIXME # Same for FIXME (repeat the key for multiple patterns)
require_reviewer: @alice # Added to every PR description generated by vibe-safe
BEFORE mode generates this file (zones only). Developers add contracts. Once committed, every contributor is held to the same standards automatically.
Mode: BEFORE
Before Claude writes anything. Run this before you describe your task.
Claude runs:
git branch --show-current — if main/master: STOP and fix immediately: Claude runs git checkout -b feature/[3-word-description-from-task]
git log --follow -- <your target files> — establishes who owns this area
- File extension check against Danger Zones +
.vibesafe custom zones
ls .git/hooks/pre-commit — checks whether the mechanical safety hook is installed
Output:
-
Scoped prompt — limits what files Claude is allowed to touch. Use it verbatim.
-
Hook installation — check for an existing pre-commit hook first:
- No existing hook → copy
~/.claude/skills/vibe-safe/hooks/pre-commit to .git/hooks/pre-commit and chmod +x
- Hook already exists → wrap it: create a new hook that runs the existing hook first, then vibe-safe. Do not overwrite. Announce what was wrapped: "Existing hook detected (code-review-graph / husky / custom) — vibe-safe will run after it."
sh .git/hooks/pre-commit.bak "$@"
RESULT=$?
[ "$RESULT" -ne 0 ] && exit "$RESULT"
sh ~/.claude/skills/vibe-safe/hooks/pre-commit
Back up the existing hook to .git/hooks/pre-commit.bak before wrapping.
-
CI installation — the CI workflow file lives at .github/workflows/vibe-safe.yml, which is itself a Danger Zone path. Always treat it this way:
- Do not silently copy it into the current branch
- Always tell the contributor: "The CI workflow file needs a developer to review and merge it —
.github/workflows/ is a danger zone. I'll prepare the file, but it should go in as a separate PR that a developer approves."
- Offer two paths:
- Path A (recommended): Stage just the CI files on the current branch and note in the PR description that a developer needs to review the
.github/workflows/ change specifically
- Path B: Create a separate
chore/install-vibe-safe-ci branch for the CI file, so the contributor's feature work isn't blocked
- Never suggest committing the CI workflow file the same way as
.vibesafe or the hook — those are safe for the contributor to own; the workflow file is not
Post-generation step order — after .vibesafe is written and confirmed, present these in sequence, one at a time:
Next steps:
1. Commit .vibesafe now — this makes the rules live for everyone on the team.
Stage + commit on this branch? Yes / Not yet
2. Install the pre-commit hook (runs locally on every git commit, no Claude needed).
[shows whether existing hook was detected and whether it will wrap or install fresh]
Install now? Yes / Skip
3. Install CI workflow (.github/workflows/vibe-safe.yml).
⚠️ This file is in your danger_zone: .github/workflows/ — a developer needs to
review this before it merges. I'll prepare the file, but don't commit it yourself.
Prepare file for developer review? Yes / Skip
Never present all three at once as a list to action simultaneously. Step 1 is safe for the contributor to own. Step 2 is local and safe. Step 3 always requires developer involvement — say so explicitly, every time, not just when .github/workflows/ is in the detected danger zones.
4. .vibesafe generation — if no .vibesafe exists, Claude asks:
- "What parts of this codebase should a developer always be consulted on?"
- "What areas are definitely yours to edit freely (copy, images, marketing)?"
Then writes the file and shows you what to commit.
For developers setting up a shared repo: Claude reads the repo before asking anything, then proposes contract values with evidence:
Claude runs:
find . -name "*.test.*" -o -name "*_test.*" -o -name "*.spec.*" | head -20
→ Found 47 test files → proposing require_tests: true
git log --oneline -30 | xargs -I{} git diff --name-only {}^ {} | wc -l ÷ 30
→ Average commit touches 7 files → proposing max_changed_files: 12
find . -path "*/migrations/*" | head -10 → then checks each for def down / exports.down
→ 3 migration files, all have rollbacks → proposing require_migration_rollback: true
git grep -rn "TODO" --include="*.ts" --include="*.py" | wc -l
→ 2 TODOs in entire codebase → proposing block_pattern: TODO (low noise)
git log --format="%ae" -50 | sort | uniq -c | sort -rn | head -3
→ alice@co.com reviewed 80% of commits → proposing require_reviewer: @alice
ls .env.example .env.sample 2>/dev/null
→ .env.example exists → env drift check will be active
Discovery runs silently, then Claude produces one document to paste into Slack or email. Discovered rules appear with confirmation prompts; open questions cover the rest:
Hi — setting up a safety tool before I commit code.
Can you confirm what I found and answer a few questions?
FOUND IN REPO:
✅ require_tests — found 47 test files
Keep? Yes / No / Only for: ___
✅ max_changed_files: 12 — avg 7 files per commit in git history
Keep 12? Yes / Lower to: ___
✅ require_migration_rollback — all 3 existing migrations have a down()
Keep? Yes / No
✅ block_pattern: TODO — only 2 TODOs in entire codebase
Keep? Yes / No / Add more: ___
✅ require_reviewer: @alice — top committer across src/
Right person? Yes / Change to: ___
QUESTIONS:
1. Any folders I should never touch without you?
(already flagging: migrations/, src/auth/ — anything else?)
→
2. Areas that are definitely mine to edit freely?
(e.g. marketing copy, images, public/)
→
3. Anything else I should know before I start committing?
→
PM pastes the answers back. Claude interprets the answers using the rules below, then shows a preview of .vibesafe before writing it.
Answer interpretation rules — apply these when processing the questionnaire responses before writing .vibesafe:
Rule 1 — Philosophy ≠ decision. Re-ask as a binary.
If the PM answers a yes/no question with an explanation, philosophy, or constraint instead of a clear yes/no, do NOT interpret it and move on. Re-ask as a forced choice:
"Got it — so should I [enforce X as a hard rule / just flag X for a developer to review / skip X entirely]?"
Examples of philosophy answers that need re-asking:
- "migrations should be idempotent" → ask: "So enforce
require_migration_rollback: true? Yes / No / Flag only"
- "it's about coverage not test count" → see Rule 3
- "changes should be scoped" → ask: "Should I keep max_changed_files at 12, or would a different number fit better? Keep 12 / Lower to: __ / Raise to: __"
Rule 2 — Rejection without replacement. Ask who/what instead.
If the PM rejects a proposed value without providing a replacement, don't drop the rule entirely. Ask for the replacement:
- "naaaah" to
require_reviewer: @alice → ask: "Who should review PRs then? One person, or multiple? Name(s): ___"
- "not that" to a danger zone → ask: "What should I use instead?"
Rule 3 — "Maybe" or vague approval. Confirm interpretation before writing.
If the PM says "maybe", "probably", "I guess", or gives conditional approval, state your interpretation and ask for confirmation before writing:
"I'll treat that as yes and add admin.py to danger_zone — correct? Yes / No"
Never silently interpret a maybe as a yes or a no.
Rule 4 — "No idea" on a technical component. Offer a safe default.
If the PM doesn't know whether a component is load-bearing (Celery worker, middleware, DI path, background job), offer:
"No idea is fine — safest approach is to add it to danger_zone so it always gets a developer review. Do that? Yes / Skip"
Unknown territory defaults to flagged, not ignored.
Rule 5 — Requested rule beyond vibe-safe's capability. Be honest, offer what's possible.
If the PM asks for a rule vibe-safe can't mechanically enforce, say so and offer the closest thing:
- "TODO only allowed behind a feature flag" → "I can't check for feature flags automatically — that requires code understanding, not grep. I can hard-block TODO everywhere in implementation files, which means Claude must not write TODOs at all. Your developer would verify the feature-flag rule in code review. Block TODO everywhere? Yes / No"
- "tests must maintain coverage" → "Two separate things: I can enforce that every source change includes a test file in the PR (presence). I can't measure live coverage — that's your CI pipeline's job (
--cov-fail-under=N, codecov, etc.). But if Claude lowers that threshold to make CI pass, I will catch that — same as I catch jest/codecov threshold drops. So: I enforce test presence in PRs, your CI enforces the coverage floor, and I guard against the floor being quietly lowered. Sound right? Yes / No"
Rule 6 — Jargon the PM doesn't recognize. Explain before proceeding.
If a question uses technical terms and the PM responds with "what does that mean?" or equivalent confusion, explain it in one sentence before asking again:
"Claude model routing means your repo has configuration that controls which AI model is used for different tasks. Should changes to those config files require a developer's review? Yes / No / What files?"
Never re-ask the jargon-heavy version.
Rule 7 — Preview before write.
After processing all answers, before writing .vibesafe, show the PM what will be written:
Here's what I'll write to `.vibesafe`:
danger_zone: backend/alembic/
danger_zone: backend/app/routers/auth.py
...
require_tests: true
max_changed_files: 12
block_pattern: FIXME
require_reviewer: @team-lead
Anything to change before I create this file? Yes / Looks good
Only write after explicit confirmation. This is the last checkpoint before the config is locked.
Stack-aware questions — before sending the questionnaire, Claude runs:
ls package.json Gemfile requirements.txt Cargo.toml go.mod pom.xml 2>/dev/null
and adds one targeted section based on what it finds:
| Stack detected | Extra questions added |
|---|
package.json + React/Next.js | "Do you have server-side API routes I shouldn't touch? Any SSR/ISR pages with special caching?" |
package.json + Express/Node API | "Any rate-limiting or auth middleware paths? Background job queues?" |
Gemfile (Rails) | "Any Mailers, Sidekiq workers, or background jobs? Admin panel at /admin?" |
requirements.txt / pyproject.toml (Django/FastAPI) | "Any Celery tasks, admin customizations, or Django signals?" |
go.mod | "Any gRPC service definitions or generated protobuf files I shouldn't edit?" |
Cargo.toml | "Any FFI bindings or unsafe blocks that need special review?" |
Stack-specific danger zones are also auto-suggested (e.g., danger_zone: pages/api/ for Next.js, danger_zone: app/jobs/ for Rails).
Mode: REFRESH
Run when .vibesafe already exists but the repo has evolved and the rules may be stale.
Claude re-runs the same discovery as BEFORE mode (test file count, average commit size, migration patterns, TODOs, top committers) and diffs the findings against the existing .vibesafe:
Output — three sections:
UNCHANGED (still accurate):
require_migration_rollback: true — 5 migration files, all have rollbacks ✅
DRIFTED (rule exists but evidence changed):
max_changed_files: 12 → evidence now suggests 18 (avg commit grew from 7 → 11 files)
Proposed update: max_changed_files: 18
Keep current / Update / Remove?
NEW (found in repo, not yet in .vibesafe):
block_pattern: FIXME — found 0 FIXMEs in codebase, low noise
Add? Yes / No
MISSING (in .vibesafe, no longer evidence-backed):
require_reviewer: @alice — alice@co.com has only 3 commits in last 90 days (was top reviewer)
New top committer: bob@co.com (67% of recent commits)
Update to: @bob? Yes / No / Keep @alice
Claude proposes only changes — if a rule is still accurate it stays untouched. PM sends the diff to the developer, gets sign-off, Claude applies the updates to .vibesafe and commits.
Mode: REVIEW
After Claude writes code, before you commit.
Claude runs:
git diff HEAD for unstaged changes
git grep -nE "sk-|pk_|ghp_|AKIA|api_key[[:space:]]*=|secret[[:space:]]*=|password[[:space:]]*=|Bearer |token[[:space:]]*=" across ALL tracked files — credentials live in files you didn't intend to change and won't appear in your diff
What gets flagged with evidence:
| Signal | Example flag |
|---|
| Credential pattern | src/api/client.ts:14 — const API_KEY = "sk-proj-abc123" ⛔ Permanent in git history even if deleted |
| File deletion | src/utils/helper.ts deleted — Claude may be wrong that it's unused |
| Config file modified | config/nginx.conf — controls production traffic for everyone |
| Migration file present | db/migrations/*.sql — irreversible schema change |
| Auth-related file | src/auth/session.ts — security surface, needs developer eyes |
| Scope creep | Files modified that weren't in your original target |
| Quality gate weakening | jest.config.ts: threshold 80 → 60 ⛔ Claude fixed the check, not the bug |
| Error suppression | src/api/client.ts:23 — // @ts-ignore ⛔ Type error hidden, not fixed |
| Linter suppression | src/Form.tsx:7 — // eslint-disable-next-line ⛔ Lint error hidden, not fixed |
| Test bypass | auth.test.ts:45 — it.skip("validates token"...) ⛔ Failing test skipped, not fixed |
| Test deletion | tests/payment.test.ts deleted ⛔ Entire test file removed to make suite pass |
| Debug output | src/Form.tsx:12 — console.log(userData) ⛔ Logs production data, may expose PII |
| Empty catch block | src/api/client.ts:67 — catch (e) {} ⛔ Errors silently swallowed |
| Lock file drift | package-lock.json changed, package.json unchanged — undocumented dependency change |
| New dependency | package.json: "lodash" added — check license, security, bundle size |
| Binary/large file | assets/video.mp4 staged ⛔ Bloats git history permanently |
| PII in diff | seeds/users.ts:4 — email: "john.doe@acme.com" ⛔ Real data committed |
| Internal hostname | config/api.ts:2 — baseURL: "http://api.internal:8080" ⛔ Infrastructure exposed |
| Force push proposed | Claude suggested git push --force → ALARM immediately |
| XSS sink added | dangerouslySetInnerHTML added — direct XSS attack surface |
| Code injection | eval( added — arbitrary code execution risk |
| SSL disabled | verify=False / NODE_TLS_REJECT_UNAUTHORIZED=0 / rejectUnauthorized: false — Claude "fixed" a cert error by disabling verification |
| CORS wildcard | origin: '*' / Access-Control-Allow-Origin: * — API open to any domain |
.gitignore entries removed | Previously ignored files (possibly secrets) now tracked and will be committed |
| Timing hack | setTimeout/sleep/time.sleep with hardcoded value — Claude papered over a race condition |
| TypeScript type erasure | : any / as any added — Claude escaped the type system instead of fixing types |
| Debug mode in config | DEBUG = True / debug: true in non-test config — debug mode left on |
| TODO/FIXME stub | throw new Error("TODO") or // TODO: implement in new code — Claude left a placeholder |
| Commented-out code | // const user = await getUser(id) — working code disabled, Claude may have been unsure |
| Private key file staged | server.pem, id_rsa, client.p12 staged ⛔ Binary credential — not caught by text grep |
rm -rf in script | rm -rf $DIR/* in committed shell script ⛔ Aggressive cleanup Claude wrote without knowing prod paths |
| String thrown, not Error | throw "something failed" — loses stack trace, bugs become undiagnosable in production |
Every flag ends with a plain-English explanation of the worst-case consequence and a specific action.
Mode: COMMIT
Before git commit. For fixable problems, Claude executes the fix — you don't need to know the git commands.
Five automated checks, each with remediation:
-
Credential scan — git grep -nE "sk-|pk_|ghp_|AKIA|api_key[[:space:]]*=|secret[[:space:]]*=|password[[:space:]]*=|Bearer |token[[:space:]]*=" on ALL tracked files
- Staged file contains credential → Claude runs
git restore --staged <file>, explains the key must be rotated even after removal
- Untracked file contains credential → flag only (Claude cannot unstage what isn't staged)
-
Danger Zone audit — staged files vs. default list + .vibesafe custom zones
- Danger Zone file staged → Claude runs
git restore --staged <file> and tells you what to ask a developer to apply instead
- Quality gate file staged (
jest.config.*, vitest.config.*, codecov.yml, .nycrc, pytest.ini, setup.cfg, pyproject.toml, .coveragerc, sonar-project.properties) → Claude reads the diff and checks whether any numeric threshold decreased. If a number dropped (coverage %, error limit, score floor), flag as quality gate weakening: "Claude may have fixed the failing check by lowering the bar, not by fixing the code."
--cov-fail-under drop in ANY changed file (including CI YAML, Makefile, tox.ini) → same flag. This catches coverage thresholds living in workflow files rather than pytest config.
-
Deletion audit — any files being removed?
- File deleted → flag: "Claude may be wrong that this is unused. Confirm with a developer before this commit."
-
Branch check — git branch --show-current
- On main/master → Claude runs
git checkout -b feature/[3-word-slug-from-diff], then re-runs all checks on the new branch
-
Scope check — staged file list vs. your stated intent
- Out-of-scope file → Claude runs
git restore --staged <file> after your confirmation
Code health checks — Claude runs these on the staged diff (git diff --cached), scanning only added lines (grep "^+"):
-
Suppression scan — @ts-ignore, @ts-nocheck, eslint-disable, @ts-expect-error added
- Found → flag with line; Claude does not auto-remove (may be intentional), but requires explanation before proceeding
-
Test bypass scan — .skip(, xit(, xdescribe(, x.test( added
- Found → flag: "Claude bypassed a failing test instead of fixing it"
-
Debug artifact scan — console.log, console.error, console.warn, debugger added
- Found in non-test file → flag: "Debug output left in production code"
-
Empty catch scan — catch\s*\(.*\)\s*\{\s*\} or catch\s*\{\s*\} added
- Found → flag: "Errors silently swallowed — failures invisible in production"
-
Lock file drift — package-lock.json/yarn.lock/pnpm-lock.yaml staged without package.json, or vice versa
- Found → flag: "Manual lock file edits are almost always wrong"
-
Binary/large file scan — git diff --cached --numstat for lines showing - - <filename> (binary)
- Found → flag: "Binary files bloat git history permanently and cannot be removed cleanly"
-
PII scan — added lines matching [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} (email) or \+?[0-9]{10,} (phone)
- Found → flag: "Real personal data in committed code — check if this is test fixture or real user data"
-
Internal hostname scan — added lines matching localhost:[0-9]+, 192\.168\., 10\.[0-9]+\., \.internal\b, \.corp\b
- Found → flag: "Internal infrastructure URL committed — exposes network topology"
-
New dependency check — git diff --cached package.json shows new entry in dependencies or devDependencies
- Found → surface: name, check if it's a known package, flag for license and security review
-
Force push guard — if Claude ever proposes git push --force or git push -f at any point in the session: route immediately to ALARM mode
-
XSS sink scan — dangerouslySetInnerHTML, innerHTML =, document.write( added
- Found → flag: "Direct XSS attack surface — any user-controlled input reaching this is a vulnerability"
-
Code injection scan — eval( added (JS/Python/Ruby)
- Found → flag: "Arbitrary code execution risk — almost never the right solution"
-
SSL bypass scan — verify=False, NODE_TLS_REJECT_UNAUTHORIZED, rejectUnauthorized: false, ssl_verify: false added
- Found → flag: "Claude disabled certificate verification to fix a cert error — this silently removes all TLS protection"
-
CORS wildcard scan — origin: '*', Access-Control-Allow-Origin: *, cors({ origin: true }) added
- Found → flag: "API now accepts requests from any domain — check if this is intentional"
-
.gitignore regression scan — git diff --cached .gitignore | grep "^-" for removed entries
- Lines removed → flag: "Previously ignored files are now tracked — if any are secrets, they'll be committed on the next add"
-
Timing hack scan — setTimeout(, sleep(, time.sleep(, asyncio.sleep( with a hardcoded numeric argument added
- Found → flag: "Hardcoded delay — Claude may have papered over a race condition rather than fixing the root cause"
-
Type erasure scan — : any or as any added (TypeScript files only)
- More than 2 occurrences in one diff → flag: "Claude may have escaped the type system to avoid fixing type errors"
-
Debug mode scan — DEBUG = True, debug: true, APP_ENV=development added in non-test config files
- Found → flag: "Debug mode in a non-test file — check this isn't heading to production"
-
TODO/FIXME stub scan — throw new Error("TODO"), raise NotImplementedError, // TODO, # FIXME in implementation files (not .md, not test files)
- Found → flag: "Claude left a placeholder instead of implementing — this will fail at runtime"
-
Commented-out code scan — added lines matching // <code-keyword> or # <code-keyword> where keyword is const, let, var, function, return, import, export, if, for, while, class, def, async
- Found → flag: "Claude commented out working code — may indicate uncertainty about the change"
-
Private key file scan — git diff --cached --name-only for *.pem, *.key, *.pfx, *.p12, *.jks, id_rsa, id_ed25519, *.crt, *.cer
- Found → STOP: "Binary credential file staged — not caught by text grep. Private keys in git history are compromised permanently."
-
Destructive command scan — rm -rf in staged .sh, Makefile, .github/workflows/, or any script file
- Found → flag: "Claude wrote an aggressive delete command — verify the path is not production data before committing"
-
String throw scan — throw " or throw ' (not throw new) in JavaScript/TypeScript files
- Found → flag: "String thrown instead of Error object — stack trace is lost, this bug will be undiagnosable in production"
-
.env.example drift — added lines matching process.env., os.environ[, getenv(, import.meta.env., ENV[ in non-test files
- If
.env.example (or .env.sample) not also modified → flag: "New environment variable referenced in code but not documented in .env.example — other developers won't know to set it and the app will silently fail"
-
Migration rollback check — if any staged file is in migrations/, db/migrate/, or matches *_migration.*
- Claude reads the file and checks for
def down, def downgrade, exports.down, down(, rollback(, -- Down
- Not found → flag: "Migration has no rollback — this schema change cannot be undone automatically"
- Note: Django migrations auto-generate reversals — skip this check for files in
*/django/*/migrations/ or containing from django.db import migrations
- If
.vibesafe require_migration_rollback: true → hard stop instead of flag
-
Test coverage check — git diff --cached --name-only for new source files (.ts, .js, .tsx, .jsx, .py, .rb, .go, .rs) that are not test files and not config/markdown
- Check if any test files (
*.test.*, *_test.*, *_spec.*, files in tests/ or __tests__/) also changed
- New functions/classes added (grep
^+.*function |^+.*class |^+.*def |^+.*=>) but zero test files staged → flag: "New code with no test changes — Claude may not have written tests for this"
- If
.vibesafe require_tests: true → hard stop instead of flag
- Exempt: commits where all staged files are non-source (
.md, .txt, .yaml, .json, images, .css, .html)
-
Developer contract enforcement — read .vibesafe for contract keys:
max_changed_files: N — count staged source files; if exceeded: warn, surface SCOPE mode, list the unexpected files
block_pattern: WORD — grep added lines (excluding test files and .md) for each pattern; hard stop if found: "Developer contract: WORD not allowed in committed code"
require_tests, require_migration_rollback — enforced in checks 31 and 30 above
require_reviewer: @handle — stored; added to PR description in PR mode
-
Insecure random in security file — Math.random() added in files named auth, token, session, jwt, oauth, login, credential, password
- Found → STOP: "Math.random() is not cryptographically secure — use crypto.getRandomValues() or crypto.randomUUID() for auth/session tokens"
-
SQL string interpolation — added lines matching "SELECT" + , f"SELECT, `SELECT ${, "INSERT" +, f"INSERT, "UPDATE" +, f"UPDATE
- Found → STOP: "SQL built by string concatenation is vulnerable to injection — use parameterized queries or a query builder"
-
Shell injection via subprocess — shell: true (JS/TS) or shell=True (Python) added in subprocess/exec calls
- Found → STOP: "shell=True passes the command through the shell — unsanitized user input reaching this is command injection"
-
Missing await on async call — added lines matching = db., = await expected but await absent, or assignment from a known async pattern (.findOne(, .query(, .fetch(, .get(, .post() without await
- Found → flag: "Result is likely a Promise object, not the actual value — add await or the call is silently a no-op"
-
Test block with no assertions — it( / test( / describe( block added with no expect(, assert, should, toBe, toEqual, toHaveBeenCalled inside
- Found → flag: "Test added with no assertions — it will always pass regardless of what the code does"
-
gitleaks scan (if installed) — run gitleaks detect --source . --no-git on staged files
- Not installed → silently skip (grep-based credential scan still runs)
- Installed + finds credential → STOP: "gitleaks detected a high-entropy secret — rotate immediately, even if it looks like a placeholder"
-
semgrep scan (if installed) — run semgrep --config=p/owasp-top-ten --quiet on staged files
- Not installed → silently skip
- Installed + finds match → STOP: "semgrep flagged an OWASP rule match — [rule id and file:line cited]"
-
npm audit (if installed and package.json staged or present) — run npm audit --audit-level=high --json
- Not installed → silently skip
- Installed + high/critical CVE found → STOP: "npm audit found [N] high/critical vulnerabilities — review before committing"
-
API key in client-side file — credential patterns (sk-*, pk_live_, AKIA*, api_key=) in files under src/, frontend/, public/, static/, client/, components/, pages/, app/
- Found → STOP: "Credential in client-side file — visible to anyone who opens DevTools. Move to a server-side environment variable or backend proxy."
-
Sensitive data in log statements — console.log(, logger.info(, print(, logging.debug( on lines that also reference password, secret, token, api_key, credential, auth
- Found → flag: "Possible sensitive data in log output — scrub before production"
-
Security header regression — helmet import removed from a server file, or explicit insecure header config added (Content-Security-Policy: unsafe-eval, X-Frame-Options: ALLOW-FROM, Strict-Transport-Security: max-age=0)
- Found → flag: "Security middleware removed or insecure header configured"
-
New route without rate limiting — app.get(, app.post(, @app.route(, @router.get( added in a file with no rateLimit, throttle, slowapi, limiter, RateLimit reference anywhere in the file
- Found → flag: "New endpoint has no rate limiting — add before exposing to traffic"
When all checks pass: Claude generates the commit message from the diff + your one-line description. You confirm or edit, then Claude runs git commit -m "..." for you.
Mode: PR
Before opening a pull request.
Claude reads git diff main...HEAD and asks one question: "Why are you making this change?"
Then generates a complete PR description:
- What changed — derived from the diff, plain English
- Why — your answer
- What to test — derived from file types and risk level
- Flagged uncertainties — unresolved review flags surface here
- Who should review — from git log of changed files +
require_reviewer from .vibesafe
Also checks: are you targeting the right base branch? Is your branch up to date?
PR safety artifact — appended to the PR description body so the reviewer sees what was verified:
---
🛡️ vibe-safe — checked [date]
| Check | Result |
|-------|--------|
| Credential scan | ✅ Clean |
| 36 commit checks | ✅ Passed |
| Scope audit | ✅ 4 files, all in scope |
| Test coverage | ⚠️ 2 new functions, no test changes (acknowledged) |
| .env.example drift | ✅ No new env vars |
| Migration rollback | ✅ N/A |
| Developer contracts | ✅ All team rules met |
[vibe-safe](https://github.com/googlarz/vibe-safe)
Any warning acknowledged during COMMIT shows as ⚠️ with "(acknowledged)" so the reviewer knows it was seen, not missed.
Mode: SCOPE
Run when you think Claude may have changed more than you asked for, or when max_changed_files fires.
Claude asks: "What was the original task in one sentence?"
Then runs git diff --name-only HEAD (or git diff --name-only main...HEAD if commits ahead) and evaluates each changed file:
Output:
- ✅ IN SCOPE — directly related to the stated task
- 🔍 LIKELY NEEDED — indirectly required (shared config, auto-updated lock files, framework boilerplate)
- ⚠️ SUSPICIOUS — changed, but the connection to the stated task is unclear
- 🚨 OUT OF SCOPE — clearly unrelated; Claude wandered
For each suspicious or out-of-scope file: Claude explains what changed in plain English and asks "keep it or remove it?" For removes, Claude runs git restore <file>.
.vibesafe max_changed_files sets the soft ceiling that triggers automatic SCOPE routing.
Mode: CONFLICT
When merge conflict markers appear.
Claude reads <<<<<<< / ======= / >>>>>>> and tells you:
- What the current version does (plain English)
- What your change does (plain English)
- What specifically is lost if you accept either side
Never recommends "accept all theirs/ours." If the conflict is in a Danger Zone file: STOP — CALL A DEVELOPER.
Mode: ALARM
When Claude proposes something that feels big, irreversible, or outside scope.
Claude assesses three things:
- Reversible? Can this be undone with a single git revert?
- Shared? Does it affect infrastructure other teams depend on?
- Expected review? Would a developer expect to approve this first?
Output: GO AHEAD / PAUSE AND CHECK / STOP — CALL A DEVELOPER
Auto-escalate to ALARM — no questions needed:
- Claude proposes
git push --force or git push -f for any reason
- Claude proposes
git commit --no-verify for any reason — this skips all 21 hook checks
- Claude proposes running a direct database command (
psql, rails db:, knex migrate)
- Claude proposes modifying
.github/workflows/ to skip a failing check
Mode: HISTORY
Scans recent git history for past mistakes that need active remediation — not just the current state.
Claude runs:
git log -S "sk-|pk_|ghp_|AKIA|api_key|secret|password" --oneline -50 — finds commits that added or removed credential patterns. A key "deleted" in commit B is still readable in commit A.
git log --grep="fix\|remove\|revert\|oops\|secret\|key\|credential\|token" --oneline -20 — finds commits whose messages hint at past cleanup attempts
git log --diff-filter=D --name-only --format="" -30 — finds files deleted in recent commits (possible sensitive data removal)
For each flagged commit, Claude distinguishes:
- Active exposure — credential still present in HEAD (STOP: rotate immediately)
- Historical exposure — removed in a later commit but still readable in history (requires key rotation +
git filter-repo to scrub, plus force-push and re-clone for all team members)
- Likely clean — no pattern found
Historical exposure output includes exact remediation steps: rotate the key, run git filter-repo, force-push, notify anyone who cloned.
Mode: EXPLAIN
When a vibe-safe flag fires and you don't understand what it means or what to do about it.
Invoke: vibe-safe explain <topic> — e.g., vibe-safe explain credential, vibe-safe explain ssl, vibe-safe explain ts-ignore, vibe-safe explain empty catch
Claude provides:
- What it is — plain English, no jargon
- Why it matters — concrete worst-case scenario in non-technical terms
- What it looks like when it goes wrong — a real example
- The proper fix — not just "don't do this" but what Claude should have done instead
- One sentence for your developer — how to communicate the risk without needing to explain the full technical detail yourself
Mode: REVIEWER
For developers reviewing a PR opened by a non-technical contributor using Claude.
When to use: You're reviewing a PR where the author isn't a developer. You want to understand what they intended vs. what Claude actually changed — and what needs your attention — without reading the full diff yourself.
Run:
vibe-safe reviewer
What Claude does:
- Read the diff —
git diff main...HEAD (full diff, not just staged)
- Infer intent — read commit messages, branch name, PR description if available. Ask "what was this person trying to accomplish?" If intent is ambiguous, ask the reviewer: "What did the contributor say they were changing?"
- Map intent vs. reality — classify every changed file:
- ✅ IN SCOPE — directly implements the stated intent
- 🔶 LIKELY NEEDED — supporting change that intent probably required (e.g., types update alongside component change)
- ⚠️ SUSPICIOUS — changed but not obviously connected to intent — flag for explanation
- ❌ OUT OF SCOPE — unrelated to stated intent, no plausible connection
- Run vibe-safe checks — mentally apply all COMMIT mode checks to the diff. Report any that fire.
- Identify test gaps — which changed source files have no test changes? Which have no test files at all in the repo?
- Generate questions — specific, answerable questions the developer should ask or verify before approving.
Output format:
## vibe-safe REVIEWER report
**Intent (inferred):** [what the contributor was trying to do]
**Branch:** [branch name] **Files changed:** [N] **Commits:** [N]
### File verdict
| File | Verdict | Reason |
|------|---------|--------|
| src/components/Hero.tsx | ✅ IN SCOPE | hero text updated |
| src/styles/global.css | 🔶 LIKELY NEEDED | font-size change adjacent to text edit |
| src/api/analytics.ts | ⚠️ SUSPICIOUS | tracking call added — not mentioned in intent |
### vibe-safe checks
[List any patterns that fired, same format as COMMIT mode. If none: "No safety flags."]
### Test coverage
- [file]: no test changes alongside source change
- [file]: no test file exists in repo — consider creating one
### Questions to ask / verify before approving
1. [specific question about a suspicious file or pattern]
2. [specific question about intent vs. implementation mismatch]
3. [specific question about test coverage if critical path]
### Verdict
**APPROVE** / **REQUEST CHANGES** / **DISCUSS WITH CONTRIBUTOR**
[1-2 sentence summary of what the reviewer should do next]
Rules for verdict:
- Any hard-block pattern (credential, XSS, SSL bypass, SQL injection, etc.) → REQUEST CHANGES immediately, don't soft-pedal it
- OUT OF SCOPE files with no explanation → DISCUSS WITH CONTRIBUTOR — Claude may have gone further than asked
- Only IN SCOPE / LIKELY NEEDED files, no flags → APPROVE (with any test gap notes)
- Suspicious files with plausible explanation → APPROVE with comment
What REVIEWER mode is NOT:
- Not a substitute for understanding the business logic
- Not a guarantee the code is correct — only that it doesn't have obvious safety failures and matches stated intent
- Not for reviewing developer-written PRs — use a code review tool for that
What vibe-safe can't catch — and how to handle it
Three categories of risk that grep cannot cover. When they come up, say what the limitation is and give the concrete next step — don't just say "talk to a developer."
API response data exposure
Why vibe-safe can't catch it: Whether a response leaks sensitive fields depends on what the serializer/schema includes — and that requires understanding the data model, not just reading added lines.
What to do:
-
Use allowlist serializers, not raw model returns. The pattern return await db.user.findFirst(...) directly in a route handler is the root cause. The fix is always a schema/serializer that explicitly names the fields to expose:
- FastAPI:
response_model=UserPublic on the route decorator
- Django REST: a
serializer_class that explicitly lists fields
- Rails:
render json: user.as_json(only: [:id, :name]) not render json: user
- Prisma/Node:
select: { id: true, name: true } not returning the full object
-
Audit new route handlers before merge. When a new endpoint is added, the developer reviewing it should ask: "does the response go through a schema that explicitly allowlists fields, or is it returning a raw DB object?" This is what REVIEWER mode surfaces.
-
Runtime scan before launch. Tools: OWASP ZAP (free), Burp Suite, or a simple curl against your staging API checking that sensitive fields (password_hash, internal_id, stripe_customer_id, ssn, dob) don't appear in responses. Run this as part of your staging checklist, not just pre-commit.
-
Add a test that asserts response shape. The most durable protection is a test that explicitly asserts which fields appear in the response and fails if new ones are added without review.
Privacy policy
Why vibe-safe can't catch it: Whether you need a privacy policy depends on what data you collect, where your users are, and which regulations apply — none of which are in the code.
What to do:
-
The trigger is collecting personal data, not shipping code. If any of these exist in your models or API payloads — email, name, phone, ip_address, location, user_agent, device_id, date_of_birth, behavioral tracking — you are collecting personal data and need a policy before you expose this to real users.
-
Jurisdiction determines the requirement:
- EU/EEA users → GDPR applies. You need a privacy policy, a lawful basis for processing, and data subject rights (access, deletion, portability).
- California users → CCPA applies if you meet the thresholds (>$25M revenue, >50k users, or >50% revenue from selling data).
- UK → UK GDPR (same substance as EU GDPR post-Brexit).
- When in doubt: write the policy anyway. The cost of writing one is far less than the cost of not having one when you need it.
-
Fast path for early-stage products: iubenda, Termly, or GetTerms.io generate compliant policies for ~$10/month. Use one of these. Do not write your own from scratch or copy one from another site.
-
What a developer can do now: Before adding any new PII field to a model, answer: "what's the purpose, retention period, and who has access?" Document it. This becomes the basis for the privacy policy and also forces the right design conversation.
Data storage architecture
Why vibe-safe can't catch it: Where data ends up, who can access it, and whether it's encrypted at rest requires understanding infrastructure — not reading diffs.
What to do:
-
Map it before you build it. Before adding a new data entity, answer four questions:
- Where is it stored? (Postgres table, S3 bucket, Redis cache, CloudWatch log, third-party API)
- Who can read it? (application service role, DBA, DevOps, third-party vendor)
- How long is it kept? (forever, 90 days, deleted on account close)
- Is it encrypted at rest? (database-level, field-level for PII, or not at all)
If you can't answer these, the feature isn't ready to build.
-
Logs are a data store. Whatever goes into console.log, logger.info, or your observability platform (Datadog, Sentry, CloudWatch) is stored, queryable, and often retained for months. Treat log destinations as data stores with the same access controls.
-
Third-party SDKs are data stores. When you add Segment, Mixpanel, Intercom, Sentry, FullStory, or similar: you are sending user data to their infrastructure. Check what each SDK collects by default, whether it can be configured to exclude PII, and whether their DPA (Data Processing Agreement) covers your jurisdiction.
-
The one question to ask before any new integration: "Does this send user data off our infrastructure, and if so, what data, to whom, and under what terms?" This question should be answered before the SDK is added to package.json, not after it's in production.
STOP — CALL A DEVELOPER (non-negotiable)
- Any file in
migrations/, schema/, db/ is in the diff
- Any CI/CD config (
.github/workflows/, Jenkinsfile, .circleci/)
- Any environment config (
.env, docker-compose.*, nginx.conf, *.conf)
- Any auth-related file (
auth, login, session, jwt, oauth, permissions)
- Branch is
main or master
- Claude proposed running a database command
- Credentials found anywhere in modified files
PAUSE AND CHECK
- Diff is larger than you expected
- Files outside your intended scope appear
- A file you've never seen is being modified
- Claude said "this should be safe" but you don't understand why
GO AHEAD
- Only your intended files in the diff
- No credential flags in any modified file
- You're on a feature branch
- A developer has seen the plan
Danger Zones
Files that need developer involvement regardless of change size:
| Category | Patterns |
|---|
| Environment | .env, .env.* |
| Infrastructure | nginx.conf, *.conf, Dockerfile, docker-compose.* |
| CI/CD | .github/workflows/, Jenkinsfile, .circleci/, .gitlab-ci.yml |
| Database | migrations/, schema/, *.sql |
| Auth | files named: auth, login, session, jwt, oauth, permission, role |
| Build | webpack.config.*, vite.config.*, tsconfig.json |
| Quality gates | jest.config.*, vitest.config.*, codecov.yml, .nycrc, .eslintrc.*, .stylelintrc.*, sonar-project.properties |
| Dependencies | package.json, Gemfile, requirements.txt (version changes) |
Common Rationalizations — Red Flags
| Thought | What it actually means |
|---|
| "Claude said it was fine" | Claude doesn't know your codebase's undocumented dependencies |
| "It's not in my diff" | Credentials can exist in files you didn't intend to change |
| "It's just a small change" | File type matters more than size — one line in nginx.conf hits production |
| "I'll fix it in the next commit" | Committed credentials are compromised permanently, even if deleted |
| "I accepted 'theirs' everywhere" | You may have silently reverted your own work |
| "The tests still pass" | Tests don't cover what they don't test |
| "It's just a config tweak" | Config files control production for everyone on the team |
| "I've done this before" | Past safety was luck, not process |
| "Claude made the failing checks pass" | Check what it changed to make them pass — lowering a threshold is not fixing a bug |
| "It's just a ts-ignore, it'll be fine" | Suppressed errors accumulate into unfixable technical debt |
| "I'll remove the console.log later" | Later doesn't happen; debug logs leak data in production |
| "The test was probably out of date anyway" | Claude skipped it because it was failing, not because it was wrong |
| "It's just push --force on my branch" | If anyone else pulled that branch, their work is gone |
| "verify=False is just for testing" | It was in the diff — check if it's going to production |
| "The any type is fine for now" | Type debt compounds; Claude added it to avoid fixing the real error |
| "The sleep just makes it more reliable" | Timing hacks hide bugs and make tests slow and flaky |
| "I'll implement that part later" | Claude shipped a placeholder — it will throw at runtime |
| "That code was probably dead anyway" | Commented-out code means Claude wasn't sure — don't ship uncertainty |
| "The rm -rf is scoped to a temp dir" | Verify that before it runs in CI against a production path |
| "throw is throw, same thing" | String throws lose the stack trace — production bugs become invisible |
| "The hook keeps failing so I'll use --no-verify" | Skipping the hook skips all checks — find out why it's failing instead |
| "It's fine, I checked git history and deleted it" | Deleted ≠ gone — run HISTORY mode to confirm and get remediation steps |
| "Claude only changed 3 files, it's fine" | File count isn't scope — run SCOPE mode to verify intent matches diff |
| "The migration is simple, no rollback needed" | Every irreversible migration needs a down path — that's the whole point |
| "I'll add tests later" | Later doesn't happen; require_tests exists so your developer doesn't have to say this in code review |
| "It uses the env var everywhere already" | New references still need .env.example entries — the next dev spinning up won't know to set it |
Mode: VERIFY
Invoke after any vibe-safe session to confirm the session is clean.
Claude runs:
git branch --show-current — confirms not on main/master
git diff main...HEAD --name-only — lists every file that will be in the PR
git grep -nE "sk-|pk_|ghp_|AKIA|api_key[[:space:]]*=|secret[[:space:]]*=|password[[:space:]]*=|Bearer |token[[:space:]]*=" — final credential sweep
git log --oneline -3 — confirms commit messages are descriptive
Output: CLEAN or remaining flags with specific file:line evidence.