October 4, 2026 · Yunus Emre Vurgun
Why Does My YAML Config File Keep Breaking?
YAML config files usually break for one of five reasons: tabs instead of spaces, inconsistent indentation, unquoted strings that YAML interprets as booleans or numbers, multiline scalars that fold newlines away, and duplicate keys that silently overwrite each other. Every one of these is preventable with a small set of habits. This guide walks through each gotcha with broken and fixed examples.
Indentation: spaces only, and be consistent
YAML forbids tab characters for indentation — a single tab anywhere in the indent is a parse error in every compliant parser. Use spaces everywhere, and pick one width (two spaces is the common convention) and stick with it. Inconsistent width is legal YAML as long as nesting is unambiguous, but it is a bug magnet:
# Broken: the port line is ambiguous
server:
host: example.com
port: 8080
# Fixed: uniform two-space indent
server:
host: example.com
port: 8080
Most editors can be told to insert spaces when you press tab — turn that on for YAML, and add an .editorconfig with indent_style = space so every contributor gets it automatically. In CI, a one-line check with yamllint or python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" catches indentation errors before they reach production. If your YAML lives next to shell automation, the shell scripting guide shows patterns for validating files in deploy scripts.
Strings that are not strings
YAML tries to be helpful by typing unquoted scalars, and that helpfulness backfires constantly. The famous victim is the Norway problem: the country code NO parses as boolean false in YAML 1.1, so a list of country codes quietly gains a false. Version strings are the everyday version of the same trap:
| What you wrote | What YAML 1.1 sees | Fix |
|---|---|---|
country: NO | boolean false | country: "NO" |
enabled: yes | boolean true | enabled: "yes" or use true |
version: 3.10 | float 3.1 | version: "3.10" |
port: 08080 | parse error or octal surprise | port: 8080 |
ratio: 1:2 | sexagesimal number in YAML 1.1 | ratio: "1:2" |
password: hunter:2 | mapping error (colon in plain scalar) | quote it: "hunter:2" |
The safe rule is simple: quote any scalar that is not obviously a number or a real boolean. YAML 1.2 narrowed the boolean set to true and false only, which fixes NO and yes — but many tools still parse YAML 1.1 semantics by default, so do not count on it. When in doubt, "quotes" cost nothing and remove all ambiguity. Related parsing surprises with dates and text encodings are covered in units, dates, and encodings.
Multiline scalars: literal versus folded
The | (literal) and > (folded) block scalars look interchangeable until your newlines vanish. Literal preserves line breaks exactly, which is what you want for scripts, certificates, and SQL. Folded joins lines with spaces, which suits prose paragraphs:
script: |
#!/bin/sh
echo hello
echo world
description: >
This long sentence
becomes one line
with spaces.
The trailing-newline chomping indicators trip people up next: | keeps a single trailing newline, |- strips it, and |+ keeps all of them. For embedded scripts the difference matters — a missing trailing newline is harmless to most interpreters, but an extra blank line inside a heredoc can change behavior. When a multiline value misbehaves, print it with visible line endings before blaming the consumer. One more multiline pitfall: the block's indentation sets the baseline, so a stray extra space on one line becomes part of the value. This commonly breaks embedded Python or Makefiles, where leading whitespace is significant — if generated output looks shifted by a space or two, compare the block's indentation against its parent key before rewriting the template, and diff rendered versus expected output with whitespace visible to settle it in seconds.
Anchors, aliases, and merge keys
Anchors (&) and aliases (*) remove duplication by defining a block once and reusing it:
defaults: &defaults
timeout: 30
retries: 3
service_a:
<<: *defaults
host: a.example.com
service_b:
<<: *defaults
host: b.example.com
timeout: 60
Three cautions apply. First, the merge key << is a YAML 1.1 extension, not core YAML — most parsers support it, but strict YAML 1.2 parsers may not, so check yours before relying on it. Second, aliases are references, not copies: if your application mutates the parsed structure, every alias sees the change, which produces spooky action at a distance in long-lived processes. Third, overriding a merged key works only at the top level of the mapping — you cannot surgically override one nested field of a merged block, so deep overrides need restructuring rather than cleverness.
Why is my YAML config silently ignoring values?
The most-searched YAML complaint has a dull but important answer: duplicate keys. If a mapping defines the same key twice, most parsers silently keep the last value and discard the first, with no warning:
server:
host: old.example.com
port: 8080
host: new.example.com # wins silently; the first host is gone
This usually happens after a bad merge or a copy-paste, and it is invisible until the wrong value takes effect in production. Defend against it in layers: run a linter that flags duplicate keys, prefer many small config files over one giant one so merges stay clean, and log your effective configuration at startup so a quick glance reveals surprises. Finally, pin your parser version in CI so a YAML 1.1 to 1.2 upgrade never silently changes what your files mean. Teams that outgrow hand-edited YAML often move variable parts into the environment instead — the infrastructure-as-code tools roundup shows how modern tooling generates config rather than hand-maintaining it.