Git Line Endings — End the CRLF Hell Between Mac/Linux and Windows
A single .gitattributes guarantees the same line endings everywhere. The core.autocrlf trap and the right way.
If the same repo is shared between Mac/Linux and Windows, you'll almost certainly hit it — CRLF vs LF. PR diffs explode into "1000 lines changed when nothing changed," shell scripts break with \r: command not found, Python throws SyntaxError: invalid character.
This guide is the right way to guarantee the same line endings on every OS via .gitattributes. Set it up once; never look at it again.
TL;DR
- Put
.gitattributesat the repo root with* text=auto eol=lf - Turn off
core.autocrlf—.gitattributesoverrides it anyway - Force LF on Windows shell scripts (
*.sh text eol=lf) - Force CRLF on Windows batch files (
*.bat text eol=crlf) - After unifying, run
git add --renormalize .to realign existing files
Prerequisites
- Git 2.10+ (supports
eol=in.gitattributes) - Repo pushed by both Mac/Win users simultaneously
1. Problem — Why CRLF Won't Go Away
1.1 Default Line Endings by OS
| OS | Line ending | History |
|---|---|---|
| Unix · macOS · Linux | LF (\n, 0x0A) | Unix 1970+ |
| Windows | CRLF (\r\n, 0x0D 0x0A) | DOS 1980+ |
| Classic Mac OS 9 | CR (\r) | 1984–2001 |
1.2 git's Auto-conversion — core.autocrlf
| Value | On checkout | On commit |
|---|---|---|
true (Windows default) | LF → CRLF | CRLF → LF |
input (Unix recommended) | none | CRLF → LF |
false | none | none |
Issue — each user has different settings, fresh machines vary, and silent conversions cause "I didn't change it but it changed" incidents.
1.3 Right Way Is .gitattributes
Place inside the repo to enforce the same rules on everyone. Overrides core.autocrlf.
2. .gitattributes Baseline — 5 min
.gitattributes at repo root:
# Default — store all text files as LF; auto-detect text-ness
* text=auto eol=lf
# Explicit text — force LF
*.c text eol=lf
*.cc text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.hpp text eol=lf
*.cs text eol=lf
*.go text eol=lf
*.java text eol=lf
*.kt text eol=lf
*.py text eol=lf
*.rb text eol=lf
*.rs text eol=lf
*.swift text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.js text eol=lf
*.jsx text eol=lf
*.mjs text eol=lf
*.cjs text eol=lf
*.json text eol=lf
*.jsonc text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.toml text eol=lf
*.md text eol=lf
*.mdx text eol=lf
*.txt text eol=lf
*.html text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.svg text eol=lf
*.xml text eol=lf
*.sql text eol=lf
# Shell scripts — LF (CRLF breaks them)
*.sh text eol=lf
*.bash text eol=lf
*.zsh text eol=lf
*.fish text eol=lf
Dockerfile* text eol=lf
Makefile* text eol=lf
# Windows-only — CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.psm1 text eol=crlf
# Binary — no conversion
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.avif binary
*.pdf binary
*.zip binary
*.gz binary
*.tar binary
*.7z binary
*.exe binary
*.dll binary
*.so binary
*.dylib binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.mp3 binary
*.mp4 binary
*.mov binary
# Quiet diffs — text but pointless to diff
*.lock text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
package-lock.json text eol=lf -diff
yarn.lock text eol=lf -diff
Cargo.lock text eol=lf -diff⚠️ PowerShell files (
*.ps1) want CRLF. Windows PowerShell occasionally rejects LF-only ps1.
3. Apply to Existing Repo — 5 min
Add .gitattributes and commit; then realign already-wrong line endings.
3.1 renormalize
# At repo root
git add .gitattributes
git commit -m "feat: add .gitattributes baseline"
# Realign all files according to .gitattributes
git add --renormalize .
git status # see what changed
git commit -m "chore: renormalize line endings"--renormalize keeps your working tree untouched; only the index is rewritten. Others auto-align on pull.
3.2 Big Repos / Active Collaboration
--renormalize makes a huge PR. Coordinate:
- Time window ("hold pushes after X")
- Merge the renormalize and have everyone
git pullonce - Or
git rebase origin/mainon feature branches to absorb it
3.3 Small Repos / Solo
Just merge as one PR.
4. User Global Setting — Tame core.autocrlf — 1 min
.gitattributes makes core.autocrlf moot in that repo. But for legacy / external repos without .gitattributes, it matters. Recommendation:
# macOS / Linux
git config --global core.autocrlf input
# No conversion on checkout; CRLF → LF on commit
# Windows
git config --global core.autocrlf input
# Even on Windows, prefer input — your files commit as LF. Others' CRLF still normalized.
# Or trust only .gitattributes
git config --global core.autocrlf falseWhy input even on Windows: your output isn't influenced by OS defaults — always LF.
core.eol
git config --global core.eol lfDefault line ending on checkout. eol= in .gitattributes wins.
5. Editor Settings — Align Your IDE
VS Code (.vscode/settings.json or user settings):
{
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}JetBrains: Settings → Editor → Code Style → Line separator → Unix and macOS (\n).
Vim:
" ~/.vimrc
set fileformats=unix,dos
set fileformat=unix6. Verify — Are They Really LF?
6.1 Single File
# macOS/Linux
file scripts/build.sh
# ASCII text ← OK
file scripts/build.sh | grep -q CRLF && echo has CRLF
# Or
od -c scripts/build.sh | head -3
# Lines end in \n only. If you see \r \n, CRLF is present.6.2 Whole Repo
# Find files with CRLF
git grep --cached -lI $'\r' -- ':!*.bat' ':!*.cmd' ':!*.ps1'-I excludes binaries. Exclude intentional CRLF files (ps1, bat).
6.3 Add a CI Gate
GitHub Actions:
- name: Check no CRLF in LF files
run: |
crlf=$(git grep --cached -lI $'\r' -- ':!*.bat' ':!*.cmd' ':!*.ps1' || true)
if [ -n "$crlf" ]; then
echo "CRLF found in LF-expected files:"
echo "$crlf"
exit 1
fi7. Common Pitfalls and Fixes
7.1 .sh Fails on Windows with ^M: command not found
A CRLF shell script committed in. Quick fix:
dos2unix scripts/build.sh
# Or
sed -i 's/\r$//' scripts/build.shRoot fix: *.sh text eol=lf in .gitattributes + git add --renormalize.
7.2 Python SyntaxError: invalid character
.py file with CRLF + UTF-8 BOM. Force LF in .gitattributes + remove BOM in your editor.
sed -i '1s/^\xEF\xBB\xBF//' file.py7.3 .gitattributes Not Actually Applying
.gitattributesnot at root but in a subdir — only applies in that subdir- Files weren't renormalized — existing endings remain (must run renormalize once)
7.4 IDE Resaves as CRLF
Add .editorconfig:
# .editorconfig (at repo root)
root = true
[*]
end_of_line = lf
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.{bat,cmd,ps1}]
end_of_line = crlfVS Code, JetBrains, Vim, Sublime, etc. read .editorconfig automatically.
7.5 Windows Editor Adds BOM Automatically
PowerShell Out-File defaults to UTF-16 BOM. Force UTF-8:
$content | Out-File "file.txt" -Encoding utf8 -NoNewline
# Or PowerShell 7+
$content | Out-File "file.txt" -Encoding utf8NoBOM8. PR Checklist to Fix an Existing Repo
- Add
.gitattributesbaseline (§2) - Add
.editorconfig(§7.4) -
git add --renormalize .+ commit - Add CI CRLF check (§6.3)
- Notify the team — "After pulling, confirm
git config --global core.autocrlf input" - One README line — "This repo unifies LF. See
.gitattributes."
Next Steps
- Multi-OS file sync — beyond Git too
- Mac initial setup — git global baseline
- Windows initial setup — WSL2 git setup
References
Changelog
- 2026-05-12 — Initial draft (devAlice M2 seed expansion)