Audit
12 Claude Code skills auto-extracted from real sessions: Docker/SSH/VPS ops, data/ML pipeline gotchas, 4 model prompting field guides, a 10-category bug audit, and a persistent project wiki (llm-wiki) with slash commands.
npx -y skills add aksheyw/claude-code-learned-skills --skill auditAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Comprehensive codebase audit skill covering 10 systemic bug categories plus CI automation. Use this skill proactively during code review, E2E testing, pre-deployment checks, or whenever you spot any of these patterns — raw database values in UI (snake_case, camelCase enums, numeric codes), API contract mismatches between client and server, Firestore/database rules gaps, CORS missing production domains, analytics/consent gating features, dynamic Tailwind/CSS classes purged in prod, window.open without noopener, orphaned data on deletion, hardcoded secrets, Android/iOS security misconfigs, performance regressions (bundle size, re-renders, lazy loading), or accessibility gaps (missing aria labels, keyboard nav, screen reader). Also triggers on "audit this", "did you check everything", "is this production ready", "what did we miss", "are you sure", or any thoroughness question. One bug instance always means many more exist — this skill enforces the ripple search.
SKILL.md
24.8 KB, as published. Nobody here has run it
Codebase Audit — 10-Category Systemic Bug Finder
This skill codifies hard-won lessons from production bugs. Every pattern here was discovered the hard way — usually one instance revealing many more across the codebase. Use it proactively, not reactively.
How to Use This Skill
Pick the relevant audit category (or run all 10 for a pre-deploy check). Each category has:
- Why it matters — the real-world consequence
- Detection — how to find instances
- The ripple search — how one bug leads to more
- Fix pattern — the proven solution
- Checklist — verify completeness
1. Display Value Formatting
The bug: Database/API values stored as machine-readable keys (bright_indirect, cactus_mix, glazed_ceramic) leak into the UI without formatting.
Why it's systemic: The same field renders in multiple views (detail pages, cards, lists, settings). If one field leaks raw values, sibling fields almost certainly do too. In the original case, one bright_indirect bug turned into 9 fixes across 7 files.
Detection
Scan for template expressions that render data fields without a formatting function:
# Red flags in rendered output:
{plant?.fieldName} # React/JSX — raw field in display context
{{ item.field_name }} # Vue/Angular/Handlebars
${data.fieldName} # Template literals
Visual red flags:
- Underscores in user-facing text (
bright_indirect,cactus_mix) - camelCase in user-facing text (
needsAttention,highHumidity) - ALL_CAPS codes (
PENDING_REVIEW,AUTH_FAILED) - Numeric codes where labels expected (
status: 2,priority: 1)
Ripple Search
- Trace the field to its source (API response, Firestore doc, form submission)
- Grep the entire codebase for the field name — not just the file where the bug was found
- Check sibling fields — if
lightNeedsleaks, also checkpotType,soilType,drainage,status - Check all output channels — UI, email templates, push notifications, PDF exports, error messages, activity feeds
Fix Pattern
Centralized formatter co-located with the data definition (single source of truth):
LOOKUP_TABLE = {
"bright_indirect": { label: "Bright Indirect", ... },
"low": { label: "Low Light", ... },
}
function formatFieldName(key):
if key is empty/null -> return sensible default
if LOOKUP_TABLE has key -> return LOOKUP_TABLE[key].label
else -> convert coded value to human-readable as fallback
Three properties: null-safe, lookup-first, graceful fallback for unknown keys.
Fallback strategies:
| Format | Strategy | Example |
|---|---|---|
snake_case | Split on _, capitalize each | bright_indirect -> Bright Indirect |
camelCase | Split on uppercase, capitalize | needsAttention -> Needs Attention |
SCREAMING_SNAKE | Split on _, capitalize first | PENDING_REVIEW -> Pending Review |
| Numeric codes | Must have explicit lookup | 2 -> Active |
When NOT to format: API responses, form values, admin/debug tools, technical identifiers, copy-to-clipboard.
Anti-Patterns
- Inline
value.replace(/_/g, ' ')scattered across components - Inconsistent fallbacks (
{field || 'A'}in one view,{field || 'B'}in another) - Formatting at write time (storing "Bright Indirect" in DB breaks queries, sorting, i18n)
Checklist
- Grepped entire codebase for all display locations of the field
- Checked sibling fields in the same data model
- Checked all output channels (UI, email, notifications, exports)
- Created or found centralized formatter (null-safe + fallback)
- Fixed ALL display locations, not just the reported one
- Removed duplicate local formatters
- Added unit tests (known keys, unknown keys, null/undefined)
- Updated test mocks for modules that re-export the formatter
2. API Contract Mismatch
The bug: Client and server disagree on request/response shape. Adding auth middleware server-side without updating client headers. Backend returning different field names than frontend expects.
Why it's systemic: Ship-stopper. In our case, adding verifyFirebaseToken to all API endpoints broke every API call because the client's API wrapper module never sent the Authorization header.
Detection
# Find all fetch/axios calls and check headers
Grep for: fetch(, axios., .post(, .get(
Cross-reference with: server-side middleware (auth, validation, CORS)
# Find response field access patterns
Grep for: response.data., result., res.json
Cross-reference with: actual API response shapes
Ripple Search
- When ANY API endpoint changes auth/validation/shape, check ALL endpoints
- Verify client sends all required headers (auth token, content-type)
- Verify error handling matches on both sides
- Check that response adapters are used (never read raw response paths in components)
Fix Pattern
- Response adapters — normalize raw API responses into expected shapes (single adapter per endpoint)
- Auth header injection — centralize in one API client module, not per-call
- Contract tests — test that client request shape matches server expectations
Checklist
- All endpoints checked after any auth/middleware change
- Client sends required headers (auth, content-type)
- Response adapters exist for every API endpoint
- Components use adapted responses, never raw
- Error shapes match on client and server
3. Firestore Rules & Subcollections
The bug: Parent collection has security rules but subcollections are silently denied by default. Deletion of parent document orphans subcollection data.
Why it's systemic: Firestore rules are NOT transitive. Every subcollection needs explicit rules. And deleteDoc(parent) does NOT cascade — subcollection data persists as orphans (GDPR risk).
Detection
# Find all subcollection writes in code
Grep for: collection(db, 'parentCollection', parentId, 'subCollection')
Grep for: .doc().collection(
# Cross-reference with firestore.rules
# Every subcollection path must have a match block
Ripple Search
- List ALL subcollections from code (not just
firestore.rules) - For each: verify explicit
match /{subcoll}/{docId}rule exists - For each parent deletion: verify subcollection cleanup happens first
- Check GDPR impact — can all user data be purged on account deletion?
Fix Pattern
- Explicit rules for every subcollection in
firestore.rules - Cascade deletion — always query + delete subcollection docs before deleting parent
- Use batch/transaction for atomic cascade: query subcollection -> delete each -> delete parent
- Fire-and-forget operations (
.catch(() => {})) should at minimum log failures
Checklist
- Every subcollection in code has matching firestore.rules entry
- Parent deletion cascades to all subcollections
- Account deletion purges ALL user data (including subcollections)
- Silent failures (fire-and-forget) have at minimum error logging
4. CORS & Domain Configuration
The bug: CORS rules list dev domains (localhost, Firebase hosting) but omit the production domain. Breaks image uploads, API calls, and analytics collection from the live site.
Why it's systemic: Every external service has its own CORS config. Adding a new domain (or migrating to a new one) requires updating ALL of them.
Detection
# Find all CORS configurations
Files to check:
- Firebase Storage: cors.json
- Vercel/API: api/_common.js or middleware
- n8n webhooks: workflow CORS settings
- Firebase Analytics: collection domain config
- CDN/hosting: headers configuration
Ripple Search
For EVERY domain serving your app, verify it appears in:
- Firebase Storage CORS (
cors.json) - API proxy CORS headers
- n8n webhook allowed origins
- CSP (Content Security Policy) headers
- Image optimization domain allowlists
Checklist
- Production domain in ALL CORS configs
- Dev domains NOT in production CORS configs
- All external service CORS configs audited
- New domain additions trigger full CORS sweep
5. Consent & Analytics Gating
The bug: App features are blocked when analytics consent is denied. Or worse: analytics cookies/tracking fire despite the user denying consent (GDPR violation).
Why it's systemic: Consent logic touches every analytics call, every page load, every user interaction that's tracked.
Detection
# Find consent-gated code
Grep for: consentGranted, analyticsConsent, consent
Verify: NO feature logic is blocked by consent status
Verify: ALL trackEvent() calls are no-ops when consent denied
# Find cookie/tracking behavior
Test flow: deny consent -> check browser cookies -> verify no GA/analytics cookies set
Ripple Search
- Feature works identically with consent granted AND denied
- No analytics cookies set when consent denied
trackEvent()calls are no-ops (not errors) when consent denied- Consent state persists across sessions
- Consent can be changed after initial choice
Checklist
- App fully functional with consent denied
- Zero analytics cookies when consent denied
- trackEvent() no-ops gracefully (no errors)
- Consent persists across sessions
- Consent is revocable
6. Dynamic CSS Class Purging
The bug: Template literal CSS classes (bg-${color}-500/10) work in dev but are purged in production builds. Tailwind's purger can't parse dynamic class construction.
Why it's systemic: Every new color-coded feature risks this. Developers see it working in dev and ship broken styling to prod.
Detection
# Find dynamic Tailwind classes
Grep for regex: \$\{.*\}.*(bg-|text-|border-|ring-)
Grep for regex: `(bg|text|border|ring)-\$\{
# Also check for:
className={`text-${status === 'good' ? 'green' : 'red'}-400`}
Fix Pattern
Replace dynamic class construction with static class maps:
// BAD: purged in production
className={`bg-${color}-500/10`}
// GOOD: all classes visible to purger
const COLOR_CLASSES = {
water: 'bg-blue-500/10 text-blue-400',
fertilize: 'bg-green-500/10 text-green-400',
prune: 'bg-amber-500/10 text-amber-400',
};
className={COLOR_CLASSES[action]}
Checklist
- Zero template literal class names with dynamic color/size values
- All conditional classes use static maps or ternaries with full class strings
- Production build visually verified (not just dev)
7. Security Hygiene
The bug: Multiple security issues that individually seem minor but compound: window.open without noopener, cleartext traffic in production, hardcoded secrets, missing minification.
Detection Checklist
Browser security:
- All
window.open('url', '_blank')have third param'noopener,noreferrer' - External links don't leak user data in URL params (uid, tokens)
- No
javascript:URLs oreval()usage
Mobile/native security:
-
android:usesCleartextTraffic="false"in AndroidManifest.xml -
network_security_config.xmlonly allows cleartext for localhost in debug -
minifyEnabled truein release build type - Release keystore backed up securely
Secrets:
- No API keys, passwords, or tokens in source code
- No secrets in git history (
git log --all -p | grep -i 'password\|api_key\|secret') - All secrets in environment variables or secret manager
- Service worker SDK versions match app dependency versions
Third-party:
-
window.openusesnoopener,noreferrer - URLs with user data (uid, tokens) don't go to third parties
- CSP headers configured for production
8. Test & Mock Completeness
The bug: Adding a new export to a module breaks all tests that mock that module — the mock doesn't include the new export, so components crash with undefined is not a function.
Why it's systemic: Every test file that mocks the changed module needs updating. Miss one and CI passes but that test file is actually broken.
Detection
When you add a new export to any module:
# Find all mocks of the changed module
Grep for: vi.mock('path/to/module')
Grep for: jest.mock('path/to/module')
# Each mock must include the new export
Ripple Search
- New function added to
physicsEngine.js-> check ALL files thatvi.mock('../../lib/physicsEngine') - New export from a hook -> check all test files mocking that hook
- Changed function signature -> check all mocks match new params
Fix Pattern
When adding exports:
- Add the export
- Immediately grep for all mocks of that module
- Add the new export to every mock
- Run full test suite (not just the file you changed)
Checklist
- Grepped for all mocks of the changed module
- Every mock includes the new export with correct signature
- Full test suite passes (not just changed files)
- Mock behavior matches real implementation for edge cases (null, undefined)
9. Performance & Bundle Health
The bug: App ships with massive bundles, unnecessary re-renders, unoptimized images, and no lazy loading. Users on slow connections (India 3G/4G) see 10+ second load times or blank screens.
Why it's systemic: Performance debt accumulates silently. Each new feature adds weight. By the time someone notices, the entire app is slow.
Detection
# Bundle analysis
npx vite-bundle-visualizer # Vite
npx webpack-bundle-analyzer # Webpack
npx next build && npx @next/bundle-analyzer # Next.js
# Find missing lazy loading
Grep for: import .* from '.*/pages/ # Direct page imports (should be React.lazy)
Grep for: import .* from '.*/components/ # Large component imports
# Find unnecessary re-renders (React)
# In browser DevTools: React Profiler > "Why did this render?"
# Or add React.memo() tracking
Ripple Search
- Lazy loading — every route-level page component should use
React.lazy()or dynamicimport() - Heavy dependencies — check
node_modulesfor duplicate packages or oversized libraries (moment.js, lodash full import vs lodash-es) - Image optimization — all user-uploaded images should be resized/compressed before display; use
srcsetor<picture>for responsive images - Re-render chains — one unnecessary re-render at the top of a component tree cascades to every child
- Network waterfall — sequential API calls that could be parallel; missing
Promise.all()
Fix Patterns
// Lazy loading routes
const PlantDetail = React.lazy(() => import('./pages/PlantDetail'));
// Tree-shakeable imports
import { debounce } from 'lodash-es'; // NOT: import _ from 'lodash'
// Memoize expensive computations
const sortedPlants = useMemo(() => plants.sort(...), [plants]);
// Parallel API calls
const [plants, sites] = await Promise.all([fetchPlants(), fetchSites()]);
Checklist
- Bundle size measured and baselined (record in project docs)
- All route pages lazy-loaded
- No full library imports where tree-shakeable alternatives exist
- Images optimized (compressed, responsive, lazy-loaded below fold)
- No sequential API calls that could be parallel
- Network status detection for slow/offline connections
- Loading skeletons for async content (not blank screens)
10. Accessibility (a11y)
The bug: Interactive elements missing labels, keyboard navigation broken, screen readers can't parse the page, color contrast insufficient. Excludes users and fails compliance audits.
Why it's systemic: Accessibility is rarely tested during development. Once you find one gap, the entire app likely has dozens more — every custom component, every modal, every form.
Detection
# Automated scanning
npx axe-core # Run axe accessibility checker
npx pa11y # CLI accessibility tester
# Browser: Chrome DevTools > Lighthouse > Accessibility
# Manual checks
Grep for: onClick.*div # Clickable divs without role="button" + tabIndex
Grep for: onClick.*span # Same pattern
Grep for: <img(?!.*alt) # Images missing alt text
Grep for: aria- # Verify aria attributes are correct (not just present)
Ripple Search
- Interactive elements — every clickable element needs
role,tabIndex,aria-label(or visible label), and keyboard event handlers (onKeyDownfor Enter/Space) - Form fields — every input needs an associated
<label>oraria-label - Modals/sheets — need focus trap, Escape to close,
aria-modal="true", return focus on close - Color contrast — all text must meet WCAG AA (4.5:1 for normal text, 3:1 for large text)
- Motion — respect
prefers-reduced-motionfor all animations - Screen reader — page structure uses semantic HTML (
<nav>,<main>,<section>,<h1>-<h6>in order)
Fix Patterns
// BAD: inaccessible clickable div
<div onClick={handleClick} className="cursor-pointer">Click me</div>
// GOOD: accessible button
<button onClick={handleClick} aria-label="Add plant">Click me</button>
// Or if it must be a div:
<div onClick={handleClick} onKeyDown={(e) => e.key === 'Enter' && handleClick()}
role="button" tabIndex={0} aria-label="Add plant">Click me</div>
// Reduced motion
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
Checklist
- Lighthouse accessibility score > 90
- All interactive elements keyboard-navigable (Tab, Enter, Escape)
- All images have meaningful alt text (or
alt=""for decorative) - All form inputs have associated labels
- Modals trap focus and return it on close
- Color contrast meets WCAG AA (4.5:1)
- Animations respect
prefers-reduced-motion - Semantic HTML structure (
nav,main,h1-h6hierarchy) -
aria-liveregions for dynamic content updates (toasts, notifications)
Framework-Specific Detection Commands
Different frameworks expose raw values differently. Use these grep patterns per framework:
React / JSX
# Raw field in display context
Grep: {(plant|item|data|user|site)\?\.\w+}
Grep: {(plant|item|data|user|site)\.\w+}
# Exclude: onClick, onChange, key=, ref=, className= contexts
Vue / Angular / Svelte
# Vue: {{ object.field }}
Grep: \{\{\s*(plant|item|data|user)\.\w+\s*\}\}
# Angular: {{ object.field }}
Grep: \{\{\s*(plant|item|data|user)\.\w+\s*\}\}
# Svelte: {object.field}
Grep: \{(plant|item|data|user)\.\w+\}
Python (Django / Flask / FastAPI)
# Template rendering
Grep: \{\{\s*(object|item|record)\.\w+\s*\}\} # Django/Jinja2
Grep: f".*\{(obj|item|record)\.\w+\}" # f-strings in responses
# Django model __str__ returning raw field
Grep: def __str__.*return self\.\w+
Backend API Responses
# Returning raw enum/status values to frontend
Grep: 'status':\s*(self|obj|record)\.\w+
Grep: jsonify.*status.*=.*\.status
CI/CD Automation
Integrate these checks into your CI pipeline so they run automatically on every PR:
GitHub Actions Example
name: Audit Checks
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Category 1: Display values — grep for raw field access
- name: Check for raw database values in JSX
run: |
# Flag potential raw value display (manual review needed)
grep -rn --include="*.jsx" --include="*.tsx" \
'{plant\?\.\(potType\|soilType\|lightNeeds\|drainage\|status\)' \
src/ && echo "::warning::Possible raw database values in UI" || true
# Category 6: Dynamic Tailwind classes
- name: Check for dynamic Tailwind classes
run: |
grep -rn --include="*.jsx" --include="*.tsx" \
'bg-\${.*}\|text-\${.*}\|border-\${.*}' \
src/ && exit 1 || echo "No dynamic Tailwind classes found"
# Category 7: Security — window.open
- name: Check window.open security
run: |
grep -rn --include="*.jsx" --include="*.tsx" --include="*.js" \
"window.open.*'_blank'" src/ | \
grep -v "noopener" && exit 1 || echo "All window.open calls secure"
# Category 7: Secrets in source
- name: Check for hardcoded secrets
run: |
grep -rn --include="*.js" --include="*.jsx" --include="*.ts" \
'sk-\|api_key.*=.*"\|password.*=.*"' \
src/ api/ && exit 1 || echo "No hardcoded secrets found"
# Category 8: Test suite
- name: Run full test suite
run: npx vitest run
# Category 10: Accessibility
- name: Lighthouse accessibility audit
uses: treosh/lighthouse-ci-action@v11
with:
urls: http://localhost:3000
configPath: .lighthouserc.json
Pre-Commit Hook (Local)
#!/bin/sh
# .husky/pre-commit — run before every commit
# Check for dynamic Tailwind classes
if grep -rn 'bg-\${.*}\|text-\${.*}' --include="*.jsx" src/; then
echo "ERROR: Dynamic Tailwind classes found. Use static class maps."
exit 1
fi
# Check for window.open without noopener
if grep -rn "window.open.*'_blank'" --include="*.jsx" --include="*.js" src/ | grep -v "noopener"; then
echo "ERROR: window.open missing noopener,noreferrer"
exit 1
fi
# Check for console.log
if grep -rn "console\.log" --include="*.jsx" --include="*.js" src/lib/ src/pages/ src/components/; then
echo "WARNING: console.log found — use logger utility instead"
fi
What to Automate vs. Manual Review
| Check | Automate? | Why |
|---|---|---|
| Dynamic Tailwind classes | Yes | Pure pattern match, zero false positives |
| window.open security | Yes | Pure pattern match |
| Hardcoded secrets | Yes | Pattern match + git history scan |
| Bundle size threshold | Yes | size-limit or bundlesize packages |
| Test suite passes | Yes | Standard CI |
| Lighthouse a11y score | Yes | Automated scoring |
| Raw display values | Partial | Can flag candidates, but needs human judgment |
| API contract match | No | Requires cross-referencing client + server |
| CORS config | No | Requires checking external service configs |
| Consent behavior | No | Requires manual browser testing |
Pre-Deployment Full Audit
Run all 10 categories before any production deployment:
| # | Category | Quick Command |
|---|---|---|
| 1 | Display values | Grep for raw field access in JSX/template display contexts |
| 2 | API contracts | Diff client headers vs server middleware requirements |
| 3 | Firestore rules | List code subcollections, cross-ref with firestore.rules |
| 4 | CORS | Check all external service CORS for production domain |
| 5 | Consent | Test full app flow with consent denied |
| 6 | Dynamic CSS | Grep for template literal Tailwind classes |
| 7 | Security | Run checklist above |
| 8 | Test mocks | Run full test suite, check for undefined function errors |
| 9 | Performance | Bundle analysis + lazy loading check + image audit |
| 10 | Accessibility | Lighthouse a11y audit + keyboard nav + screen reader test |
Meta-Lesson: The Ripple Search
The single most important audit principle: one bug instance always means many more. When you find any bug from the categories above:
- Same field, all views — grep the entire codebase, not just the file
- Sibling fields — if
lightNeedsleaks, also checkpotType,soilType, etc. - Same pattern, other entities — if
plant.statusleaks, checksite.status,user.tier - All output channels — UI, email, push notifications, exports, error messages
Never fix just the reported instance. The skill to develop is: from one bug, discover the systemic pattern, then fix every instance at once.
Iterative Depth
Single-pass review catches 0-2 bugs. Iterative pushback catches all of them. In the audit that originated this skill, 28 rounds of review found 14 production bugs (2 ship-stoppers, 4 HIGH, 8 MEDIUM) that single-pass review missed entirely.
The rule: review is not "complete" until a full round produces zero new findings.