CRLF vs LF The Line Ending Bug That Breaks CI
Your shell script works locally but CI throws 'bad interpreter: /bin/bash^M'. This is the CRLF line ending bug. Learn what causes it, how to detect it, and how to permanently fix it with .gitattributes.
Your shell script runs perfectly on your laptop. You push it to GitHub. CI fails with a cryptic /bin/bash^M: bad interpreter error. You’ve just been bitten by the most invisible bug in software development: the wrong line ending.
This guide explains what CRLF and LF actually are, why mixing them silently corrupts scripts and configs, and the exact steps to prevent line ending bugs from ever reaching your pipeline again.
What Are CRLF and LF?
Every text file needs a way to mark the end of a line. Two conventions exist, inherited from the era of physical teletype machines:
- LF (Line Feed) — a single
\ncharacter (byte0x0A). Used by Linux, macOS, and every Unix-derived system. - CRLF (Carriage Return + Line Feed) — two characters,
\r\n(bytes0x0D 0x0A). Used by Windows and MS-DOS.
The names come from typewriter mechanics. A carriage return moved the print head back to the start of the line. A line feed advanced the paper one row. Windows kept both; Unix dropped the redundant carriage return.
Why CRLF Breaks CI Pipelines
On Linux (where virtually all CI runners execute), the \r is not whitespace — it’s a literal character. When a shell script is saved with CRLF endings, each line ends with \r before the newline. The kernel interprets the shebang line as #!/bin/bash\r and looks for a binary literally named bash\r. That binary does not exist.
The resulting error looks like this:
: bad interpreter: /bin/bash^M: No such file or directory
The ^M is how terminals display the carriage return character. It’s invisible in most text editors, which is what makes this bug so disorienting.
Other Places CRLF Silently Causes Damage
- Dockerfiles — A
RUNinstruction with CRLF will inject\rinto every command, breaking string comparisons and file paths. - Python scripts —
SyntaxError: unexpected character after line continuation characterwhen\is followed by\r\n. - .env files — Environment variable values pick up a trailing
\r, soAPP_ENV=production\rnever matches the expectedproduction. - CSV and data files — Parsers that read line by line may include
\rin the last field of every row. - SSH authorized_keys — A CRLF-encoded key file will be silently rejected by the SSH daemon.
- Git diffs — Every line appears changed, burying real changes in noise.
How to Detect Line Ending Problems
Most editors hide line endings by default. Here are reliable ways to check:
Using cat or hexdump
# Show ^M characters
cat -A yourfile.sh | head -5
# Hex dump to see 0x0d (CR) characters
hexdump -C yourfile.sh | head -10
Using file command
file yourfile.sh
# CRLF output: yourfile.sh: ASCII text, with CRLF line terminators
# LF output: yourfile.sh: ASCII text
Using grep
# Returns exit code 0 (found) if CRLF endings exist
grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml"
How to Fix CRLF Line Endings
Option 1: dos2unix (fastest one-off fix)
dos2unix strips carriage returns from files. It’s available on all major Linux distributions:
# Fix a single file
dos2unix yourscript.sh
# Fix all shell scripts recursively
find . -name "*.sh" -exec dos2unix {} \;
# Reverse: convert LF to CRLF (unix2dos)
unix2dos yourfile.sh
Option 2: sed (no extra tools needed)
# Remove carriage returns in-place
sed -i 's/\r//' yourscript.sh
# Or using tr
tr -d '\r' < input.sh > output.sh
Option 3: Fix in VS Code or JetBrains
In VS Code, the line ending mode is shown in the status bar (bottom right). Click it to switch between CRLF and LF for the current file. To change the default for new files, set "files.eol": "\n" in your settings.json.
In JetBrains IDEs, go to File → Line Separators to change the current file, or set the default in Editor → Code Style → Line separator.
The Right Fix: .gitattributes
One-off fixes don’t scale. The correct solution is a .gitattributes file that tells Git exactly which line endings to enforce, regardless of what editor or OS contributors use.
# .gitattributes — commit this to the root of your repository
# Default: normalize all text files to LF in the repo
* text=auto eol=lf
# Explicitly enforce LF for scripts and configs
*.sh text eol=lf
*.bash text eol=lf
*.py text eol=lf
*.rb text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.env text eol=lf
Dockerfile text eol=lf
Makefile text eol=lf
# Windows-only files can keep CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Binary files — never touch line endings
*.png binary
*.jpg binary
*.gif binary
*.zip binary
*.pdf binary
After adding this file, run the following to re-normalize your entire repository in one shot:
git add --renormalize .
git commit -m "chore: normalize line endings via .gitattributes"
Git’s autocrlf Setting — and Why It Often Makes Things Worse
Git has a core.autocrlf setting that attempts to convert line endings automatically:
core.autocrlf=true— converts LF to CRLF on checkout (Windows), CRLF to LF on commit. Intended for Windows users.core.autocrlf=input— converts CRLF to LF on commit, does nothing on checkout. Safer for Mac/Linux.core.autocrlf=false— Git does nothing. Whatever the editor saves is what gets committed.
The problem: core.autocrlf is a local setting stored in ~/.gitconfig. Every developer on your team has a different value, so commits from different machines produce different line endings. This creates constant noise in diffs and intermittent CI failures depending on who last touched a file.
The rule of thumb: use .gitattributes to set line ending policy in the repository. Let core.autocrlf be whatever each developer has — .gitattributes overrides it.
Adding a CI Guard
Even with .gitattributes in place, it’s worth adding an explicit check in CI to catch any files that slip through. A two-line step covers most cases:
# In your CI workflow (GitHub Actions example)
- name: Check for CRLF line endings
run: |
if grep -rlP "\r" . --include="*.sh" --include="*.py" --include="*.yml" --include="Dockerfile"; then
echo "ERROR: CRLF line endings found. Run dos2unix on the above files."
exit 1
fi
This step fails loudly at the source — the PR — rather than silently at deployment.
Quick Reference: CRLF vs LF
LF (\n) | CRLF (\r\n) | |
|---|---|---|
| Bytes | 0x0A | 0x0D 0x0A |
| Used by | Linux, macOS, Unix | Windows, MS-DOS |
| Safe for shell scripts | Yes | No — breaks shebang |
| Safe for Dockerfile | Yes | No |
| Safe for .env files | Yes | No — adds trailing \r to values |
| Git recommendation | Normalize to LF in repo | Only for .bat/.cmd/.ps1 |
Install Our Extensions
Add IO tools to your favorite browser for instant access and faster searching
恵 Scoreboard Has Arrived!
Scoreboard is a fun way to keep track of your games, all data is stored in your browser. More features are coming soon!
Must-Try Tools
View All New Arrivals
View AllUpdate: Our latest tool was added on Apr 28, 2026
