devAlice
← Multi-OS

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

  1. Put .gitattributes at the repo root with * text=auto eol=lf
  2. Turn off core.autocrlf.gitattributes overrides it anyway
  3. Force LF on Windows shell scripts (*.sh text eol=lf)
  4. Force CRLF on Windows batch files (*.bat text eol=crlf)
  5. 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

OSLine endingHistory
Unix · macOS · LinuxLF (\n, 0x0A)Unix 1970+
WindowsCRLF (\r\n, 0x0D 0x0A)DOS 1980+
Classic Mac OS 9CR (\r)1984–2001

1.2 git's Auto-conversion — core.autocrlf

ValueOn checkoutOn commit
true (Windows default)LF → CRLFCRLF → LF
input (Unix recommended)noneCRLF → LF
falsenonenone

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 pull once
  • Or git rebase origin/main on 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 false

Why input even on Windows: your output isn't influenced by OS defaults — always LF.

core.eol

git config --global core.eol lf

Default 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=unix

6. 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
    fi

7. 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.sh

Root 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.py

7.3 .gitattributes Not Actually Applying

  • .gitattributes not 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 = crlf

VS 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 utf8NoBOM

8. PR Checklist to Fix an Existing Repo

  • Add .gitattributes baseline (§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

References

Changelog

  • 2026-05-12 — Initial draft (devAlice M2 seed expansion)

Comments