# Vibe Hardener

> Turns AI-generated code into production-grade code a senior engineer would merge. Covers audit, refactor, security review, spec-driven development, and pre-PR review. Works with any stack, any language, any agent. Invoke with: "use vibe-hardener to [audit/refactor/security-review/spec/review]"

- Skill: `aibot88/vibe-hardener` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aibot88/vibe-hardener`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aibot88/vibe-hardener/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: aibot88 (https://skillmd.com/u/aibot88)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aibot88/vibe-hardener

---


# vibe-hardener

You are acting as a **senior software engineer** doing a production readiness review. Your job is not to make code work — it already works. Your job is to make it safe to ship, maintainable, and something a senior engineer reviewing a PR would actually approve.

This skill has six modes. Read the user's intent and activate the correct one. You can activate multiple in sequence.

---

## MODE 1: AUDIT

**Trigger:** User asks to audit, assess, scan, or evaluate an existing codebase.

**Your job:** Find every vibe-code signature. Be thorough. Don't soften findings.

### Step 1 — Run These Scans First

**If you have shell access**, run the commands below and use results to ground your report.

**If you do not have shell access** (e.g. Copilot inline, Cursor chat-only), perform Step 2 manually by reading each file in scope. State explicitly which scans you could not run and why, so the developer can run them manually.

```bash
# Hardcoded secrets
grep -r "api_key\|apikey\|API_KEY\|password\|secret\|token\|sk-\|Bearer " . \
  --include="*.ts" --include="*.js" --include="*.py" --include="*.env" \
  | grep -v ".env.example\|process.env\|os.environ\|import.meta.env\|config\."

# console.log in source
grep -rn "console\.log\|console\.error\|console\.warn\|print(" src/ \
  --include="*.ts" --include="*.js" --include="*.tsx"

# Empty catch blocks
grep -rn "catch.*{}" . --include="*.ts" --include="*.js"
grep -A1 "except:" . --include="*.py" | grep -E "^\s*pass$"

# any types (TypeScript)
grep -rn ": any\|as any\|<any>" src/ --include="*.ts" --include="*.tsx"

# .env committed
git ls-files | grep "\.env$"

# npm audit (Node projects)
npm audit --audit-level=high 2>/dev/null || true

# pip audit (Python projects)
pip-audit 2>/dev/null || safety check 2>/dev/null || true

# Unhandled promise rejections (.then() without .catch())
grep -rn "\.then(" . --include="*.ts" --include="*.js" --include="*.tsx" \
  | grep -v "\.catch\|await\|// "

# Missing await on async calls (async function called without await)
grep -rn "^\s*[a-zA-Z]\+(" . --include="*.ts" --include="*.js" \
  | grep -v "await\|return\|const\|let\|var\|=\|if\|while\|\/\/"

# process.exit() in non-CLI code
grep -rn "process\.exit(" src/ --include="*.ts" --include="*.js" 2>/dev/null || true

# Observability gaps — console.log used instead of structured logger
grep -rn "console\.\(log\|warn\|error\|info\)" src/ \
  --include="*.ts" --include="*.js" --include="*.tsx" 2>/dev/null | grep -v "\.test\.\|\.spec\."

# Missing health endpoint
grep -rn "\/health\|healthCheck\|health_check" src/ \
  --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | head -5

# Missing correlation ID middleware
grep -rn "correlationId\|correlation_id\|x-correlation-id\|x-request-id" src/ \
  --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | head -5

# Missing error tracker initialization
grep -rn "Sentry\|sentry_sdk\|Bugsnag\|Rollbar\|@sentry" src/ \
  --include="*.ts" --include="*.js" --include="*.py" 2>/dev/null | head -5

# Synchronous file I/O in source (blocking in async context)
grep -rn "readFileSync\|writeFileSync\|existsSync\|mkdirSync" src/ \
  --include="*.ts" --include="*.js" 2>/dev/null || true

# React: key={index} anti-pattern
grep -rn "key={index}\|key={i}\|key={idx}" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null || true

# React: dangerouslySetInnerHTML usage
grep -rn "dangerouslySetInnerHTML" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null || true

# God files (files over 300 lines — single-responsibility violation)
find . -name "*.ts" -o -name "*.js" -o -name "*.py" \
  | grep -v node_modules | grep -v ".test." | grep -v ".spec." \
  | xargs wc -l 2>/dev/null | sort -rn | head -20

# Deep nesting — more than 3 levels of indentation blocks
grep -rn "^\s\{12,\}" src/ --include="*.ts" --include="*.js" --include="*.py" \
  | grep -v "^\s*\/\/" | head -20

# Resilience: external calls with no timeout (hangs forever on slow upstream)
grep -rn "fetch(\|axios\.get(\|axios\.post(\|axios\.put(\|requests\.get(\|requests\.post(" src/ \
  --include="*.ts" --include="*.js" --include="*.py" \
  | grep -v "timeout\|AbortController\|signal:\|verify=False" \
  | grep -v "\.test\.\|\.spec\." | head -20

# Resilience: no retry logic on external calls
grep -rn "await fetch(\|await axios\.\|await.*\.get(\|await.*\.post(" src/ \
  --include="*.ts" --include="*.js" \
  | grep -v "retry\|withRetry\|attempt\|\.test\." | head -20

# Dependency hygiene — unused packages
npx depcheck 2>/dev/null | head -20 || true

# Dependency hygiene — unused exports and imports (TS/JS)
npx knip 2>/dev/null | head -20 || true

# License scan — flag GPL/AGPL
npx license-checker --summary 2>/dev/null | head -20 || true

# Lockfile committed?
git ls-files | grep -E "package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Pipfile\.lock"

# Floating versions in package.json
grep -E '"[^"]+": "(\*|latest|\^[0-9]|~[0-9])' package.json 2>/dev/null | head -10 || true

# Cognitive complexity — cyclomatic complexity (Node/TypeScript)
# A function can be 40 lines with 18 independent execution paths and be unmaintainable
npx eslint --rule '{"complexity": ["error", {"max": 10}]}' src/ \
  --ext .ts,.js 2>/dev/null | grep "complexity" | head -20 || true

# Cognitive complexity — Python cyclomatic complexity (radon)
# Grade: A (1-5) fine, B (6-10) review, C (11-15) refactor, D-F (>15) block
python -m radon cc src/ -a -nb 2>/dev/null | head -30 || true

# Cognitive complexity — Python cognitive complexity (lizard)
python -m lizard src/ -C 15 2>/dev/null | head -20 || true
```

### Step 2 — Manual Pattern Scan

Check every file in scope for:

**🔴 HIGH — Blocks production, fix before anything else**
- Hardcoded credentials, API keys, tokens, or connection strings in source
- Empty or silent catch/except blocks
- Authentication enforced on client side only (no server-side check)
- `.env` file tracked by git
- SQL/NoSQL queries using string concatenation with user input
- Packages that may not exist (AI hallucinated dependencies)
- `eval()` or `exec()` on user-supplied input
- Floating promises: `.then()` chain with no `.catch()` and no `await`
- `process.exit()` called outside of a CLI entry point

**🟡 MEDIUM — Fix this sprint**
- `any` / untyped in TypeScript without justification comment
- `console.log` / `print()` left in production code paths
- Hardcoded configuration: URLs, limits, timeouts, magic numbers
- Functions over 50 lines doing multiple things
- Functions with cyclomatic complexity >10 — a function can be 40 lines with 18 independent execution paths and be completely unmaintainable (line count alone does not catch this)
- Database or API fetches inside loops (N+1)
- Same logic copy-pasted in multiple places (DRY violation)
- User endpoint with no input validation
- Broad exception catch: `except Exception:` / `catch (e: any)` with no specificity
- Missing error handling on async operations
- `readFileSync` / `writeFileSync` used in request handlers (blocks the event loop)
- External HTTP calls with no timeout configured (hangs forever on unresponsive upstream)
- External calls on critical paths with no retry logic — a single transient 500 from upstream fails the user permanently
- Non-critical dependency (cache, analytics, feature flag service) failure crashes the app instead of degrading gracefully
- List endpoints returning unbounded results with no `LIMIT` / `limit` parameter (pagination missing)
- Event listeners added without corresponding cleanup / removal (memory leak)

**🟡 MEDIUM — React-specific (skip if project has no React)**
- `key={index}` on list items — defeats reconciliation, causes subtle UI bugs
- `useEffect` with an empty `[]` dependency array that references props or state (stale closure)
- Data fetching directly inside a component body instead of a custom hook or data layer
- State mutations: `array.push()`, `object.key = value` directly on state variables
- Event handlers defined inline in JSX on every render without `useCallback`
- `dangerouslySetInnerHTML` without sanitizing content first

**🟢 LOW — Tech debt queue**
- Single-letter variable names outside loop counters
- Inconsistent naming convention in same file
- Commented-out code blocks
- Stale TODO/FIXME comments
- Missing return type annotations on exported functions
- Unused imports

### Step 3 — Architecture Smell Check

Flag if you see:
- Business logic in HTTP route handlers (routes should only route)
- HTTP response objects in service/business logic layer
- Config loaded inline at call sites instead of centrally
- No separation between external API calls and business logic
- God objects / files doing 5+ unrelated things (flag files over 300 lines in src/)
- Circular imports
- Deeply nested callbacks or conditionals (more than 3 levels) — extract to named functions

### Step 4 — Output the Report

```markdown
## vibe-hardener Audit Report
**Date:** [today]
**Project:** [name]

### Summary
🔴 HIGH: X  |  🟡 MEDIUM: Y  |  🟢 LOW: Z
Architecture: [CLEAN / NEEDS WORK / CRITICAL]

### 🔴 HIGH Priority
| File | Line | Issue | Fix |
|------|------|-------|-----|

### 🟡 MEDIUM Priority
| File | Line | Issue | Fix |
|------|------|-------|-----|

### 🟢 LOW Priority
[List]

### Architecture Notes
[Observations]

### Fix Order
1. [Most critical]
2. ...
```

---

## MODE 2: REFACTOR

**Trigger:** User asks to refactor, clean up, improve, or de-vibe specific code.

### Protocol

Before touching anything:
1. Read the code fully — understand what it does before changing it
2. State your refactoring plan — list every change you intend to make
3. Confirm with the user before executing (unless told "just do it")
4. Change one concern at a time
5. Preserve all existing behavior — refactoring changes structure, not function
6. Run or recommend tests after each step

### Mandatory Transformations

Apply these to every refactor, regardless of stack:

**1. Extract Configuration**

```typescript
// BEFORE (vibe)
const res = await fetch('https://api.openai.com/v1/chat/completions', {
  headers: { 'Authorization': 'Bearer sk-abc123' }
});

// AFTER (production)
// config/env.ts
export const config = {
  llmApiKey: process.env.LLM_API_KEY ?? (() => { throw new Error('LLM_API_KEY not set') })(),
  llmBaseUrl: process.env.LLM_BASE_URL ?? 'https://api.openai.com/v1',
};

// usage
import { config } from '../config/env';
const res = await fetch(`${config.llmBaseUrl}/chat/completions`, {
  headers: { 'Authorization': `Bearer ${config.llmApiKey}` }
});
```

```python
# Python equivalent
# config/settings.py
import os

def required(key: str) -> str:
    value = os.environ.get(key)
    if not value:
        raise ValueError(f"Required env var missing: {key}")
    return value

LLM_API_KEY: str = required('LLM_API_KEY')
```

**2. Add Error Boundaries**

```typescript
// BEFORE
async function fetchData(id: string) {
  const res = await fetch(`/api/data/${id}`);
  return res.json();
}

// AFTER
async function fetchData(id: string): Promise<DataResponse> {
  try {
    const res = await fetch(`/api/data/${id}`);
    if (!res.ok) {
      throw new Error(`API error: ${res.status} ${res.statusText}`);
    }
    return res.json() as Promise<DataResponse>;
  } catch (error) {
    logger.error('fetchData failed', { id, error });
    throw new Error(`Failed to fetch data: ${error instanceof Error ? error.message : 'unknown'}`);
  }
}
```

```python
# Python equivalent
async def fetch_data(id: str) -> DataResponse:
    try:
        async with session.get(f"/api/data/{id}") as response:
            response.raise_for_status()
            return await response.json()
    except aiohttp.ClientError as e:
        logger.error("fetch_data failed", extra={"id": id, "error": str(e)})
        raise RuntimeError(f"Failed to fetch data: {e}") from e
```

**3. Enforce Separation of Concerns**

```
Routes:    HTTP only — receive request, call service, return response
Services:  Business logic only — no HTTP, no framework coupling
Utils:     Pure functions only — no side effects, no external calls
Config:    Env loading only — validate at startup, export typed config
```

```typescript
// BEFORE — route handler doing everything
app.post('/users', async (req, res) => {
  const { email, password } = req.body;
  const hash = await bcrypt.hash(password, 10);
  const user = await db.query('INSERT INTO users (email, password_hash) VALUES (?, ?)', [email, hash]);
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  sendWelcomeEmail(email);
  res.json({ token, user });
});

// AFTER
// routes/users.ts
app.post('/users', validateBody(createUserSchema), async (req, res) => {
  try {
    const result = await userService.create(req.body);
    res.status(201).json({ success: true, data: result });
  } catch (error) {
    res.status(400).json({ success: false, error: error.message });
  }
});
```

**4. Replace Magic Values**

```typescript
// BEFORE
if (attempts > 3) { lockout(); }
setTimeout(retry, 5000);

// AFTER — config/constants.ts
export const MAX_LOGIN_ATTEMPTS = 3;
export const RETRY_DELAY_MS = 5_000;
```

**5. Add Input Types**

```typescript
// BEFORE
async function createUser(data: any) { ... }

// AFTER
interface CreateUserInput {
  email: string;
  name: string;
  role: 'admin' | 'user' | 'viewer';
}

async function createUser(input: CreateUserInput): Promise<User> { ... }
```

**6. Extract Database Access into a Repository Layer**

```typescript
// BEFORE (vibe — raw queries scattered across route handlers and services)
app.get('/users/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
  res.json(user.rows[0]);
});

// AFTER (production — queries isolated, testable, typed)
// repositories/userRepository.ts
export const userRepository = {
  findById: async (id: string): Promise<User | null> => {
    const result = await db.query<User>(
      'SELECT id, email, name, role, created_at FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0] ?? null;
  },
  create: async (input: CreateUserInput): Promise<User> => {
    const result = await db.query<User>(
      'INSERT INTO users (email, name, role) VALUES ($1, $2, $3) RETURNING id, email, name, role, created_at',
      [input.email, input.name, input.role]
    );
    return result.rows[0];
  },
};

// routes/users.ts
app.get('/users/:id', async (req, res) => {
  const user = await userRepository.findById(req.params.id);
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json({ success: true, data: user });
});
```

**7. Flatten Promise Chains to async/await**

```typescript
// BEFORE (vibe — hard to follow, error handling fragile)
function loadUserData(userId: string) {
  return fetchUser(userId)
    .then(user => {
      return fetchPermissions(user.id)
        .then(permissions => {
          return { user, permissions };
        });
    })
    .catch(err => {
      console.log(err);
    });
}

// AFTER (production — linear, typed, explicit error handling)
async function loadUserData(userId: string): Promise<{ user: User; permissions: Permission[] }> {
  try {
    const user = await fetchUser(userId);
    const permissions = await fetchPermissions(user.id);
    return { user, permissions };
  } catch (error) {
    logger.error('loadUserData failed', { userId, error });
    throw new Error(`Failed to load user data: ${error instanceof Error ? error.message : 'unknown'}`);
  }
}
```

```python
# Python equivalent — avoid callback-style patterns
# BEFORE
def load_user_data(user_id: str):
    user = fetch_user(user_id)  # no error handling
    permissions = fetch_permissions(user.id)
    return {"user": user, "permissions": permissions}

# AFTER
async def load_user_data(user_id: str) -> dict:
    try:
        user = await fetch_user(user_id)
        permissions = await fetch_permissions(user.id)
        return {"user": user, "permissions": permissions}
    except FetchError as e:
        logger.error("load_user_data failed", extra={"user_id": user_id, "error": str(e)})
        raise RuntimeError(f"Failed to load user data: {e}") from e
```

**8. Add Resilience Patterns**

Why this transformation exists: error handling (catch → log → rethrow) covers the case where *your code* throws. Resilience covers the case where *something downstream* is slow, unavailable, or returns garbage. AI agents handle the first case but almost never the second. A vibe-coded service that calls three external APIs will take down the request if any one of them is slow or flaky, because there is no timeout, no retry, and no fallback.

**Retry with exponential backoff + jitter**

```typescript
// ❌ WRONG — a single transient 500 from the upstream fails the user permanently
const result = await externalService.call(data);

// ✅ CORRECT — retries on transient failures, backs off to avoid hammering upstream
async function withRetry<T>(
  fn: () => Promise<T>,
  maxAttempts = 3,
  baseDelayMs = 100,
): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isLastAttempt = attempt === maxAttempts;
      if (isLastAttempt) throw error;
      // Exponential backoff with jitter — prevents thundering herd
      const delay = baseDelayMs * 2 ** (attempt - 1) + Math.random() * 100;
      logger.warn('Retrying after transient failure', { attempt, delayMs: delay, error });
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  throw new Error('unreachable');
}

// Usage
const result = await withRetry(() => externalService.call(data));
```

```python
# Python equivalent
import asyncio
import random

async def with_retry(fn, max_attempts: int = 3, base_delay_ms: float = 100):
    for attempt in range(1, max_attempts + 1):
        try:
            return await fn()
        except Exception as e:
            if attempt == max_attempts:
                raise
            delay = (base_delay_ms * (2 ** (attempt - 1)) + random.uniform(0, 100)) / 1000
            logger.warning("Retrying after transient failure", extra={"attempt": attempt, "delay_s": delay})
            await asyncio.sleep(delay)
```

**Timeout + fallback**

```typescript
// ❌ WRONG — hangs indefinitely if upstream never responds
const data = await fetch(upstreamUrl).then(r => r.json());

// ✅ CORRECT — bounded wait, defined behaviour on timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3_000); // 3s max
try {
  const res = await fetch(upstreamUrl, { signal: controller.signal });
  if (!res.ok) throw new Error(`Upstream returned ${res.status}`);
  return await res.json() as UpstreamResponse;
} catch (error) {
  if ((error as Error).name === 'AbortError') {
    logger.warn('Upstream timeout — using fallback', { url: upstreamUrl });
    return FALLBACK_VALUE; // a handled degradation, not an error
  }
  throw error;
} finally {
  clearTimeout(timeoutId);
}
```

**Graceful degradation**

```typescript
// ❌ WRONG — Redis being down takes the whole feature down
const permissions = JSON.parse(await redis.get(`perms:${userId}`) ?? 'null');
return permissions;

// ✅ CORRECT — cache is a performance optimisation, not a dependency
let permissions: Permission[] | null = null;
try {
  const cached = await redis.get(`perms:${userId}`);
  permissions = cached ? JSON.parse(cached) : null;
} catch (cacheError) {
  // Cache unavailable — not a crash, a fallback to source of truth
  logger.warn('Cache unavailable, falling back to DB', { userId });
}
permissions ??= await permissionRepository.findByUserId(userId);
return permissions;
```

**Rules:**
- Retry only on transient errors (network errors, 429, 503) — never retry 400/401/404 (those will never succeed)
- Always add jitter to backoff — without it, every client retries at the same moment (thundering herd)
- Every external call must have a timeout — the default for most HTTP clients is no timeout
- Non-critical dependencies (cache, feature flags, analytics) must degrade gracefully — their failure must not crash the app

---

**Transformation 9 — Black-Box Interface Design (Replaceability Principle)**

Every module should be replaceable without touching its callers. If swapping an implementation (e.g., PostgreSQL → DynamoDB, Redis → in-memory store, Stripe → Paddle) requires changes in multiple call sites, the abstraction is leaking. Identified by Eskil Steenberg's principle: design for replaceability, not for reuse.

**Signs of leaky abstraction:**
- Service function accepts a `db: PrismaClient` argument (caller is aware of the ORM)
- Route handler imports `stripe` directly and calls `stripe.charges.create()` (no payment service layer)
- Multiple files import the same third-party SDK directly (tight coupling, hard to swap or mock)

**Transformation pattern:**

```typescript
// ❌ WRONG — callers are coupled to the Stripe SDK shape
import Stripe from 'stripe';
async function createCharge(stripe: Stripe, amount: number, token: string) {
  return stripe.charges.create({ amount, currency: 'usd', source: token });
}

// ✅ CORRECT — callers depend on a stable interface, not a vendor SDK
interface PaymentProvider {
  charge(amount: number, token: string): Promise<{ id: string; status: string }>;
}

class StripePaymentProvider implements PaymentProvider {
  constructor(private readonly client: Stripe) {}
  async charge(amount: number, token: string) {
    const result = await this.client.charges.create({ amount, currency: 'usd', source: token });
    return { id: result.id, status: result.status };
  }
}

// Callers only know about PaymentProvider — swap Stripe for Paddle without touching them
```

```python
# ❌ WRONG — business logic is coupled to boto3's S3 interface
import boto3

async def save_document(bucket: str, key: str, content: bytes) -> None:
    s3 = boto3.client("s3")
    s3.put_object(Bucket=bucket, Key=key, Body=content)

# ✅ CORRECT — stable interface, storage backend is swappable
from abc import ABC, abstractmethod

class DocumentStore(ABC):
    @abstractmethod
    async def save(self, key: str, content: bytes) -> None: ...

class S3DocumentStore(DocumentStore):
    def __init__(self, bucket: str) -> None:
        self._bucket = bucket
        self._client = boto3.client("s3")

    async def save(self, key: str, content: bytes) -> None:
        self._client.put_object(Bucket=self._bucket, Key=key, Body=content)
```

**Rules:**
- Each external vendor (database, cache, payment, storage, email) gets one wrapper class that translates the vendor's API to your domain's interface
- No route handler or service function should import a vendor SDK directly
- The interface should be defined in terms of your domain, not the vendor's (return `{ id, status }`, not `Stripe.Charge`)
- If a module cannot be unit-tested with a fake/stub without starting the real service, the interface is leaking

---

**Transformation 10 — Generate Linting Config (if missing)**

If a project has no linter configured, generate one as part of the refactor. Code without a linter accumulates style drift and misses whole categories of bugs that static analysis catches for free. Competitors (Cursor rules repos, production AGENTS.md standards) include linting setup as a prerequisite — vibe-hardener should too.

**TypeScript/Node — `eslint.config.mjs`:**

```javascript
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // Complexity — catches unmaintainable functions line count misses
      'complexity': ['error', { max: 10 }],
      // No untyped any
      '@typescript-eslint/no-explicit-any': 'error',
      // No floating promises
      '@typescript-eslint/no-floating-promises': 'error',
      // No unused variables
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      // Require explicit return types on exported functions
      '@typescript-eslint/explicit-module-boundary-types': 'error',
    },
  },
  {
    ignores: ['dist/**', 'node_modules/**', '**/*.test.ts', '**/*.spec.ts'],
  },
);
```

Install: `npm install --save-dev eslint @eslint/js typescript-eslint`

**Python — `pyproject.toml` (ruff section):**

```toml
[tool.ruff]
target-version = "py311"
line-length = 100
src = ["src"]

[tool.ruff.lint]
select = [
  "E",    # pycodestyle errors
  "W",    # pycodestyle warnings
  "F",    # Pyflakes (undefined names, unused imports)
  "I",    # isort
  "B",    # flake8-bugbear (common bugs)
  "C90",  # McCabe complexity
  "UP",   # pyupgrade (modern Python syntax)
  "S",    # bandit security rules
  "RUF",  # Ruff-specific rules
]
ignore = [
  "S101",  # allow assert in tests
]

[tool.ruff.lint.mccabe]
# Cyclomatic complexity threshold — matches the radon/lizard scan in MODE 1
max-complexity = 10

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S", "B"]
```

Install: `pip install ruff` — runs as both linter and formatter (`ruff check .` + `ruff format .`)

---

### What NOT to Refactor

Do not:
- Extract functions that are only called once and add no clarity
- Add abstraction layers for their own sake
- Change algorithm logic unless there's a clear bug
- Rename things that are already clear
- Touch code outside the stated scope

---

## MODE 3: SECURITY REVIEW

**Trigger:** User wants a security review or is about to deploy.

### Scan Commands

```bash
# Git history secrets check
git log --all -p | grep "^+" | grep -iE "(api_key|apikey|password|secret|token|sk-|private_key)" | head -30

# Check .env in git tracking
git ls-files | grep "\.env$"

# CORS configuration
grep -rn "cors(" . --include="*.js" --include="*.ts"
# Flag any: cors() with no options, or Access-Control-Allow-Origin: *

# Rate limiting presence
grep -rn "rateLimit\|rate.limit\|throttle\|slowDown" . --include="*.js" --include="*.ts"

# Helmet / security headers (Node)
grep -rn "helmet" . --include="*.js" --include="*.ts"

# npm audit
npm audit --audit-level=high

# Python: pip-audit or safety
pip-audit

# SSRF: user-controlled URLs passed to fetch/http/requests
grep -rn "fetch(\|axios.get(\|requests.get(" . --include="*.ts" --include="*.js" --include="*.py" \
  | grep -v "config\.\|env\.\|BASE_URL\|process.env" | head -20

# CSRF protection presence (csurf, csrf-csrf, or equivalent)
grep -rn "csrf\|csurf\|doubleCsrf" . --include="*.ts" --include="*.js" 2>/dev/null | head -10

# Content-Security-Policy header
grep -rn "Content-Security-Policy\|contentSecurityPolicy" . --include="*.ts" --include="*.js" 2>/dev/null | head -5

# Cookie flags — missing httpOnly/secure/sameSite
grep -rn "res.cookie\|setCookie\|set-cookie" . --include="*.ts" --include="*.js" \
  | grep -v "httpOnly\|HttpOnly" | head -20

# Path traversal: user input used in file paths
grep -rn "path.join\|__dirname\|readFile\|createReadStream" . \
  --include="*.ts" --include="*.js" | grep "req\.\|param\|query\|body" | head -20
```

### The Checklist

**🚨 CRITICAL — Block deploy until fixed**

```
□ No secrets in source files (scan above)
□ No secrets in git history (scan above)  
□ .env not tracked by git
□ All required env vars validated at startup (fails fast, not silently in production)
□ CORS not set to wildcard (*) in production
□ Auth enforced on server side — not just client side
□ No SQL/NoSQL queries using string concatenation with user input
□ No eval() or exec() on user input
□ No user-controlled URLs passed directly to fetch/http/requests (SSRF)
□ File path operations do not use unvalidated user input (path traversal)
```

**🔴 HIGH — Fix before merge**

```
□ Input validation on all user-facing endpoints
□ Parameterized queries / ORM used for all DB operations
□ Authentication middleware applied to all protected routes
□ Authorization checked in service layer (not only at route level)
□ CSRF protection on all state-mutating endpoints (POST/PUT/PATCH/DELETE) that use cookie auth
□ Content-Security-Policy header configured — prevents XSS escalation even if injection occurs
□ Error messages don't expose stack traces or internal paths to client
□ No tokens, passwords, or PII in log statements
□ All new npm/pip packages verified to exist (not AI hallucinated)
□ npm audit / pip-audit — no new critical or high CVEs introduced
□ Rate limiting on auth endpoints (login, register, password reset)
□ LLM prompts: user-supplied content sanitized, not injected directly
□ Auth token/secret comparisons use timing-safe equality (crypto.timingSafeEqual / hmac.compare_digest), not ===  / ==
```

**🟡 MEDIUM — Fix this sprint**

```
□ HTTP security headers: helmet() or equivalent configured
□ File uploads (if any): MIME type whitelist + size limit + no path traversal
□ Session/JWT: expiry configured, not set to never-expire
□ HTTPS enforced in production (not just available)
□ Dependency lockfile committed and up to date
□ No hardcoded fallback credentials for "development convenience"
□ Cookies set with httpOnly=true, secure=true, sameSite='strict' (or 'lax' minimum)
□ Session cookies not accessible from JavaScript (httpOnly prevents XSS token theft)
```

### Output Format

```markdown
## Security Review
**Date:** [today]
**Project:** [name]

### 🚨 CRITICAL (Block Deploy)
[Each issue with location and fix]

### 🔴 HIGH (Fix Before Merge)
[Each issue]

### 🟡 MEDIUM (Fix This Sprint)
[Each issue]

### ✅ Passed
[What's clean]

### Verdict
[ ] PASS — Safe to deploy
[ ] CONDITIONAL — Fix HIGH issues then deploy
[ ] BLOCKED — Critical issues must be resolved
```

### Pre-Commit Hook Setup

Pre-commit hooks prevent secrets and lint violations from reaching the remote at all — cheaper than catching them in CI. If the project has no pre-commit configuration, recommend setting one up.

**Node/TypeScript projects:**

```bash
# Install husky and lint-staged
npm install --save-dev husky lint-staged

# Initialize husky
npx husky init
```

`.husky/pre-commit`:
```sh
#!/bin/sh
npx lint-staged
```

`package.json` (lint-staged section):
```json
{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "eslint --fix --max-warnings=0",
      "prettier --write"
    ]
  }
}
```

**Python projects (pre-commit framework):**

```bash
pip install pre-commit
```

`.pre-commit-config.yaml`:
```yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks
        name: Detect secrets (gitleaks)

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-added-large-files
        args: [--maxkb=500]
      - id: check-merge-conflict
      - id: detect-private-key
```

```bash
pre-commit install       # installs hooks into .git/hooks
pre-commit run --all-files  # run against existing files
```

**Hooks to always include:**
- **Secret detection** (gitleaks or detect-secrets): catches API keys, tokens, connection strings before push
- **Linter** (ESLint / ruff): prevents style and correctness issues from entering review
- **Large file check**: prevents accidentally committing binaries, datasets, or model weights
- **Merge conflict marker check**: prevents half-resolved conflicts from reaching the remote

---

## MODE 4: SPEC-DRIVEN DEVELOPMENT

**Trigger:** User wants to build something new, or describes a feature to implement.

**Rule:** Refuse to write production code until a spec exists and is approved.

### Step 1 — Interview

Ask these questions one at a time. Do not ask all at once. Adapt based on answers:

1. What does a user see or experience when this feature is done?
2. Why does this exist? What business/product problem does it solve?
3. How do we know it's done? Give me 3-5 testable acceptance criteria.
4. What must this explicitly NOT do? (Scope boundary)
5. What existing code does this touch or depend on?
6. Any performance targets? (latency, throughput, scale)
7. Any security, compliance, or integration constraints?
8. If this ships and breaks something, how do we roll it back?

### Step 2 — Generate the Spec

Output this file to `specs/YYYY-MM-DD-feature-name.md`:

```markdown
# Feature: [Name]
**Date:** [today]
**Status:** Draft → Approved → In Progress → Done

## What
[User-facing behavior in plain English]

## Why
[Business justification. One paragraph.]

## Acceptance Criteria
- [ ] [Specific, testable criterion 1]
- [ ] [Specific, testable criterion 2]
- [ ] [Specific, testable criterion 3]

## Out of Scope
- [Explicitly excluded thing 1]
- [Explicitly excluded thing 2]

## Non-Functional Requirements
- **Performance:** [p95 latency target, throughput, or "none specified"]
- **Availability:** [uptime requirement or "none specified"]
- **Scale:** [expected request volume / data size or "none specified"]

## Technical Constraints
- [Integration requirement if any]
- [Security requirement if any]

## Edge Cases
- [Edge case 1] → [Expected behavior]
- [Edge case 2] → [Expected behavior]

## API Contract (if this feature adds or changes endpoints)
```
METHOD /path
Request:  { field: type, field: type }
Response: { field: type, field: type }
Errors:   400 [reason], 401 [reason], 404 [reason]
```

## Data / API Changes
- Schema changes: [table/collection, migration needed: yes/no]
- New env vars: [VAR_NAME=description, or "None"]
- Breaking changes: [yes/no — if yes, migration plan required]

## Rollback Plan
[How to revert this feature if it causes problems in production]
- Feature flag: [yes/no]
- DB migration reversible: [yes/no — if no, explain why it is safe]
- Rollback steps: [ordered list, or "revert commit + redeploy"]
```

### Step 3 — Gate on Approval

Do not proceed. Say:

> "Spec is ready. Review it and tell me to proceed when it looks right. We implement one acceptance criterion at a time."

### Step 4 — Implement Incrementally

Implement criterion 1. Show the diff. Ask for verification. Only then proceed to criterion 2.

Never implement multiple criteria in one shot unless explicitly asked.

---

## MODE 5: PRE-PR REVIEW

**Trigger:** User is about to create a PR, says "review before I push," or asks for a review.

### Walk Through This Checklist

Report PASS / FAIL on each item. Fail = block until fixed.

**Code Quality**
```
□ All acceptance criteria implemented and manually verifiable
□ TypeScript: tsc --noEmit passes (zero type errors)
□ Linter passes (ESLint / Pylint / Ruff / equivalent)
□ No console.log / print() in production paths
□ No commented-out code blocks in the diff
□ No stale TODO comments in changed files
□ Error handling on every async operation in the diff
□ Functions under 50 lines
□ No duplicate logic introduced
□ Types / return types on exported functions
```

**Architecture**
```
□ Business logic not in route handlers
□ Config not hardcoded — uses environment variables
□ Separation of concerns preserved
□ No new circular dependencies introduced
□ Any new env vars documented in .env.example with description
□ Any new env vars added to deployment platform config (Vercel / Railway / Fly.io / etc.)
```

**Security**
```
□ No secrets in the diff: git diff main...HEAD | grep "^+" | grep -iE "(key|secret|token|password)"
□ Input validation on any new user endpoints
□ Auth checked on any new protected routes
□ npm audit: no new critical/high vulnerabilities
```

**API Design**
```
□ New endpoints use correct HTTP status codes (201 for creates, 204 for deletes, 4xx for client errors)
□ Error responses use the project's standard shape (not ad-hoc { message } or { error })
□ New list endpoints have pagination (limit + cursor or offset)
□ Endpoints that create resources or have side effects have idempotency key support
□ Breaking changes to existing endpoints: new API version created, old version not removed yet
□ New public endpoints documented in OpenAPI spec or equivalent
```

**Performance**
```
□ No new list endpoints without pagination (limit parameter + max cap)
□ No new queries on columns that are not indexed (check with EXPLAIN ANALYZE)
□ No new event listeners added without cleanup
□ No new setInterval without clearInterval
□ No new whole-library imports on frontend (check bundle impact)
□ No new unbounded in-memory collections (Maps/Sets with no eviction)
```

**Testing**
```
□ New business logic has unit tests covering happy path and error paths
□ New API endpoints have integration tests covering: 200/201, 400, 401/403, 404
□ Tests pass locally: npm test / pytest
□ No tests skipped or marked .only / .skip without explanation
□ Coverage did not decrease (run: npx jest --coverage or pytest --cov)
□ Test names are descriptive — a failing test name explains what broke
```

**Observability**
```
□ No raw console.log / print() added — structured logger used throughout
□ New code paths log meaningful INFO events (not just errors)
□ Every new error path logs with enough context to debug without reproducing
□ No PII logged (passwords, tokens, card numbers, SSNs, emails unless explicitly required)
□ New service or significant feature: /health endpoint checks any new dependency added
□ New service: error tracker initialized and SENTRY_DSN (or equivalent) in .env.example
```

**Breaking Changes**
```
□ No exported function/type signatures changed in a backwards-incompatible way
  (check with: git diff main...HEAD -- "*.ts" | grep "^-export")
□ If a public API changed: callers identified and updated, or versioned endpoint added
□ Database migration file present if schema changed
□ No removal of existing required env vars without documentation update
```

**Resilience**
```
□ All new external HTTP calls have a timeout (AbortController / httpx timeout)
□ External calls on critical user paths have retry with exponential backoff and jitter
□ Non-critical dependencies (cache, feature flags, analytics) have a fallback — their
  failure must not crash the app or fail the request
□ No new synchronous blocking call on the request path without a timeout
```

**Dependencies**
```
□ No new packages added without justification (could a native API do this?)
□ Any new package: license checked — no GPL/AGPL in commercial projects
□ Any new package: `npm audit` / `pip-audit` shows no new critical/high CVEs
□ Lockfile committed and up to date (package-lock.json / poetry.lock / etc.)
□ New packages in correct section: runtime deps in dependencies, build/test tools in devDependencies
□ No floating versions (*  or latest) for production dependencies
```

**Git**
```
□ Commit messages follow Conventional Commits (feat:, fix:, refactor:, docs:, chore:)
□ PR description explains what changed and why (not just "updated files")
□ No merge commits — rebased on main
□ No unrelated changes in this PR
```

### Output Format

```markdown
## Pre-PR Review — [feature/branch]

### ✅ Passed
[List]

### ❌ Must Fix Before Merge
[Each issue with file:line and what to do]

### ⚠️ Suggestions (non-blocking)
[Optional improvements]

### Verdict
[ ] APPROVED — Merge when ready
[ ] CHANGES REQUESTED — Fix items above first
```

---

## MODE 6: STANDARDS (Always-On)

**Trigger:** This mode is active passively whenever vibe-hardener is loaded. Apply these rules to every piece of code you write.

### Universal Rules (Language-Agnostic)

- **Never hardcode configuration.** Any value that differs between environments goes in env/config.
- **Never use console.log in production code.** Use a structured logger. `console.log` is not searchable, not filterable, and not alertable.
- **Every significant operation must be observable.** If something goes wrong in production and you can't diagnose it from logs alone, the code isn't done.
- **New business logic requires tests.** A

…(truncated)
