Your app works perfectly on your machine. You push it, the build runs on Vercel, Netlify, or Replit’s deploy step, and it fails with Module not found: Can't resolve './Header' for a file that is sitting right there in your repo. Nothing in the code changed between “works” and “fails.” The only thing that changed is the filesystem underneath it.
Why this happens
Your coding agent, whether that’s Cursor, Claude Code, Lovable, Bolt, or Replit’s Agent, doesn’t always write an import that matches a filename’s casing exactly. It might generate import Header from './header' when the file it created is actually Header.tsx. On your laptop this is invisible: macOS and Windows both ship with case-insensitive filesystems by default, so header.tsx and Header.tsx resolve to the same file. The dev server, the build, the preview all just work.
Production build servers run on Linux, and Linux’s filesystem is case-sensitive. header.tsx and Header.tsx are two different paths there, and only one of them exists. The bundler cannot resolve the import, and the build fails, not because your code is wrong in any way you’d catch by reading it, but because two strings that mean the same thing to your OS mean different things to the one actually running the deploy.
It gets worse when a file gets renamed for casing only, say header.tsx to Header.tsx. On a case-insensitive filesystem, git and your editor often don’t register that as a real change, because as far as the OS is concerned the path didn’t change. The old casing can stay baked into your commit history while your editor shows you the new one, so the drift between what you see locally and what’s actually committed keeps growing.
How to tell if this is your problem
- Read the exact error path. The bundler tells you which import it couldn’t resolve. Compare that string, character by character, against the actual filename in your repo (not what your editor’s file tree shows you, since some editors normalize casing in the sidebar).
- Check it on GitHub, not locally. Open the file in your repo on GitHub.com and read the casing directly from the URL. That’s the canonical casing the Linux build server sees, independent of what macOS or Windows is showing you.
- Grep for the import across your codebase. If one import uses
./Headerand another uses./headerfor the same file, you have the mismatch, and it’s a matter of time before the second one hits a case-sensitive build too.
The fix
Make every import match the real filename exactly, then make sure git actually records any casing fix as a rename. A same-casing git operation on a case-insensitive filesystem can silently no-op, so rename through a throwaway intermediate name:
git mv components/header.tsx components/header-tmp.tsx
git mv components/header-tmp.tsx components/Header.tsx
git commit -m "Fix Header casing"
That forces git to log two distinct path changes, which a Linux checkout will replay correctly instead of leaving the old casing behind.
Catch it before you ever push. If you’re on webpack (which Next.js uses by default), add case-sensitive-paths-webpack-plugin so a casing mismatch throws locally instead of only on the production build:
npm install --save-dev case-sensitive-paths-webpack-plugin
Wire it into your webpack config’s plugins array and a mismatched import becomes a build error on your own machine, the same day the agent wrote it, instead of a broken deploy days later.
If you want your local machine to behave like the production server, you can create a case-sensitive disk image on macOS and do your work from there, or simply run your build inside a Linux Docker container before you push. Either one turns “works locally, breaks in prod” into “breaks locally too,” which is the outcome you actually want.
How to avoid this next time
Case sensitivity isn’t something any of these tools check for you by default, and it’s not something you’ll spot by reading a diff, since the code reads correctly either way. Add the webpack plugin (or your bundler’s equivalent) once, early in a project, and every AI-agent-authored import that drifts on casing gets caught in seconds instead of showing up as a failed deploy you have to debug from a build log.
Comments
Sign in to join the conversation.
No comments yet — be the first.