Fixing Git Line-Ending Errors Across Windows and macOS
A Windows developer's container exits immediately with /usr/bin/env: 'bash\r': No such file or directory, a pull request from the same person shows every line of compose.yaml as changed although they edited one, and git status on a Mac reports warning: in the working copy of 'scripts/seed.sh', CRLF will be replaced by LF the next time Git touches it. Line endings are one of the most common first-week failures in mixed-OS teams, and they are entirely preventable with a policy committed to the repository. This page diagnoses the current state, sets the policy and repairs existing clones, as part of common local failure points.
The fix does not depend on anyone's personal Git settings, which is the point: policies in .gitattributes travel with the repository.
Diagnostic
Show the line endings Git has stored and what each working-tree file contains:
#!/usr/bin/env bash
set -euo pipefail
git config --show-origin --get core.autocrlf || echo "core.autocrlf unset"
cat .gitattributes 2>/dev/null || echo "no .gitattributes"
git ls-files --eol | awk '$1 != $2 || $2 ~ /crlf|mixed/' | head -10
file scripts/seed.sh
Expected bad output from a Windows clone:
file:C:/Program Files/Git/etc/gitconfig true
no .gitattributes
i/lf w/crlf attr/ scripts/seed.sh
i/crlf w/crlf attr/ compose.yaml
i/mixed w/crlf attr/ api/src/server.ts
scripts/seed.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators
i/ is the index, w/ the working tree. The script is LF in the repository but CRLF on disk because of core.autocrlf=true; compose.yaml was committed with CRLF; server.ts has mixed endings. With no .gitattributes, each developer's Git decides.
Root cause
Git can convert line endings on checkout and commit, and the default behaviour depends on each developer's configuration: Git for Windows installs with core.autocrlf=true (LF in the repository, CRLF on disk), macOS and Linux default to no conversion. Without a repository policy, files committed from Windows editors may carry CRLF into the repository, files checked out on Windows are converted to CRLF on disk, and tools inside Linux containers read those CRLF files through bind mounts. A shebang line ending in \r names an interpreter that does not exist, YAML and shell parsers choke on stray carriage returns, and every re-save flips a whole file's endings, producing enormous diffs. .gitattributes overrides personal settings per path, so one committed file sets a consistent policy for everyone.
The problem tends to surface during onboarding specifically because that is when a Windows developer first clones the repository with default settings. Existing team members on macOS and Linux never see it, the repository has "always worked", and the new hire's first experience is a container that will not start with an error message that mentions bash rather than line endings. Without someone who has seen it before, the usual response is to reinstall Docker, then WSL, then ask in a channel — a morning lost to a one-file fix. That is why this belongs in the environment baseline rather than in troubleshooting notes.
Mixed files, the third state in the diagnostic, deserve their own mention. They usually come from copy-pasting between editors with different settings, and they defeat both autocrlf and many linters because neither convention is consistently applied. Renormalisation fixes them in the same pass.
Resolution
- Commit a
.gitattributespolicy. LF for everything text, with explicit CRLF only where Windows tools require it:
* text=auto eol=lf
*.sh text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
*.sln text eol=crlf
*.png binary
*.jpg binary
*.zip binary
text=auto lets Git detect binary files; eol=lf makes the working tree LF on every platform, overriding core.autocrlf.
- Renormalise the repository once so stored files match the policy, in a dedicated commit:
#!/usr/bin/env bash
set -euo pipefail
git add --renormalize .
git status --short | wc -l | xargs echo "files renormalised:"
git commit -m "chore: normalise line endings with .gitattributes"
git rev-parse HEAD >> .git-blame-ignore-revs
git add .git-blame-ignore-revs && git commit -m "chore: ignore line-ending commit in blame"
- Refresh existing clones after pulling the change; files already on disk keep their old endings until Git rewrites them:
#!/usr/bin/env bash
set -euo pipefail
git stash push --include-untracked -m "before eol refresh" || true
git rm -r --cached --quiet .
git reset --hard
git stash pop || true
git ls-files --eol | awk '$2 == "w/crlf" && $3 !~ /eol=crlf/' | head -5
The last command should print nothing: no file outside the CRLF exceptions has CRLF on disk.
- Make editors agree with an
.editorconfig, so new files are created with the right endings:
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{bat,cmd,ps1}]
end_of_line = crlf
Expected output
$ git ls-files --eol | awk '{print $1, $2}' | sort | uniq -c
412 i/lf w/lf
3 i/crlf w/crlf
18 i/-text w/-text
$ file scripts/seed.sh
scripts/seed.sh: Bourne-Again shell script, ASCII text executable
$ docker compose up -d --wait seed-runner
✔ Container shop-seed-runner-1 Healthy
Text files are LF in the index and on disk, the three CRLF files are the Windows scripts that need it, binaries are untouched, and the shell script runs in the container.
Diffs return to normal immediately: a one-line edit on Windows now shows as one changed line in the pull request. That is often the most noticeable improvement for reviewers, who had learned to ignore whole-file diffs from certain colleagues and could no longer see what actually changed.
Prevention
- Check line endings in CI:
#!/usr/bin/env bash
set -euo pipefail
bad=$(git ls-files --eol | awk '($1 == "i/crlf" || $1 == "i/mixed") && $3 !~ /eol=crlf/ {print $4}')
[ -z "$bad" ] && echo "line endings ok" || { echo "CRLF or mixed endings committed:"; echo "$bad"; exit 1; }
Add the
mixed-line-endingpre-commit hook with--fix=lf, which is consistent with the policy once.gitattributesexists; see fixing pre-commit hooks that fail only on Windows.Recommend
core.autocrlf=inputorfalsefor Windows developers in onboarding docs; with.gitattributesin place it matters less, but it avoids surprises in repositories without a policy.
Platform caveats
Windows: after the refresh, editors that were set to CRLF may still create new files with CRLF until
.editorconfigis picked up; most editors support it natively or with an extension.
WSL2: clones inside WSL use Linux Git and are unaffected by the Windows
core.autocrlfdefault. Clones on the Windows side that are bind-mounted into containers are where CRLF causes trouble.
macOS: nothing changes for existing LF files; macOS users mainly see the one-off renormalisation diff.
Rollback
Revert the policy commits; files stay as they are until re-checked-out:
#!/usr/bin/env bash
set -euo pipefail
git revert --no-edit "$(git log -1 --format=%H -- .gitattributes)"
Frequently Asked Questions
Why does my shell script fail with bash\r: No such file or directory?
The script has CRLF line endings, so the shebang line ends with a carriage return and names an interpreter that does not exist. Add *.sh text eol=lf to .gitattributes, renormalise and refresh the clone.
Should Windows developers set core.autocrlf=true?
With a .gitattributes policy it no longer decides the outcome. Without one, true converts files to CRLF on disk, which breaks scripts used in Linux containers; input or false is safer for container-based work.
Will renormalising break history or blame?
It creates one commit that touches many files. Add its hash to .git-blame-ignore-revs and configure blame.ignoreRevsFile so blame skips it.
Which files should keep CRLF?
Files consumed by Windows-only tools that require it, such as .bat, .cmd, some .ps1 and Visual Studio .sln files. Everything else can be LF.