Back to blog
Field Notes2026-07-065 min

Works Locally, 404s in Production, Zero Console Errors

A web page that worked on every developer machine, deployed cleanly, returned 200, rendered fine — and did nothing. Interactive features dead. Console: clean. No errors, no warnings, nothing.

The debugging tour visited everything you'd expect: the application code, the build pipeline, the CDN, the cache config. The actual bug was one line in .gitignore.

The one line

The line was dist/ — the single most common entry in any .gitignore on the internet. Unanchored, that pattern doesn't mean "the build output directory at the root of my repo." It means every directory named dist, at any depth, anywhere in the tree.

This project vendored third-party platform JavaScript — committed on purpose, meant to deploy with the site. That vendored code shipped its files inside its own dist/ directory. Git silently refused to track any of it, from the very first commit.

On the machine where the code was written, everything worked: the files existed on disk, and gitignore doesn't delete anything. In CI, the checkout simply had holes where those files should have been. The deployed page dutifully requested scripts that had never left the original laptop, and got 404s.

Why the console was clean

JavaScript that never loads never throws. A 404'd script tag isn't an exception — the code it would have contained simply doesn't exist in the page, so its features are silently absent rather than visibly broken. Unless you open the network tab and look at each request, the browser presents a perfectly healthy-looking page that happens to be a shell.

Git's behavior compounds it. Ignoring is silent, and it's one-sided: the machine where the files exist keeps working forever, while every fresh clone is quietly broken. The person who wrote the ignore line is structurally the last person who can observe the failure. Local worked, production 404'd, and both were telling the truth.

Why AI assistants produce this

Boilerplate .gitignore blocks — dist/, build/, node_modules/, coverage/ — are among the most-reproduced snippets in AI training data, and they are unanchored by convention, because that's how every template writes them. Ask an assistant to "add a standard .gitignore" and this is what you get.

And the convention is right, for roughly 95% of repos. It becomes wrong the day you vendor a dependency that ships its own dist directory — a change the assistant that wrote the ignore line will never see, made months later by someone (or something) with no reason to re-read .gitignore. The two edits are individually correct and jointly broken. No review of either diff would catch it.

The fix, and the rule we're building

# matches every dist directory in the tree,
# including inside vendored code
dist/

# matches only the build output at the repo root
/dist/

Anchor your build-output ignores to the paths you actually mean:

# every file present on disk that git is ignoring
git ls-files --others --ignored --exclude-standard

# which rule claims a specific path, and where it lives
git check-ignore -v vendor/platform/dist/loader.js

When in doubt, git check-ignore -v <path> tells you exactly which line of which ignore file is claiming a path — the command that ends this class of debugging session in one minute.

And if you want to audit an existing repo right now, ask git to show you everything it's silently ignoring:

Scan that first list for anything under a vendored or committed-on-purpose directory. If you find one, you have this bug today — you just haven't deployed from a fresh clone yet.

We're building **PROJ009 (unanchored-build-dir-gitignore)** for this: it flags unanchored build-directory ignore patterns, but only when a file the pattern ignores is actually referenced by tracked source. That condition is what separates "your build output, correctly ignored" from "a hole in your repo that production is about to fall into" — and it's what keeps the rule from crying wolf on every repo with a normal .gitignore.

Works-locally-broken-in-prod bugs are the signature of AI-assisted development, because every artifact involved looked correct on its own. Catching them is the whole product: pip install stablestack

Free with every install. No license key required.

pip install stablestack