name: vibe-coding-guardrails description: Fast post-coding scan that catches common anti-patterns introduced during rapid AI-assisted coding sessions. Runs pattern-matching checks against project conventions before code leaves the working tree. argument-hint: "[scope] [options]" user_invocable: true
Vibe-Coding Guardrails
Fast, automated convention-compliance scan for changes in the working tree. Catches the mistakes that AI coding assistants commonly introduce — wrong i18n patterns, CSS violations, accessibility gaps, security anti-patterns, and project-specific convention breaks — before they reach review or deployment.
Design goal: Complete in under 60 seconds. No builds, no tests, no servers — pure static pattern matching on changed files.
Scoping
Parse the user's arguments:
- No arguments: scan all uncommitted changes (
git diff --name-only HEAD+ untracked files). - App target:
/vibe-coding-guardrails apps/citizen— restrict scan to files under that directory. - File list:
/vibe-coding-guardrails src/Dashboard.tsx src/Login.tsx— scan specific files only. --staged: scan only staged files (git diff --cached --name-only).--all: scan entire codebase, not just changed files (slower but thorough).
Options:
no-fix— report findings only, do not apply fixes.fix— auto-fix trivially fixable violations (default: report only).strict— treat P2 findings as blockers (default: only P0/P1 block).
Operating Rules
- Speed over depth: this is a fast guardrail, not a full review. Prefer false negatives over slow execution.
- Evidence-first: every finding must cite
file:lineand the matched text. - Changed-files focus: by default, only scan files that have uncommitted changes. This keeps the scan fast and relevant.
- No builds: never run
npm run build,tsc, orvite build. Userg/grep/globonly. - No network: never
curl,fetch, or start servers. - Fix-forward: when
fixmode is active, apply the simplest correct fix. If the fix is ambiguous, report instead of guessing. - One pass: do not re-scan after fixes. Report what was found and what was fixed in a single pass.
- Deduplication: if the same pattern appears N times in one file, report once with count, not N separate findings.
Severity Definitions
| Level | Meaning | Examples |
|-------|---------|---------|
| P0 | Breaks compliance or causes runtime failure | label={t()} in citizen app, SQL string interpolation, hardcoded secret |
| P1 | Functional/UX problem on target devices | 100vh, missing credentials: "include", hover-only interaction |
| P2 | Convention violation, code quality issue | Hardcoded spacing, any type on new code, missing aria-expanded |
| P3 | Style/consistency nit | Key naming convention, import path preference |
Verdict
| Verdict | Condition | |---------|-----------| | CLEAN | Zero P0/P1 findings | | WARN | Zero P0, some P1 findings | | BLOCKED | Any P0 finding remains unfixed |
Phase 0: Preflight — Identify Changed Files
# Determine file set based on scope
git diff --name-only HEAD
git diff --cached --name-only
git ls-files --others --exclude-standard
Classify changed files into buckets:
| Bucket | Pattern | Checks Applied |
|--------|---------|----------------|
| citizen-tsx | apps/citizen/src/*.tsx | Phases 1-7 (all) |
| citizen-css | apps/citizen/src/*.css | Phase 2 |
| citizen-ts | apps/citizen/src/*.ts (non-tsx) | Phases 1, 4, 5 |
| officer-tsx | apps/officer/src/*.tsx | Phases 2-7 (skip bilingual Field check) |
| officer-css | apps/officer/src/*.css | Phase 2 |
| api-ts | apps/api/src/*.ts | Phases 4, 5, 6 |
| package-ts | packages/*/src/*.ts | Phases 4, 5 |
| locale | */locales/*.ts | Phase 1.3 |
| css | *.css | Phase 2 |
| config | *.json, *.yaml, Dockerfile* | Phase 4.4 |
| other | Everything else | Skip |
If no changed files match any bucket, report "No scannable changes found" and exit CLEAN.
Record the file count per bucket for the report header.
Phase 1: Internationalization Compliance
Applies to: citizen-tsx, citizen-ts, locale files
1.1: Field Label Anti-Pattern (P0)
The #1 bilingual compliance violation. <Field> labels in the citizen app MUST use <Bilingual>, never t().
# P0: Field labels using t() instead of <Bilingual>
rg 'label=\{t\(' <changed-citizen-tsx-files>
Expected: zero matches. Every match is a P0 finding.
Auto-fix (when fix mode):
label={t("section.key")} → label={<Bilingual tKey="section.key" />}
1.2: Hardcoded English Strings (P0)
User-visible text in citizen app JSX must go through i18n.
# P0: Hardcoded English in headings
rg '<h[1-6][^>]*>[A-Z][a-z]' <changed-citizen-tsx-files>
# P0: Hardcoded English in paragraphs and spans used as labels
rg '<(p|span)\s+className="[^"]*label[^"]*">[A-Z]' <changed-citizen-tsx-files>
# P1: Hardcoded English in button text (not using t())
rg '<button[^>]*>[A-Z][a-z]{2,}' <changed-citizen-tsx-files>
rg '<Button[^>]*>[A-Z][a-z]{2,}' <changed-citizen-tsx-files>
Exceptions (not findings):
- Code comments and JSDoc
classNamestring valuesconsole.log/console.errormessagesaria-labelvalues that are English-only by design (screen readers)- Test files (
*.test.tsx,*.spec.tsx)
1.3: Missing Locale Keys (P0)
Every i18n key used in citizen app must exist in all three locale files.
# Extract keys used in changed citizen TSX files
rg 'tKey="([^"]+)"' <changed-citizen-tsx-files> -o --no-filename | sort -u
rg "t\(['\"]([^'\"]+)['\"]" <changed-citizen-tsx-files> -o --no-filename | sort -u
# For each key, verify presence in all three locale files
rg '<key>' apps/citizen/src/locales/en.ts
rg '<key>' apps/citizen/src/locales/hi.ts
rg '<key>' apps/citizen/src/locales/pa.ts
If a key exists in en.ts but is missing from hi.ts or pa.ts, that is a P0 finding.
If locale files themselves were changed, cross-check that all three files have the same set of top-level keys.
1.4: Key Naming Convention (P3)
Keys should follow section.descriptor pattern with snake_case descriptors.
# P3: camelCase keys (should be snake_case)
rg 'tKey="[a-z]+\.[a-z]+[A-Z]' <changed-citizen-tsx-files>
rg "t\(['\"][a-z]+\.[a-z]+[A-Z]" <changed-citizen-tsx-files>
Phase 2: CSS & Responsive Compliance
Applies to: all changed CSS files + inline styles in TSX files
2.1: Viewport Height (P0)
# P0: 100vh (must use dvh)
rg '100vh' <changed-css-files> <changed-tsx-files>
Auto-fix: 100vh → 100dvh
2.2: Pixel Breakpoints (P0)
# P0: px values in @media queries (must use rem)
rg '@media[^{]*\d+px' <changed-css-files>
Verify matches are actually breakpoint values (width/height), not properties inside the media block.
2.3: Ad-hoc Breakpoints (P1)
# P1: Breakpoints not matching the three defined tokens
# Valid values: 22.5rem (360px), 48rem (768px), 80rem (1280px)
rg '@media[^{]*(min-width|max-width):\s*\d' <changed-css-files>
For each match, verify the value is one of 22.5rem, 48rem, or 80rem. Any other value is a P1 finding.
2.4: Hardcoded Spacing in Inline Styles (P1)
# P1: Inline styles with hardcoded spacing (should use CSS tokens)
rg 'style=\{\{[^}]*(padding|margin|gap):\s*"?\d+(px|rem)' <changed-tsx-files>
Exception: style={{ gap: "var(--space-4)" }} (using CSS variable in inline style) is acceptable.
2.5: Fixed Widths (P1)
# P1: Fixed pixel widths that will overflow on mobile
rg 'width:\s*\d{3,}px' <changed-css-files> <changed-tsx-files>
Widths >= 100px hardcoded in pixels are suspect. Should use min(), max-width, or percentage/rem values.
2.6: Hover Without Active (P1)
# P1: :hover styles without corresponding :active
rg ':hover' <changed-css-files> -l
For each file with :hover, verify a corresponding :active rule exists for the same selector.
2.7: Hardcoded Colors (P2)
# P2: Hardcoded hex/rgb colors instead of CSS custom properties
rg '(color|background|border).*#[0-9a-fA-F]{3,8}' <changed-css-files>
rg '(color|background|border).*rgb\(' <changed-css-files>
Exceptions: Inside CSS custom property definitions (:root { --color-x: #abc; }) and SVG fill/stroke attributes.
2.8: Standalone vw Units (P2)
# P2: vw not inside clamp() — causes horizontal scroll
rg '\d+vw' <changed-css-files> <changed-tsx-files>
vw is acceptable inside clamp(). Outside clamp(), it is a P2 finding.
Phase 3: Accessibility Compliance
Applies to: changed TSX files (citizen + officer)
3.1: Touch Targets (P1)
# P1: Small interactive elements without min-height
rg '<button\b' <changed-tsx-files> -l
rg 'onClick=\{' <changed-tsx-files> -l
For each file, verify that clickable elements have CSS rules with min-height: 2.75rem or use the shared <Button> component.
3.2: Missing aria-label on Icon Buttons (P1)
# P1: Buttons with only SVG/icon content and no aria-label
rg '<button[^>]*>\s*<svg' <changed-tsx-files>
rg '<Button[^>]*>\s*<svg' <changed-tsx-files>
3.3: div-as-button Anti-Pattern (P1)
# P1: div/span with onClick but no role="button" or tabIndex
rg '<(div|span)[^>]*onClick=' <changed-tsx-files>
For each match, verify role="button" and tabIndex={0} are present. Better fix: convert to <button>.
3.4: Missing aria-expanded on Toggles (P2)
# P2: Toggle patterns without aria-expanded
rg '(setIsOpen|setExpanded|setShow|toggle)' <changed-tsx-files> -l
3.5: Images Without alt (P2)
# P2: img tags without alt attribute
rg '<img\b(?![^>]*\balt=)' <changed-tsx-files>
3.6: Safe-Area Insets on Fixed/Sticky Elements (P1)
# P1: position fixed/sticky without safe-area-inset
rg 'position:\s*(fixed|sticky)' <changed-css-files> -l
For each fixed/sticky rule, verify env(safe-area-inset-*) or max(..., env(...)) is present.
Phase 4: Security Quick Scan
Applies to: all changed TS/TSX files
4.1: SQL Injection (P0)
# P0: String interpolation in SQL queries
rg 'query\s*\(\s*`' <changed-ts-files>
rg "query\s*\(\s*['\"].*\\\$\{" <changed-ts-files>
rg 'query\s*\(.*\+\s*(req|request|body|params|query)' <changed-ts-files>
Must use parameterized queries ($1, $2).
4.2: Hardcoded Secrets (P0)
# P0: Secrets/keys/passwords in source code
rg '(password|secret|api_key|apiKey|token)\s*[:=]\s*["\x27][^"\x27]{8,}' <changed-ts-files> --glob '!*.test.*' --glob '!*.spec.*' --glob '!locales/*'
False positive filters:
- Type definitions (
password: string) - Variable names without values (
const secret = process.env.SECRET) - Test fixtures in
*.test.*files - Locale string values containing "password" as a label
4.3: Console.log in Production Code (P2)
# P2: console.log left in production code
rg 'console\.(log|debug|info)' <changed-ts-files> --glob '!*.test.*' --glob '!*.spec.*'
4.4: Secrets in Config Files (P0)
# P0: Secrets in committed config files
rg '(password|secret|key|token)\s*[:=]\s*["\x27][A-Za-z0-9+/=]{16,}' <changed-config-files> --glob '!package-lock.json' --glob '!*.example'
4.5: Raw reply.send for Errors (P2)
# P2: Raw reply.send with error objects instead of sendError()
rg 'reply\.(code|status)\(\d+\)\.send\(\{' <changed-api-files>
rg 'reply\.send\(\{\s*error' <changed-api-files>
Should use sendError(), send400(), send401(), etc. from the errors module.
Phase 5: TypeScript Hygiene
Applies to: all changed TS/TSX files
5.1: New any Types (P2)
# P2: any type annotations in new/changed code
git diff HEAD -- <file> | rg '^\+.*:\s*any\b'
git diff HEAD -- <file> | rg '^\+.*as\s+any\b'
5.2: Missing Type on Button Elements (P1)
# P1: Raw <button> without explicit type attribute
rg '<button\b(?![^>]*\btype=)' <changed-tsx-files>
5.3: Import Path Conventions (P3)
# P3: Direct relative imports from packages instead of workspace aliases
rg "from ['\"]\.\.\/\.\.\/packages\/" <changed-ts-files>
# P3: Importing from @puda/shared root instead of sub-path
rg "from ['\"]@puda/shared['\"]" <changed-ts-files> -A 1
Phase 6: API Pattern Compliance
Applies to: changed API and frontend files
6.1: Missing Credentials Include (P0)
# P0: fetch() calls without credentials: "include" (breaks cookie auth)
rg 'fetch\(' <changed-citizen-tsx-files> <changed-officer-tsx-files> -A 5
For each fetch() call, verify credentials: "include" is present either in the options object or via a shared authHeaders() call.
Exception: Fetches to external URLs may intentionally omit credentials.
6.2: Missing Auth Headers (P1)
# P1: fetch without authHeaders() in authenticated components
rg 'fetch\(' <changed-citizen-tsx-files> -B 5 -A 10
6.3: Missing Offline Guard on Mutations (P1)
# P1: Form submit or mutation button without isOffline check
rg '(onSubmit|handleSubmit|handleDelete|handleUpdate|handleSave)' <changed-tsx-files> -l
For each mutation handler, verify:
- The component receives
isOfflineprop or uses an offline hook - The submit/mutation button has
disabled={isOffline}or equivalent guard - An offline banner is shown when offline
Phase 7: Component Pattern Compliance
Applies to: changed TSX files
7.1: Missing useCallback on Fetch Functions (P2)
# P2: Functions used in useEffect deps without useCallback
rg 'useEffect\(\s*\(\)\s*=>\s*\{[^}]*\b(fetch|load|get)[A-Z]' <changed-tsx-files>
7.2: Missing Error State UI (P2)
# P2: Components with fetch but no error state rendering
rg 'catch\s*\(' <changed-tsx-files> -l
7.3: Skeleton vs Spinner Loading (P3)
# P3: Spinner loading instead of skeleton
rg '(Loading\.\.\.|spinner|Spinner|CircularProgress)' <changed-tsx-files>
Project convention is skeleton blocks for loading states, not spinners.
7.4: Missing Cache Write After Fetch (P3)
# P3: fetch without corresponding writeCached
rg 'fetch\(' <changed-citizen-tsx-files> -l
For citizen app components that fetch API data, verify writeCached() is called with the response data.
Phase 8: Report
8.1: Finding Summary
VIBE-CODING GUARDRAILS REPORT
==============================
Scope: <files scanned / git diff range>
Files scanned: <count by bucket>
Scan time: <seconds>
FINDINGS:
P0 (Blocker): <count>
P1 (High): <count>
P2 (Medium): <count>
P3 (Low): <count>
Fixed (auto): <count>
VERDICT: <CLEAN | WARN | BLOCKED>
8.2: Findings Detail Table
| # | Sev | Phase | File:Line | Finding | Status |
|---|-----|-------|-----------|---------|--------|
| 1 | P0 | 1.1 | App.tsx:42 | `label={t("login.id")}` — must use `<Bilingual>` | OPEN / FIXED |
8.3: Auto-Fix Summary (if fix mode)
| # | File:Line | Before | After |
|---|-----------|--------|-------|
| 1 | App.tsx:42 | `label={t("login.id")}` | `label={<Bilingual tKey="login.id" />}` |
8.4: Next Steps
Based on verdict:
- CLEAN: "No guardrail violations. Ready for commit."
- WARN: "P1 findings should be addressed before merge. Use
/vibe-coding-guardrails fixto auto-fix where possible." - BLOCKED: "P0 violations must be resolved. These will break compliance or cause runtime failures."
Common Fix Patterns
| Finding | Before | After | Auto-fixable? |
|---------|--------|-------|---------------|
| Field label anti-pattern | label={t("k")} | label={<Bilingual tKey="k" />} | Yes |
| 100vh | height: 100vh | height: 100dvh | Yes |
| px in breakpoint | @media (max-width: 768px) | @media (max-width: 48rem) | Yes (if standard value) |
| Missing button type | <button onClick={fn}> | <button type="button" onClick={fn}> | Yes |
| console.log | console.log("debug") | (remove line) | Yes |
| Inline px spacing | style={{ padding: "16px" }} | style={{ padding: "var(--space-4)" }} | No (ambiguous token) |
| div onClick | <div onClick={fn}> | <button type="button" onClick={fn}> | No (needs class migration) |
| Missing credentials | fetch(url) | fetch(url, { credentials: "include" }) | No (need to verify context) |
| Hardcoded English | <h2>My Apps</h2> | <h2><Bilingual tKey="nav.my_apps" /></h2> | No (need to create keys) |
| Missing locale key | Key in en.ts only | Add to hi.ts + pa.ts | No (need translation) |
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.