Don't like ads? Go Ad-Free Today

CRLF vs LF The Line Ending Bug That Breaks CI

Published on

Your shell script works locally but fails in CI with a cryptic bad interpreter error. The cause is usually CRLF line endings. Learn what they are, why they break Linux pipelines, and how to fix the problem permanently with .gitattributes and editor settings.

CRLF vs LF: The Line Ending Bug That Breaks CI 1
ADVERTISEMENT · REMOVE?

Your shell script worked fine on your laptop. You push it to GitHub, CI picks it up, and the pipeline dies with a cryptic error — something like /bin/bash^M: bad interpreter or a job that exits 126 for no obvious reason. The script is syntactically correct. You have not changed a line. But the build is broken.

Nine times out of ten, the culprit is a line ending. Specifically, Windows-style CRLF line endings hiding inside a file that Linux expects to find only LF. This article explains what those two characters actually are, why the difference matters more than most developers realise, and how to stamp the problem out for good.

What CRLF and LF Actually Mean

Every text file needs a way to mark the end of a line. Two control characters are in use across different operating systems:

  • LF (Line Feed, \n, hex 0x0A) — the Unix and Linux standard. macOS has used it since OS X. One byte per line ending.
  • CRLF (Carriage Return + Line Feed, \r\n, hex 0x0D 0x0A) — the Windows standard, inherited from DOS, which inherited it from teletype machines. Two bytes per line ending.

The names come from the physical actions of an old typewriter: a carriage return moved the print head back to the start of the line; a line feed rolled the paper up one row. DOS encoded both actions literally. Unix picked the minimal approach and kept only the line feed.

Why CI Breaks and Your Laptop Does Not

If you write code on Windows and your editor saves files with CRLF, everything works locally — Windows tooling handles both formats transparently. But your CI pipeline almost certainly runs on Linux, and Linux shell interpreters treat \r as a printable character, not whitespace. When bash reads a script line ending in \r\n, it sees the command plus a trailing carriage return. The result looks like this:

/bin/bash^M: bad interpreter: No such file or directory

The ^M in that error is how terminals render \r. Bash is trying to execute a shebang line that ends with a carriage return, and of course no interpreter at that path exists.

The same problem appears in subtler ways:

  • Environment variable values with trailing \r characters silently breaking string comparisons.
  • Docker ENTRYPOINT scripts failing to start because the shebang is corrupted.
  • Python scripts reading config files and getting keys like "setting\r" instead of "setting".
  • Makefiles ignoring rules because the tab character is followed by \r.

How to Detect the Problem

Before fixing anything, confirm the file actually has CRLF endings. A few quick ways:

cat -A (Linux/macOS)

cat -A deploy.sh

Lines ending in CRLF will show ^M$ at the end. Lines ending in LF only show $.

file command

file deploy.sh
# deploy.sh: Bash script, ASCII text, with CRLF line terminators

xxd or hexdump

xxd deploy.sh | grep "0d 0a"

Any matches confirm CRLF. If you see only 0a at line endings, you are clean.

How to Fix It

dos2unix (the fastest fix)

Install once, run on any file:

# Install
sudo apt install dos2unix    # Debian/Ubuntu
brew install dos2unix        # macOS

# Convert a single file
dos2unix deploy.sh

# Convert all shell scripts in the repo
find . -name "*.sh" -exec dos2unix {} \;

sed (no extra tools needed)

sed -i "s/\r//" deploy.sh

tr (POSIX-safe)

tr -d "\r" < deploy.sh > deploy_fixed.sh && mv deploy_fixed.sh deploy.sh

How to Prevent It: Git Configuration

Fixing files after the fact is reactive. The durable fix is telling Git how to handle line endings so the problem never reaches your repo.

The .gitattributes approach (recommended)

A .gitattributes file committed to the repo enforces consistent line endings for everyone, regardless of their local Git settings. Add this to the root of your repo:

# Normalize all text files to LF in the repo
* text=auto eol=lf

# Explicitly enforce LF for files that must never have CRLF
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.json text eol=lf

# Binary files — do not touch
*.png binary
*.jpg binary
*.gif binary
*.zip binary
*.gz binary

The text=auto eol=lf line tells Git to detect text files automatically and store them with LF regardless of the developer’s OS. The explicit lines below it lock down files where CRLF would cause runtime failures.

After adding or changing .gitattributes, renormalize existing files in the repo:

git add --renormalize .
git commit -m "normalize line endings"

The core.autocrlf setting (local only)

Git has a global setting called core.autocrlf that controls line-ending conversion on checkout and commit. The recommended values depend on your OS:

# Windows — convert CRLF to LF on commit, LF to CRLF on checkout
git config --global core.autocrlf true

# Linux/macOS — convert CRLF to LF on commit, no conversion on checkout
git config --global core.autocrlf input

The problem with relying on core.autocrlf alone: it only applies to the machine where it is set. A contributor who has never configured it can still push CRLF files. A .gitattributes file is enforced by the repo itself, so it is strictly more reliable.

Editor Settings to Get Right

Your editor is the first line of defence. A few quick settings:

VS Code

The line ending for the current file is shown in the status bar (bottom right). Click it to switch between CRLF and LF. To set the default for new files, add this to your settings.json:

{
  "files.eol": "\n"
}

JetBrains IDEs (IntelliJ, WebStorm, PyCharm)

Go to Settings → Editor → Code Style and set Line separator to Unix and macOS (\n). You can also add an .editorconfig file at the repo root:

[*]
end_of_line = lf
insert_final_newline = true

Most modern editors respect .editorconfig natively or via a plugin. Committing it to the repo means contributors get consistent defaults without manual configuration.

The Complete Checklist

If you are standardising a repo that has had line-ending problems, work through this in order:

  1. Add a .gitattributes file with * text=auto eol=lf and explicit rules for shell scripts, Dockerfiles, and config files.
  2. Add an .editorconfig file with end_of_line = lf.
  3. Run git add --renormalize . && git commit -m "normalize line endings" to fix existing tracked files.
  4. Run dos2unix on any untracked scripts that will be executed on Linux.
  5. Set core.autocrlf input globally on Linux/macOS machines; true on Windows.
  6. Add a CI lint step — something like grep -rUl $'\r' *.sh — to catch any regressions before they reach a deployment.
Want To enjoy an ad-free experience? Go Ad-Free Today

Install Our Extensions

Add IO tools to your favorite browser for instant access and faster searching

Add to Chrome Extension Add to Edge Extension Add to Firefox Extension Add to Opera Extension

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!

ADVERTISEMENT · REMOVE?
ADVERTISEMENT · REMOVE?
ADVERTISEMENT · REMOVE?

News Corner w/ Tech Highlights

Get Involved

Help us continue providing valuable free tools

Buy me a coffee
ADVERTISEMENT · REMOVE?