DEV Community

hao li
hao li

Posted on Originally published at github.com

Git Can Track Two Files Your Mac Cannot Check Out

Git can track two files your Mac cannot check out.

Here's a real one. Hermes Agent issue #99821: the repo tracks contributors/emails/agent@Agents-Mac-mini.local alongside a twin file whose name differs only by capitalization. On Linux both files live happily side by side. On a Mac, every clone or pull ends up with a permanently dirty working tree — and hermes update on macOS flat-out refuses to update past it. The issue even ships the one-liner that finds it:

git ls-tree -r --name-only <sha> | sort -f | uniq -di
# -> contributors/emails/agent@Agents-Mac-mini.local
Enter fullscreen mode Exit fullscreen mode

Found, fittingly, by an update-preflight check running on a Mac.

Why does this happen? Git's index is case-sensitive: it will faithfully track README.md and readme.md as two different files, because on the Linux machine where they were committed, they are two different files. macOS and Windows disagree — their filesystems fold case, so the second file silently overwrites the first, or the checkout fails outright. Nobody did anything wrong. The two platforms just have different answers to "are these the same file?"

There's a sneakier variant: Unicode normalization. NFC café (é as one codepoint, U+00E9) and NFD café (e plus a combining acute, U+0301) render identically in your terminal but are different byte sequences. macOS compares names without regard to normalization, so those two "different" files are one file on a Mac. Good luck spotting that in code review.

So I built casecrash: a tiny zero-dependency Python CLI that automates that one-liner for both collision classes — casefolding and Unicode normalization — and reports exactly which pairs break, and on which platforms.

pip install casecrash
cd your-repo
casecrash
Enter fullscreen mode Exit fullscreen mode

Example output from a repo with both problems:

casecrash: found 2 filename collision(s) in /tmp/demorepo (5 files scanned):

[normalization] collide after Unicode normalization (macOS):
  'cafe\u0301.md'  [NFD]
  'caf\u00e9.md'  [NFC]
  -> breaks on: macOS

[case] collide on case-insensitive filesystems (macOS, Windows):
  'README.md'
  'readme.md'
  -> breaks on: macOS, Windows

Fix: rename one of each colliding pair before a macOS/Windows user clones this repo.  'git mv' the odd one out and commit.
Enter fullscreen mode Exit fullscreen mode

The [NFC] / [NFD] labels and \u0301 escapes matter — without them you'd never see the difference between the two cafés.

It's CI-friendly by design: exit 1 when collisions are found, 0 when clean, 2 on errors. One step in a workflow and no Mac user ever gets surprised again:

- run: pip install casecrash
- run: casecrash
Enter fullscreen mode Exit fullscreen mode

There's --format json if you want to script on it. It reads git ls-files directly from the index, so a Linux CI runner catches collisions that would only ever bite macOS users. Submodules are skipped (and counted), and undecodable filenames are handled as raw bytes so they can never crash the tool.

The fix for a collision is boring on purpose: git mv one of them and commit. The value is knowing before your Mac-owning teammate finds out for you.

The same filesystem disagreement has had sharper teeth. In March 2021, CVE-2021-21300 was patched: a crafted repository combining symlinks with clean/smudge filters like Git LFS could get a just-checked-out script executed during clone — but only on case-insensitive filesystems. Different exploit, same root cause: two platforms disagreeing about whether two paths are the same. That vulnerability is fixed. The disagreement is forever.

Install: pip install casecrash — Python 3.9+, zero dependencies, MIT. Source: github.com/hahahahahahahahah6/casecrash

Top comments (0)