Broke After Deploy: find what will break in production, before or after you ship
Run it before you deploy and it lists what will break on your host, with the file, the line and the fix. Paste a production error and it finds the cause in your code, then checks what will break next.
Free during launch. Works with Claude Code, Claude.ai, Cursor and Codex.
When to use it
Use it in the hour before a launch, or the hour after one went wrong. It covers JavaScript and TypeScript apps in depth (Next.js, Vite, React, Node, Express and others) and Python web apps (Django, FastAPI, Flask) at a basic level.
- "Is this ready to deploy?" or "Will this work in production?" before a first deploy.
- "It works on my laptop but the Vercel build fails with
Module not found." - "The site is live but the API calls fail" (
ECONNREFUSED 127.0.0.1, CORS blocked). - "My Render or Railway deploy never goes live" (health check failed, wrong port).
- "Every deploy wipes my data" (SQLite or uploads on a disk the host throws away).
- "Refreshing a page gives 404",
redirect_uri_mismatch,EROFS read-only file system,Prisma P2021,ModuleNotFoundError.
What you type:
| You have | What you type | What runs |
|---|---|---|
| Nothing broken yet | "check this before I deploy" | Scan (scripts/scan.py) |
| An error, a build log, or "it broke" | Paste the error and say "my site broke after deploying" | Diagnose (scripts/diagnose.py --project), then Scan |
If you say it broke but don't paste the error, Claude asks for it in one line (browser console, build log or the host's logs) and runs the scan meanwhile. You can also type /broke-after-deploy.
How it works
- The script reads your project.
scan.pydetects the framework and the host from config files (vercel.json,render.yaml,railway.json,fly.tomland others), then checks the code for 41 known reasons an app works locally and breaks in production. In Diagnose mode,diagnose.pyfirst matches your error against the known error messages, then checks your code for the likely causes, so a cause it can point at in your code ranks first. Both scripts are read-only and use only Python's standard library. - You get a first pass within about a minute. The verdict line word for word, the host it detected, the top 3 "will break" items (what, file:line, one-line fix) and a "Not covered" line if the scan was incomplete. It is labelled "First pass, confirming now".
- Claude confirms every finding. It opens each "will break" line, drops wrong ones and says so. Then it walks
references/review-checklist.mdover the files that run in production, starting from the rows for your host. The checklist catches what patterns miss, such as a localhost URL built from constants or a helper in another file. - The verdict is strict. An item is "will break" only when the repo itself proves it fails on your host. Anything that depends on settings it can't see (your dashboard variables, for example) is "check". Each cause lists safe look-alikes, like a localhost URL inside a development-only branch, and Claude rules those out before reporting. A scan that was incomplete is never reported as clean.
- Fixes come as diffs. One diff per "will break" item, applied only after you say yes. Fixes outside the code (dashboard variables, OAuth callback URLs, migrations against production, health check paths) come as exact steps. The production build command is offered last, as an update.
What it checks
41 causes in total. The code is checked for most of them; for the rest it tells you where to look.
Config and secrets
- Environment variables your code reads that no production config defines; Vite variables without
VITE_or Next.js client variables withoutNEXT_PUBLIC_(always undefined in the browser). - Hardcoded test keys in production, or live keys in development.
- Config that only exists in a gitignored
.envfile.
Hardcoded local values
- Code that calls
localhostor127.0.0.1in production, including URLs built from constants. - Servers on a fixed port with no
PORT, or bound to localhost (including Fastify's default and Python's uvicorn without--host). http://requests from an https page, paths from your own computer (/Users/,C:\).- OAuth, magic-link and webhook callbacks pointing at localhost or ngrok.
Files and the filesystem
- Imports and asset paths whose capitalization differs from the file on disk or in git.
- File writes, SQLite files and local uploads on hosts that don't keep the disk.
- Imports of files git won't ship;
.dockerignorepatterns that drop files the app needs.
Dependencies and build
- Missing or conflicting lockfiles,
npm ciin a pnpm or yarn repo. - Node or Python version mismatches; runtime packages listed only in
devDependencies; imports missing frompackage.jsonorrequirements.txt. - Native packages built for the wrong platform;
psycopg2on slim images with no compiler. - A start command that needs a build step that never runs; Django
collectstaticnever run.
Database
- Schema changes with no migrate step in the deploy.
- Database URLs that fall back to localhost; a new connection pool per request.
Network, auth and runtime
- CORS allowlists and Django
ALLOWED_HOSTS/CSRF_TRUSTED_ORIGINSwith only localhost. - Cookies production browsers reject (
secure: false,SameSite=NonewithoutSecure, cross-site sessions). - Node-only modules in edge code; timers, cron jobs and websockets on serverless hosts.
- "Today" computed in server local time; build scripts that only work on a Mac.
- Single-page apps with no fallback rewrite (404 on refresh); health check paths no route serves.
Python (basic)
- Requirements, Python version pins, localhost and database URLs, local paths, SQLite, file writes, Django settings (
ALLOWED_HOSTS, CSRF,DEBUG, collectstatic, media storage), gunicorn and uvicorn binding, migrations.
What it can't see
- Your hosting dashboard: which variables are set for Production, the runtime version, function limits.
- Your live database: whether migrations ran, IP allowlists.
- Real traffic: timeouts, rate limits, unusual user data, stale caches. For these, and for DNS and certificate problems, it names where to look.
- Python problems beyond the basics above (type errors, import cycles, Celery or worker setup).
- Other stacks (Go, Ruby, PHP, Java and others): the scan lists them as not analysed and says the scan was incomplete.
Worked examples
1. Diagnose: the Vercel build fails with "Module not found"
A Next.js dashboard on Vercel. It runs fine with npm run dev on a Mac. The Vercel build fails. The developer pastes the build log.
import Header from './components/header';
import { getStats } from '@/lib/stats';
lib/stats.ts
const API_BASE = 'http://localhost:3000';
export async function getStats() {
const res = await fetch(`${API_BASE}/api/stats`, { cache: 'no-store' });
**Most likely cause: An import's capitalization doesn't match the file name. Found in your code at app/page.tsx:1.** Project: next · host: vercel (vercel.json or .vercel/ found) · 6 source files scanned ## Most likely: An import's capitalization doesn't match the file name - **Matched:** Module not found: Can't resolve './components/header'; "module not found"; "can't resolve" #### 1. Import './components/header' doesn't match the file name 'app/components/Header.tsx' - **Evidence:** `app/page.tsx:1` `import Header from './components/header';` - **Impact:** Mac and Windows treat Logo.tsx and logo.tsx as the same file. Linux build servers don't, so an import that works on your laptop is 'not found' in the build. - **Fix:** Change the import to './components/Header' (capitalization must match exactly on Linux). - Cause #11: An import's capitalization doesn't match the file name ## Also possible (#2): The production build fails, but the dev server hid it … ## Also possible (#3): Your code imports a file that git ignores, so it never gets deployed … - Checked your code: nothing found for this cause.Script output (trimmed): scan.py, run next for "what breaks next"
**3 things will break in production, plus 1 to check.**
Project: next · host: vercel (vercel.json or .vercel/ found) · 6 source files scanned · 0.0s
## Will break in production
### 1. Import './components/header' doesn't match the file name 'app/components/Header.tsx'
…
### 2. Hardcoded http://localhost:3000
- **Evidence:** `lib/stats.ts:1` `const API_BASE = 'http://localhost:3000';`
- **Impact:** localhost means 'this machine'. In production it points at the server itself or at each visitor's own computer, so the call fails.
- **Fix:** Use an environment variable (for example NEXT_PUBLIC_API_URL) or a relative path like /api/... instead.
- Cause #6: Your code calls localhost
### 3. Hardcoded http://localhost:3000/api/stats
- **Evidence:** `lib/stats.ts:4` `const res = await fetch(`${API_BASE}/api/stats`, { cache: 'no-store' });`
…
## Check these
### 4. Node version isn't pinned, so the host uses its default
…
What the skill tells Claude to do next: open app/page.tsx line 1 and confirm the import really points at Header.tsx, then lead with one line: "Most likely cause: the import's capitalization doesn't match the file name, in app/page.tsx line 1." The two "Also possible" causes had nothing in the code, so they stay in the background.
Then the scan, as a separate "Will also break" list. Items 2 and 3 are the same mistake (the second is the first one used), so they become one finding: the server component fetches localhost:3000, which on Vercel is not your app. Claude would then walk the checklist rows for Vercel over the route and the lib folder before posting the confirmed list. The Node version item stays a "check".
app/page.tsx - import Header from './components/header'; + import Header from './components/Header'; lib/stats.ts - const API_BASE = 'http://localhost:3000'; + // Set STATS_API_URL in Vercel for Production and Preview. + const API_BASE = process.env.STATS_API_URL;
2. Scan: a FastAPI app on Render, before the first deploy
A small notes API in FastAPI with SQLAlchemy and SQLite, deployed to Render with a render.yaml blueprint. Python gets basic checks, and the report says so.
buildCommand: pip install -r requirements.txt
startCommand: uvicorn app.main:app --host 127.0.0.1 --port 8000
healthCheckPath: /health
app/db.py
DATABASE_URL = "sqlite:///./notes.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
app/main.py
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
**3 things will break in production. Not a complete scan (Python checks are basic), so there may be more.** Project: fastapi · host: render (render.yaml found) · 4 source files scanned · 0.0s **Not covered:** - Python support is basic: requirements, Python version, localhost and database URLs, local paths, SQLite, file writes, Django settings, gunicorn/uvicorn binding and migrations are checked. Other Python problems (type errors, import cycles, Celery/worker setup, framework-specific config) are not. ## Will break in production ### 1. Your app uses a SQLite database file. render wipes the server's disk on every deploy or restart (no disk in render.yaml). - **Evidence:** `app/db.py:4` `DATABASE_URL = "sqlite:///./notes.db"` … - Cause #13: You use a SQLite file as your database on serverless hosting ### 2. CORS only allows http://localhost:5173, so your production site's requests are blocked - **Evidence:** `app/main.py:13` `allow_origins=["http://localhost:5173"],` … - Cause #26: Your API only allows requests from localhost ### 3. The server listens on 127.0.0.1, so the host's proxy can't reach it - **Evidence:** `render.yaml:6` `uvicorn app.main:app --host 127.0.0.1 --port 8000` - **Impact:** Hosts like Render, Railway, Fly and Heroku tell your app which port to use through the PORT variable and reach it from outside the container. An app hardcoded to port 3000 or bound to 127.0.0.1 is unreachable. - **Fix:** Use --host 0.0.0.0 --port $PORT. - Cause #7: Your server listens on a fixed port or only on localhost …
What the skill tells Claude to do next: post the verdict line as is, including "Not a complete scan", so you never read this as a clean bill of health for a Python app. Then confirm each item: SQLite is not a fallback here (it is hardcoded and render.yaml has no disk), and the only CORS origin is localhost. The health check path /health is served by the app, so there is nothing to report there.
Then it walks the Python rows of the checklist (binding, version pins, requirements, migrations, CORS) over app/ and render.yaml. The verdict comes first: "3 things will break in production", each with file, line and fix.
render.yaml - startCommand: uvicorn app.main:app --host 127.0.0.1 --port 8000 + startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT envVars: - key: PYTHON_VERSION value: 3.11.9 + - key: DATABASE_URL + fromDatabase: + name: notes-db + property: connectionString + - key: FRONTEND_ORIGIN + value: https://notes.example.com + databases: + - name: notes-db app/db.py + import os - DATABASE_URL = "sqlite:///./notes.db" + DATABASE_URL = os.environ["DATABASE_URL"] app/main.py + import os - allow_origins=["http://localhost:5173"], + allow_origins=os.environ.get("FRONTEND_ORIGIN", "http://localhost:5173").split(","), requirements.txt + psycopg2-binary==2.9.9
3. Scan: an Express API on Railway, with a safe localhost it should not flag
An invoice API in Express. localhost appears twice in the config, but only in the branch that runs outside production. That is a known look-alike, not a bug.
const isProd = process.env.NODE_ENV === 'production';
export function publicBaseUrl() {
if (isProd) {
return process.env.PUBLIC_URL;
}
return 'http://localhost:4000';
}
railway.json and src/server.js
"healthcheckPath": "/healthz"
app.get('/health', (req, res) => res.json({ ok: true }));
const port = process.env.PORT || 4000;
**1 thing will break in production, plus 3 to check.** Project: express · host: railway (railway config found) · 4 source files scanned · 0.0s ## Will break in production ### 1. The railway health check calls /healthz, but no route in the app serves it, so every deploy fails its health check - **Evidence:** `railway.json:5` `"healthcheckPath": "/healthz"` - **Impact:** Render, Fly, Railway and App Platform only send traffic to a new deploy after its health check URL answers 200. If that path doesn't exist in the app (a typo, /healthz vs /health), every deploy fails its health check and is never promoted, even though the app itself starts fine. - **Fix:** Point the health check at a route that exists (or add a /healthz route that returns 200). - Cause #39: The host's health check calls a path your app doesn't serve ## Check these ### 2. PUBLIC_URL is used but not defined in any env file here - **Evidence:** `src/config.js:5` `return process.env.PUBLIC_URL;` … - Cause #1: An environment variable your code uses isn't set in production - And 2 more variable(s) like it: FRONTEND_ORIGIN, DATABASE_URL …
What the skill tells Claude to do next: confirm the health check item (the app serves /health, Railway calls /healthz), and leave the two localhost strings alone. Both sit in the non-production branch of a NODE_ENV === 'production' check, which the checklist lists as a safe look-alike, and the script did not flag them either. The server already reads process.env.PORT, so the port is fine.
The three variables stay "check", not "will break": the repo can't show what is set in the Railway dashboard. Claude says so in one line and names them: PUBLIC_URL, FRONTEND_ORIGIN, DATABASE_URL.
railway.json - "healthcheckPath": "/healthz" + "healthcheckPath": "/health"
Results
| Test | Planted bugs | With the skill | Plain Claude |
|---|---|---|---|
| Blind test, JavaScript and TypeScript projects (Oct 13) | 33 | All 33 caught, 0 false alarms | 94% caught, 11 false alarms |
| Harder test (Oct 14) with Python, Bun, Deno, Docker and monorepo projects | 26 | 19 caught as will-break, 0 false alarms | 23 caught, 0 false alarms |
Install Broke After Deploy
Download broke-after-deploy.zip Free during launch. Checksums
python3 already; on Windows install it from python.org. Claude Code
- Download
broke-after-deploy.zip(button above). - Unzip it into your personal skills folder so it works in every project:
Or into one project only (commit it to share it with your team):unzip ~/Downloads/broke-after-deploy.zip -d ~/.claude/skills/unzip ~/Downloads/broke-after-deploy.zip -d .claude/skills/ - Check it landed:
ls ~/.claude/skills/broke-after-deploy/SKILL.md - Use it. Claude Code picks up new skills without a restart (if
~/.claude/skillsdid not exist when your session started, run/reload-skills). Ask in plain words, for example “check this before I deploy” or paste the error from your host's logs, or type/broke-after-deploy.
Claude.ai (web and desktop app)
- Open Settings > Capabilities and make sure Code execution and file creation is on. Skills need it. (Available on Pro, Max, Team and Enterprise plans; on Enterprise an Owner enables Skills first.)
- In the Skills section, click Upload skill and choose
broke-after-deploy.zip. Do not unzip it. - Check that it appears in your Skills list and switch it on.
- Start a chat, attach your project as a zip (or paste the error or alert) and ask, for example “check this before I deploy”. In Claude.ai the scripts run in Claude’s sandbox on the files you upload, not on your computer.
Cursor
- Unzip into Cursor’s skills folder for all projects:
or into a project:unzip ~/Downloads/broke-after-deploy.zip -d ~/.cursor/skills/.cursor/skills/. Cursor also reads~/.claude/skills/, so a Claude Code install works in Cursor too. - In Agent chat, type
/and pick broke-after-deploy, or just ask: “check this before I deploy”.
OpenAI Codex
- Unzip into your user skills folder:
or intounzip ~/Downloads/broke-after-deploy.zip -d ~/.agents/skills/.agents/skills/at the root of a repo. - Mention it with
$broke-after-deploy(or pick it from/skills), or just ask: “check this before I deploy”.
Other agents, updates and removal
- Any agent that reads
SKILL.mdfiles: unzip into its skills folder. Each skill is a folder withSKILL.md,scripts/andreferences/. - Update: download the new zip and unzip it over the old one with
unzip -o. - Remove: delete
broke-after-deploy/from the skills folder. - Check the download:
shasum -a 256 broke-after-deploy.zipand compare with SHA256SUMS.txt.
FAQ
Does it change my code?
No, not without asking. The scripts are read-only. Fixes are shown as diffs, one per item, and applied only after you say yes. It adds no files unless you ask it to keep a record of runs, in which case it appends one line per run to .techfix/ledger.md in your project.
Does my code leave my machine?
The scripts run locally, use only Python's standard library and make no network calls (they run git locally to see which files are tracked). They never print a full secret: keys and passwords in evidence lines are shortened. Your agent reads the files it reviews, the same as any other request you make to it. In Claude.ai, the scripts run in Claude's sandbox on the files you upload.
Which stacks does it support?
JavaScript and TypeScript in depth: Next.js, Vite, React, Node, Express and others, on Vercel, Netlify, Cloudflare, Lambda, Firebase, Amplify, Cloud Run, App Engine, Azure, Render, Railway, Fly, Heroku, DigitalOcean, Docker and Deno Deploy. Python web apps (Django, FastAPI, Flask) get basic checks, and the report never calls a Python project fully checked. Bun and Deno support is basic too. Go, Ruby, PHP, Java and other stacks are not analysed: the scan lists them and says it was incomplete.
How long does it take?
The scripts take under half a second on these examples (0.10 to 0.33 seconds per run, measured for both scan and diagnose). Claude's review adds time: the first pass is posted within about a minute, then the confirmed list follows after Claude reads the files.
How is this different from just asking my agent?
Your agent knows these mistakes in general. The skill gives it a fixed list of 41 causes, a script that checks every file for them in seconds, rules for which causes break on which host, and a list of safe look-alikes to rule out before calling something a bug. On the JavaScript and TypeScript blind test, plain Claude raised 11 false alarms; with the skill, none. On Python, Bun and Deno projects the advantage is smaller (see Results).
What does it cost?
It is free during launch. The normal price is $9.