Whitespace bugs are uniquely cruel. The code looks correct. The diff shows nothing suspicious. The error message points at the wrong line, or gives no line at all. And once you find the fix — deleting an invisible character or changing a line ending — the patch looks like an empty commit.
This post catalogues seven whitespace bugs that appear repeatedly in real deployments, explains exactly what triggers each one, and shows how to reproduce and fix it. All seven have caused actual production incidents. All seven are invisible to a basic code review.
The Seven Bugs
1. Windows CRLF Line Endings in a Bash Script
When a bash script is written or edited on Windows and then deployed to a Linux server, each
line may end with \r\n (CRLF) instead of the expected \n (LF). The \r character is not stripped by bash — it becomes part of the command, causing the
error:
/bin/bash^M: bad interpreter: No such file or directory Or silently corrupts string comparisons:
# On Windows-edited file:
NAME="deploy"$'\r'
if [ "$NAME" = "deploy" ]; then
echo "match" # never printed — $'\r' breaks the comparison
fi Detection: cat -A script.sh | head -5 — CRLF lines end with ^M$ instead of $.
Fix: sed -i 's/\r//' script.sh or configure git with git config core.autocrlf input to auto-convert on checkout.
2. Byte Order Mark (BOM) at the Start of a Shell Script
A UTF-8 BOM (U+FEFF, the three bytes 0xEF 0xBB 0xBF) at the very start of a
file causes bash to fail with a confusing error:
: No such file or directoryy.sh: line 1: The shebang #!/bin/bash becomes \xef\xbb\xbf#!/bin/bash, which bash cannot recognize. The BOM is added by some
Windows text editors (Notepad, older Excel exports, some IDE defaults) when saving UTF-8
files.
Detection:
hexdump -C script.sh | head -1
# With BOM: 00000000 ef bb bf 23 21 |...#!|
# Without BOM: 00000000 23 21 |#!| Fix: Remove BOM with sed -i '1s/^\xef\xbb\xbf//' script.sh. Prevention: configure editors to save
UTF-8 without BOM. A Smart Text Cleaner that strips invisible
Unicode characters including the BOM catches this before the file goes into source control.
3. Trailing Whitespace in a YAML Multi-line String
YAML block scalars (using | or >) strip trailing whitespace
from lines by default. But trailing whitespace after the block indicator itself — on the
same line as | — can cause parsers to reject the file or misinterpret the block style.
# This is valid YAML:
message: |
Hello world
# This may cause parser errors depending on implementation:
message: |
Hello world More subtly, trailing spaces inside a literal block string (after |) are
preserved verbatim. A config value that looks like "production" but is actually "production " (with trailing space) will fail string equality checks without any
error message — the service just behaves incorrectly.
4. Tabs vs Spaces in Python Source Files
Python 3 raises a TabError when a file mixes tab and space indentation within the
same indented block. Python 2 silently converted tabs to 8 spaces, which caused incorrect parsing
of visually correct-looking code. Python 3 makes the error explicit:
TabError: inconsistent use of tabs and spaces in indentation This most commonly appears when copying code from a source that uses tabs into a project that uses spaces, or when an editor with inconsistent settings saves part of a file with tabs and part with spaces.
Detection: python3 -tt script.py treats tabs mixed with spaces
as an error even in Python 2 compatibility mode. grep -Pn "\t" *.py finds all tab-indented lines.
5. Non-Breaking Space (U+00A0) in a Configuration Value
Non-breaking spaces look identical to regular spaces in most text editors and terminal output. They are introduced when copying text from web pages, word processors, or PDF exports. In a configuration file they are silent killers.
# In .env file — looks correct, but POSTGRES_HOST contains U+00A0
POSTGRES_HOST=db.internal The driver receives db.internal as the hostname. DNS resolution fails. The
error: "could not translate host name to address." The developer rereads the config file three
times and sees nothing wrong.
Detection in shell:
cat -v config.env | grep $'\xc2\xa0'
# U+00A0 encodes as 0xC2 0xA0 in UTF-8 Pasting config values through the text cleaner before committing them to source control replaces U+00A0 with a regular space automatically.
6. Trailing Whitespace in a .env File Value
Most .env parsers — including dotenv for Node.js, python-dotenv, and the
shell's export command — include trailing whitespace as part of the value. A database
URL written as:
DATABASE_URL=postgres://user:pass@localhost/db (with three trailing spaces) will cause a connection error. The logged URL looks correct because terminal output trims trailing whitespace in display. The actual string sent to the driver includes the spaces.
7. Mixed Indentation in a Makefile Recipe
Makefile recipes must be indented with exactly one hard tab character. Spaces look identical in most editors but cause Make to emit the infamous error:
Makefile:12: *** missing separator. Stop. This error is particularly confusing because "missing separator" refers to the tab character, not any visible separator. The line looks correctly indented.
# Correct (tab-indented recipe):
build:
go build ./...
# Incorrect (space-indented — breaks Make):
build:
go build ./... Detection: cat -A Makefile | grep "^ " — space-indented recipe lines will appear as go build instead of ^Igo build.
How to Detect These Issues Before They Deploy
The most reliable prevention layer is a pre-commit hook combined with editor configuration. Effective approaches:
Git attributes + editorconfig
A .editorconfig file at the repo root specifies indentation style, end-of-line
character, and trim_trailing_whitespace. Most editors respect it automatically.
# .editorconfig
root = true
[*]
end_of_line = lf
trim_trailing_whitespace = true
charset = utf-8
insert_final_newline = true
[Makefile]
indent_style = tab
[*.py]
indent_style = space
indent_size = 4 hexdump for BOM detection
# Check first bytes of any file
hexdump -C "$FILE" | head -1 | grep -q "ef bb bf" && echo "BOM detected" CI/CD whitespace gate
Add a step to your CI pipeline:
# Fail if any tracked file has CRLF line endings
git diff --check HEAD~1 HEAD Quick Fixes With a Text Cleaner
When you receive config values, environment variables, or scripts from external sources — emails, chat messages, web forms — the safest practice is to paste them through a tool that strips invisible characters before use. The Smart Text Cleaner handles BOM removal, NBSP-to-space conversion, and trailing whitespace stripping in one step. It also shows you exactly what was removed, so you can diagnose the source of the problem rather than just silently fixing it.
Invisible-character bugs are documented more thoroughly in hidden characters that break copy-paste — the overlap between "characters that break strings" and "characters that break deploys" is significant.
For one-off cleanup tasks, the quickest path is to paste the file through the text cleaner before committing it to source control. The tool highlights every removed character individually, which makes it easy to understand where the invisible content came from and whether the cleaned output is what you intended.