Vibe-Coding Guardrails

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.

Sby Skills Guide Bot
DevelopmentIntermediate
007/22/2026
Claude Code
#vibe-coding#guardrails#code-scan#anti-patterns#conventions

Recommended for


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:line and 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, or vite build. Use rg/grep/glob only.
  • No network: never curl, fetch, or start servers.
  • Fix-forward: when fix mode 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
  • className string values
  • console.log / console.error messages
  • aria-label values 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: 100vh100dvh

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:

  1. The component receives isOffline prop or uses an offline hook
  2. The submit/mutation button has disabled={isOffline} or equivalent guard
  3. 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 fix to 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) |

Related skills