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.

Reading git ls-files --eol Table explaining index and working-tree line-ending states and what each implies. Reading git ls-files --eol Index Working tree Meaning i/lf w/lf correct everywhere i/lf w/crlf autocrlf converting i/crlf w/crlf committed with CRLF i/mixed any file is inconsistent
The index column is what the repository stores; the working-tree column is what tools see.

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

  1. Commit a .gitattributes policy. 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.

  1. 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"
  1. 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.

  1. 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
Rolling Out a Line-Ending Policy Ordered steps from committing .gitattributes to every clone matching the policy. Rolling Out a Line-Ending Policy 1 — commit .gitattributes 2 — renormalise in its own commit 3 — add commit to blame ignore list 4 — each developer refreshes clone 5 — editorconfig keeps new files right
Renormalise once centrally; every developer refreshes their clone once after pulling.

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

  1. 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; }
  1. Add the mixed-line-ending pre-commit hook with --fix=lf, which is consistent with the policy once .gitattributes exists; see fixing pre-commit hooks that fail only on Windows.

  2. Recommend core.autocrlf=input or false for Windows developers in onboarding docs; with .gitattributes in place it matters less, but it avoids surprises in repositories without a policy.

Personal autocrlf vs Repository Policy Comparison of relying on each developer's core.autocrlf against committing .gitattributes. Personal autocrlf vs Repository Policy core.autocrlf per person .gitattributes in repo differs by OS and install same for every clone CRLF reaches containers LF on disk everywhere whole-file diffs only real changes fixed person by person fixed once in git
The repository policy wins over personal settings, so everyone gets the same result.

Platform caveats

Windows: after the refresh, editors that were set to CRLF may still create new files with CRLF until .editorconfig is 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.autocrlf default. 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.