Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 069 / 100

Case sensitivity issues in Git: best practices explained

Renames that break builds on Linux.

Expona Admin6 min read

Introduction

You commit a file rename on your Mac, push to GitHub, and your Linux build pipeline suddenly breaks with "file not found" errors. This is one of the most common gotchas we encounter in teams working across different operating systems. Case sensitivity issues in Git: best practices explained covers why this happens, how to spot it early, and how to prevent it from derailing your team's workflow.

The core problem is simple: macOS and Windows treat filenames as case-insensitive, while Linux treats them as case-sensitive. When developers on different systems work with the same repository, Git's case handling becomes critical. We'll walk through the exact steps to fix it, catch it in CI, and design a naming convention that works everywhere.

macOS and Windows are case-insensitive by default

On macOS (using APFS or HFS+) and Windows (NTFS), the filesystem doesn't distinguish between UserService.ts and userservice.ts. Both refer to the same file. You can create a file called Config.js, then later reference it as config.js, and your IDE and shell won't complain—the file opens just fine.

Linux filesystems (ext4, XFS, btrfs) are strictly case-sensitive. UserService.ts and userservice.ts are two completely different files. If your code imports from ./userservice but the file is actually named UserService.ts, the import fails on Linux with "module not found"—even though it works on your Mac.

Here's a realistic scenario: a developer on macOS creates a new file and names it helpers/stringUtils.js. Somewhere in the codebase, another file imports it as import { trim } from './helpers/StringUtils.js' (wrong case). On macOS, this works fine. The developer commits and pushes. On the Linux CI server, the build fails immediately. The file exists on disk as stringUtils.js but the import is looking for StringUtils.js.

Git itself stores the exact filename as it exists on your filesystem when you first add the file. If you add stringUtils.js on macOS, Git records it as lowercase, even though your local filesystem is case-insensitive. The real problem emerges when someone later tries to change the case—or when case inconsistencies hide in import statements.

Use git mv to change case

When you need to rename a file to fix its case (for example, changing userservice.ts to UserService.ts), you must use git mv instead of your IDE's rename or your shell's mv command. Here's why: on a case-insensitive filesystem, a regular mv userservice.ts UserService.ts followed by git add might not register the change correctly. Git needs explicit instruction that the case has changed.

The correct approach:

bash
git mv userservice.ts UserService.ts
git commit -m "Rename: fix case of UserService.ts"

When you run this command on macOS or Windows, Git stages the rename and updates the index. When the commit is pushed to a Linux environment (or checked out there), the file exists with the correct case from the start.

If your IDE renames the file without using git mv, Git might see it as a delete + add, or it might miss the change entirely. Always double-check git status after a rename:

bash
git status

If you see the old filename in red (deleted) and the new one as untracked, you need to undo and use git mv instead. If there are uncommitted changes in the file, stash them first:

bash
git stash
git mv OldName.ts NewName.ts
git stash pop

Another scenario: you've already committed the file with the wrong case. You can still fix it, but you need to tell Git to ignore the case-insensitive filesystem. One approach is to use a two-step rename:

bash
git mv userservice.ts temp.ts
git mv temp.ts UserService.ts
git commit -m "Rename: fix case of UserService.ts"

This forces Git to register a real change rather than assuming case-insensitive equivalence.

Catch issues in Linux CI

The best safety net is a Linux-based CI pipeline that runs before code reaches production. If your team uses GitHub Actions, GitLab CI, or Jenkins, and you build and test on a Linux container, case sensitivity bugs surface immediately. A macOS developer commits stringUtils.js but imports it as StringUtils.js—the Linux CI build fails before the PR is merged.

Make sure your CI configuration includes a build step that would catch this:

bash
npm install
npm run build
npm run test

On a Linux runner, any import mismatch surfaces right away. We recommend running CI on Linux even if your team develops on macOS or Windows, because Linux is the most common server environment and the most strict about case.

If you're using TypeScript, enable strict module resolution in your tsconfig.json:

bash
"compilerOptions": {
  "forceConsistentCasingInFileNames": true,
  "strict": true
}

The forceConsistentCasingInFileNames option tells TypeScript to error if you import a module with a different case than the file's actual name, even on case-insensitive systems. This catches the problem during development before CI even runs.

For JavaScript projects using ESLint, add a rule to catch suspicious imports:

bash
npm install --save-dev eslint-plugin-import

Then in your ESLint config, enable the import/no-unresolved rule, which can be configured with case-sensitivity checks depending on your linter's capabilities.

Keep file names lowercase

The simplest and most effective long-term solution is a team convention: all filenames are lowercase with hyphens, not camelCase or PascalCase. This doesn't apply to class or function names inside files, only to the filenames themselves.

Good naming patterns:

  • user-service.ts (not UserService.ts or userservice.ts)
  • helpers/string-utils.js (not helpers/StringUtils.js)
  • models/order-item.ts (not models/OrderItem.ts)
  • config/api-endpoints.js (not config/ApiEndpoints.js)

Inside those files, you still use PascalCase for classes and camelCase for functions:

bash
// file: user-service.ts
export class UserService {
  public getUserById(id: string) {
    // ...
  }
}

Imports remain consistent and predictable:

bash
import { UserService } from './user-service';

This convention eliminates the entire category of case-related bugs. It also makes your codebase more predictable for new team members and easier to navigate because there's no guessing about capitalization.

If your project is already using inconsistent naming, you don't need to rename everything at once. Document the convention, apply it to new code, and gradually refactor legacy files. For each rename, use git mv and update all imports in the same commit.

Common mistakes

Mistake 1: Assuming the filesystem will handle it. A developer on Windows renames a file using Windows Explorer or VS Code's UI, and assumes Git will figure it out. But on a case-insensitive filesystem, Git may not see the case change. The commit pushes with the wrong filename, and Linux CI breaks. Always use git mv.

Mistake 2: Forgetting to update imports. When you rename a file from UserService.ts to user-service.ts, you update the filename correctly with git mv, but you forget to update the import statements. The local IDE might auto-correct the import on macOS, but Linux CI fails. Do a find-and-replace for all imports at the same time as the rename.

Mistake 3: Ignoring case-sensitivity in CI configuration. A team runs CI on a macOS runner instead of Linux, so case issues never surface until production. Ensure your primary CI pipeline uses a Linux environment. It's the most representative of your server infrastructure and the strictest about case.

Mistake 4: Using case-sensitive naming without team agreement. One developer uses UserService.ts, another uses userservice.ts, and a third uses user-service.ts. Over time, the codebase becomes inconsistent, and new developers are confused. Establish a naming convention at the start and document it in your contributing guide.

Mistake 5: Committing the wrong case initially, then fixing it later. If you've already committed userservice.ts` but the convention is `user-service.ts`, you might just commit an updated import statement instead of renaming the file. This leaves the repository with a file that doesn't match the convention, and future developers will be confused. Always fix the filename itself with `git mv`.

Key takeaways

  • macOS and Windows are case-insensitive by default; Linux is case-sensitive. This mismatch causes "file not found" errors in CI.
  • Always use git mv oldname.ts NewName.ts to rename files or change case. A regular rename may not register correctly on case-insensitive filesystems.
  • Run your CI pipeline on Linux to catch case sensitivity bugs before they reach production.
  • Adopt a team convention: all filenames lowercase with hyphens (e.g., user-service.ts). This eliminates the entire class of case-related bugs.
  • Enable forceConsistentCasingInFileNames in TypeScript projects to catch case mismatches during development.
  • When renaming a file, update all imports in the same commit to avoid broken references on Linux.
  • Document the naming convention in your contributing guide so new team members understand the rules from day one.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles