Documents

YAML

Diagnose and fix YAML parse errors, coercion traps, and tool-specific quirks across parsers and platforms.

What it does

Writes, debugs, and validates YAML so a file parses identically in every parser and tool. Names which of five mechanisms is in play (scalar resolution, indentation, block-scalar folding, anchor expansion, tag construction), then quotes, indents, or pins so behavior stops depending on the parser. Covers coercion traps (booleans, octal, sexagesimal, dates, version floats), multiline rules for scripts and PEM keys, anchors and merge keys, duplicate-key linting, round-trip editing that preserves comments, safe loading with bounds, and dialect quirks for Kubernetes, Helm, GitHub Actions, Ansible, Compose, CloudFormation, OpenAPI, and Home Assistant. Pairs yamllint, schema validation, yq, and sem…

When to use it

  • A boolean, octal, or sexagesimal silently swallowed the wrong value
  • Anchors or merge keys disappeared after a YAML→JSON→YAML round trip
  • Same file loads in Python but fails in Go
  • Block scalar mangled a PEM key or shell script

The skill document

Data. At the start of every session, read ~/Clawic/data/yaml/config.yaml (what the user declared) and ~/Clawic/data/yaml/memory.md (what you observed, plus its ## Boxes index and ## Due table). Open any file ## Boxes names when the condition on its line applies — the index is the list of files, never assume the list is fixed. Every path it names is inside ~/Clawic/data/; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in configPaths — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. If none of it exists, work from defaults and say nothing about it.

Write before the session ends whenever something durable came out: which parser and spec version a project actually uses; a YAML file that matters and what reads it; a coercion or indentation trap that bit and the fix; the observed house style of an existing repo; a schema, lint config, or file layout that finally validated; a cadence the user agreed to. memory-template.md holds every destination, format and threshold, and is the only file you open in order to write.

Entities that belong to other skills stay in their shared box. A device configured through YAML (Home Assistant, netplan, cloud-init, ESPHome) goes to ~/Clawic/data/devices/devices.md, not here. A config decision that belongs to tracked work goes to ~/Clawic/data/projects/.md, and this box keeps only its name as a pointer. Protocol for both is in memory-template.md: read before adding, update your own row in place, never a second row for the same entity.

No credential is ever written anywhere under ~/Clawic/data/ — not in the files named here, not in a file you create, not in YAML the user pastes in to be saved. Strip the value, store the pointer: env:DB_PASSWORD, keychain:prod-tls, 1password:Work/Cluster/kubeconfig, sops:secrets/prod.enc.yaml, file:~/.kube/config. A pasted PEM key inside a | block is the single most common way a secret lands in a memory file — it goes in as ``, never as text. If data sits at an old location (~/yaml/ or ~/clawic/yaml/), move it to ~/Clawic/data/yaml/, and say in one line that you moved it and from where.

YAML never fails loudly at the moment you make the mistake: it produces a valid document with the wrong types, and the error surfaces three layers away as cannot unmarshal number into string. So the job is always the same — name which of five things the file is doing (a scalar being resolved, indentation defining scope, a block scalar folding, an anchor being expanded, or a tag being constructed), then quote, indent, or pin so the answer stops depending on the parser. Work from defaults immediately: never open with questions about their linter, their spec version, or how proactive to be. Precedence for any value: config.yaml~/Clawic/profile.yaml → the Configuration table default.

When To Use

  • Writing, reviewing, or fixing any YAML file: config, manifest, workflow, playbook, compose file, schema
  • A parse error, or worse, no error and the wrong value: booleans from no/on, floats from version numbers, octal from file modes, sexagesimal from HH:MM, dates from ISO strings
  • Multiline content in YAML: scripts, SQL, certificates, embedded JSON, prose — and the chomping/indentation rules that decide what survives
  • Deduplicating with anchors, aliases and merge keys, and finding out which of your tools actually keeps them
  • Loading, generating, patching or diffing YAML from code or from a CLI without destroying comments, key order, or quote style
  • Locking a file down: safe loaders against untrusted input, schema validation in CI, secrets kept out of the file
  • Not for JSON payload design (json), TOML (toml), XML (xml), Kubernetes runtime debugging (k8s), or Ansible playbook semantics (ansible) — this covers the YAML layer underneath all of them

Quick Reference

SituationPlayDepth
no, on, yes became a booleanSpec version decides: 1.1 resolves them, 1.2 does not — quote the literal (Rule 1)types.md
0644 became 420, or 010 became 8YAML 1.1 reads leading-zero integers as octal; quote every file mode and idtypes.md
22:22 became 1342Sexagesimal base-60 in 1.1 — quote every port pair, duration, and time (Rule 2)types.md
Version string 1.10 collapsed to 1.1Resolved as a float; version numbers and semvers are always quoted stringstypes.md
cannot unmarshal number into Go struct field … of type stringThe value was resolved as int; the consumer wanted a string. Quote it, do not change the schematypes.md
A colon, #, @, *, or leading - inside a value breaks the linePlain scalars cannot contain : or #; indicator characters cannot start onestrings.md
found character '\t' that cannot start any tokenA literal tab in the indentation — tabs are never valid indentationerrors.md
Script, PEM key, or JSON blob loses its newlines> folds, | preserves; chomping indicator decides the trailing newlines (Rule 5)multiline.md
Block scalar's first line is indented and everything shiftsNeeds an explicit indentation indicator (|2)multiline.md
<<: merge key ignored, or an alias came back expanded1.1 extension, and anchors die on any YAML→JSON→YAML round trip (Rule 8)anchors.md
Two keys the same, wrong one wonSpec says last wins; strict parsers error. Never rely on either — lint for it (Rule 7)errors.md
Loads in Python, fails in Go, or vice versaSpec-version and resolver matrix, per libraryparsers.md
Dumped file lost every comment, or reordered every keyThe loader threw them away — round-trip loaders are a different API (Rule 9)editing.md
Need to read, patch, merge, or diff YAML from a scriptyq flavor, in-place edits, semantic diff instead of textualediting.md
Untrusted YAML, or a file from a third partySafe loader only; alias-expansion bomb is not covered by "safe" (Rule 6)security.md
Secrets inside a manifest, base64 mistaken for encryptionsops / sealed-secrets / external refs; base64 is encodingsecurity.md
Want the editor and CI to catch this class of bug for good# yaml-language-server: $schema=, yamllint rules, schema gate in CIschemas.md
Kubernetes manifest, Helm chart, or kustomize overlayMulti-doc streams, string-typed fields, templating that is not YAML until renderedkubernetes.md
GitHub Actions, GitLab CI, CircleCI, Azure Pipelineson: truthiness, ${{ }} quoting, extends vs anchors across includespipelines.md
Ansible, Compose, CloudFormation, OpenAPI, Home Assistant, cloud-initPer-tool tags, templating layers, and the quoting each one demandsdialects.md
Designing a new config file or an env-overlay layoutMerge semantics, null vs absent, list strategy, what belongs outside the fileconfig-design.md
Anything else YAMLRound-trip it: load and re-dump with the target's own parser and diff. The diff shows what the parser really read

Coverage map: types.md resolution and coercion · strings.md quoting and escapes · multiline.md block scalars · anchors.md anchors, aliases, merge keys · errors.md parse-error signatures · parsers.md library and spec matrix · editing.md programmatic read/write/diff · schemas.md validation and linting · security.md untrusted input and secrets · kubernetes.md manifests, Helm, kustomize · pipelines.md CI workflow files · dialects.md tool-specific YAML · config-design.md designing the file itself.

Core Rules

  1. Quote any scalar whose type must not depend on the parser. Quote the token if it matches: y|n|yes|no|on|off|true|false in any case, null/~/empty, a leading zero (0644, 007), digits containing :, anything shaped like a number that is really an identity (version, phone, zip, SHA prefix, account id), or an ISO date. Test: would this value be wrong if it came back as a bool, a float, or a date object? If yes, quote it. Cost of over-quoting is a diff; cost of under-quoting is a bug three layers away.
  2. Colons in unquoted digits are base 60. YAML 1.1 resolvers (PyYAML, Ruby Psych, go-yaml v2, SnakeYAML 1.x) read a:b as a×60+b and a:b:c as a×3600+b×60+c — so 22:22 is 1342 and 1:30:00 is 5400. YAML 1.2 dropped sexagesimals and leaves them as strings. Any port mapping, duration, or clock time goes in quotes: "22:22".
  3. Indentation is spaces, and the width is a decision. Tabs are never legal as indentation in any version. Use indent_width (default 2) everywhere in a file; a nested block that changes width parses, then confuses every reader and every patch tool. Block sequences may sit at their key's indentation or two spaces in — both are valid, sequence_indent picks one, and mixing the two inside one file is what makes yq rewrites produce a 400-line diff.
  4. A key is only a key when the colon is followed by a space or a line end. key:value is the six-character scalar key:value; key: value is a mapping. Inside flow style {a:1} is one scalar too — write {a: 1}. Conversely, a plain scalar containing : splits the line, which is why msg: Note: this breaks needs quotes.
  5. Pick the block scalar by whether newlines are data. | keeps every newline (scripts, PEM, embedded YAML/JSON, logs); > folds single newlines into spaces and is only correct for prose. Chomping suffix decides the end: default (clip) keeps exactly one trailing newline, - strips all, + keeps every trailing blank line. A PEM key needs | and its final newline: | alone, not |-, or the consumer rejects the key.
  6. Never load untrusted YAML with a full-tag loader, and know that "safe" is not "bounded". yaml.safe_load, js-yaml v4 load, SnakeYAML ≥2.0 default, Psych 4 YAML.load — all block object construction. None of them bound alias expansion: a 200-byte file with nested anchors still expands to gigabytes (the billion-laughs shape behind CVE-2019-11253). Add a size cap and a depth/alias cap yourself (security.md).
  7. Duplicate keys are a lint failure, never a merge strategy. The spec says the last one wins; PyYAML silently obeys, go-yaml v3 and js-yaml raise. A file that relies on either behavior breaks the day the toolchain changes. Enable key-duplicates in yamllint and treat a hit as an error.
  8. An anchor survives only a true round-trip. Aliases resolve at parse time, so anything that goes YAML → object → JSON → YAML (kubectl, most API servers, yq -o=json, every code generator) emits the expanded copy and the DRY-ness is gone from that point on. The << merge key is a YAML 1.1 extension absent from the 1.2 core schema — supported by PyYAML, ruamel and go-yaml, not guaranteed elsewhere. Anchors are also file-scoped: they never cross an include.
  9. Loading and re-dumping is lossy unless you chose a round-trip API. A normal load returns plain data: comments gone, key order re-sorted (PyYAML dump sorts alphabetically by default), quote style normalized, anchors expanded, long lines wrapped at the emitter's default width (~80 columns in PyYAML and go-yaml). Editing a human's file means ruamel.yaml round-trip mode, yq in place, or a text patch — never load-then-dump (editing.md).
  10. Validate the file with the consumer's own parser before delivering it. Being valid YAML is necessary and not sufficient: the tool's schema is the real contract. schema_gate (default true) means every generated file is checked — yamllint -s, then the tool's own validator (kubeconform, helm template, actionlint, ansible-lint, cfn-lint, docker compose config) — before it is presented as done.

Resolution Table

What an unquoted scalar becomes. The 1.1 column is PyYAML, Ruby Psych, SnakeYAML 1.x and go-yaml v2; the 1.2 column is js-yaml v4, SnakeYAML Engine, and the core schema (parsers.md has the per-library detail).

WrittenYAML 1.1YAML 1.2 coreWrite instead
no, off, n (n in some resolvers only)falsestring"no"
yes, on, ytruestring"yes"
True, TRUE, truetruetrueleave, or quote if it is text
0644420 (octal)644 (decimal)"0644"
0o644string420pick per target
22:221342string"22:22"
1.0float 1.0; go-yaml and JS emitters re-emit it as 1, PyYAML/Psych keep 1.0same"1.0"
1.2.3string (two dots)stringfine unquoted, quote for consistency
2026-07-26date objectstring"2026-07-26" unless a date is wanted
~, null, Null, emptynullnull"~", "null", or ""
.inf, -.Inf, .nanfloat infinity / NaNsame".inf"
e5, 12e3string / string (1.1 floats need a dot)string / 12000quote identifiers that look numeric
NO, SE, BS (country codes)false, string, stringstringsalways quote country codes
key: (nothing after)nullnull"" for empty string, {} for empty map

Error Signatures

The message names the layer: a scanner error is about characters, a parser error is about structure, a constructor error is about types, and a schema error means the YAML was fine.

MessageReal causeFirst move
found character '\t' that cannot start any tokenLiteral tab in indentationShow whitespace in the editor; convert tabs to spaces (Rule 3)
mapping values are not allowed in this contextA second : on a line that was already a scalar, or a plain value containing : Quote the value (Rule 4)
did not find expected key / expected Indentation of one line does not match its siblings, usually a list item one space offDiff the leading spaces of the block; the first divergent line is the one
could not find expected ':'A flow collection was opened ({ or [) and never closed, often a stray {{ }} templateQuote the templated value (dialects.md)
found undefined alias 'x'Alias used before its anchor, or across filesAnchors are file-scoped and forward references are illegal (Rule 8)
duplicated mapping keyTwo identical keys in one mappingRule 7 — never resolve by deleting whichever one the tool complained about; check which is live
could not determine a constructor for the tag '!Ref'Tool-specific local tag hitting a generic parserUse the tool's parser or register the tag (dialects.md)
cannot unmarshal !!int into string (Go), expected str, got intCorrect YAML, wrong type — coercion, not syntaxQuote the value; do not loosen the schema (Rule 1)
control characters are not allowedUTF-16 file, a BOM mid-stream, or a stray \x00/\rCheck encoding and line endings (strings.md)
Parse succeeds, tool says the field is missingThe key landed in the wrong parent — indentation, not spellingDump the parsed structure and read the actual tree (editing.md)
Same file, one machine worksDifferent library version or spec version, not different YAMLParser matrix (parsers.md)
Anything elseBisect: delete the bottom half of the document, re-parse, repeat. Five rounds locate any error in a 1,000-line fileerrors.md

Output Gates

Before delivering any YAML file, patch, or loader snippet:

  • Is every value whose type matters quoted — bools-in-disguise, leading zeros, HH:MM, versions, ids, dates (Rule 1)?
  • Does it parse under the target's spec version, and was it checked with the consumer's own validator (Rule 10)?
  • Indentation all spaces, one width, one sequence style; no duplicate keys?
  • Do multiline blocks use | where newlines are data, with the chomping indicator that produces the intended trailing newline (Rule 5)?
  • If this file will be read and rewritten by a tool, does it avoid anchors — or is losing them acceptable (Rule 8)?
  • If I edited a file the user wrote, are their comments, key order, and quote style intact (Rule 9)?
  • No secret value in the file, and none written into ~/Clawic/data/ — pointer only?
  • Did anything durable come out of this — a parser/spec fact, a file that matters, a trap that bit, a working schema or lint config, an agreed cadence? Then it is written to its box in memory-template.md, with its ## Boxes line, in this same turn.

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/yaml/config.yaml.

VariableTypeDefaultEffect
spec_version1.1 | 1.2 | autoautoWhich column of the Resolution Table applies; auto means infer from the consumer's library and say which one was assumed
indent_widthnumber (2-4)2Indentation of every generated file and of every emitter setting suggested (Rule 3)
sequence_indent0 | 22Whether - sits at its key's column or two spaces in; matched to the repo's existing style when one is observed
quote_styleminimal | double | singleminimalQuoting of generated scalars; minimal quotes only what Rule 1 requires, double/single quote every string
max_line_widthnumber (60-200)120Emitter width setting and the yamllint line-length rule; unset emitters wrap at ~80 and churn diffs (Rule 9)
yq_flavorgo | python | nonegoWhich yq syntax examples use — mikefarah (Go) and kislyuk (Python jq wrapper) share a name and nothing else (editing.md)
linteryamllint | prettier | noneyamllintThe lint config generated and the rule names cited in schemas.md
loader_policysafe-only | allow-fullsafe-onlyWhether a full-tag loader may ever be emitted; safe-only also blocks !!python/ and !!java tag examples outside security.md
anchor_policyallow | avoidallowWhether generated files use anchors and merge keys, or repeat themselves for round-trip safety (Rule 8)
schema_gatebooltrueWhether every emitted file must pass the consumer's validator before being presented as done (Rule 10)

Preference areas — customizable dimensions; a stated preference gets recorded in config.yaml and applied from then on:

  • Tooling — editor and its YAML server, lint vs format vs both, which schema store, yq/dasel/ruamel/jq-after-conversion for edits — affects every example's shape
  • Conventions — key order (source vs alphabetical), whether documents open with ---, comment style and header blocks, empty-value spelling (null vs ~ vs omit), list-of-maps vs map-of-maps for named collections — affects generated files and the diff noise they cause
  • Platform — the primary consumer (Kubernetes, a CI provider, Ansible, an app config loader) and its parser; single-doc vs multi-doc files; file layout per environment — affects which spoke's rules take precedence
  • Safety posture — how untrusted the inputs are, whether generated loaders are hardened by default, whether secrets are allowed in-file at all under any encryption — affects security.md defaults
  • Output register — full file vs unified diff, whether the reasoning for each quote is shown, how much of the parsed tree to print when explaining — affects every answer's shape
  • Cadence — schema re-validation after a dependency bump, lint sweep, secret scan over YAML in the repo, spec-version audit — every accepted cadence becomes a row in the ## Due table of memory.md

Traps

TrapWhy it failsDo instead
Fixing a coercion bug by widening the consumer's schemaTurns one bad value into a permanently loose contract, and the next id still arrives as an intQuote at the source (Rule 1)
Trusting that "it parsed" means it is rightYAML's failure mode is a valid document with wrong types — no error is raised anywhereDump the parsed structure and read the types once (editing.md)
load() then dump() to edit a fileComments, order, quote style and anchors all die; the diff is unreviewableRound-trip loader or in-place patch (Rule 9)
Anchors to DRY up Kubernetes or Compose filesExpanded by the first tool that re-emits, and invisible to anyone reading the rendered outputKustomize overlays, Helm values, Compose extends (kubernetes.md)
> for a shell script or a certificateFolds every newline into a space; the script becomes one line and the PEM becomes invalid|, with chomping chosen deliberately (Rule 5)
Deep indentation to express structure the tool does not haveSilently creates a nested map the schema ignores, and nothing reports itValidate against the schema, not against the parser (Rule 10)
Base64 in a Kubernetes Secret treated as protectionEncoding, not encryption — base64 -d is the whole attacksops, sealed-secrets, or an external secret store (security.md)
Commenting out a list item to disable itLeaves - orphans or an empty sequence that parses as null and overrides a default with nothingDelete it, or move it under a disabled key with a comment
Copying YAML out of a web page or a chatNon-breaking spaces and smart quotes survive the paste and produce impossible errorsRetype the indentation, or run it through the linter before reading it
One giant multi-document file for a whole environmentOne syntax error takes down every document, and tooling that splits on --- gets it wrong inside block scalarsOne concern per file; a directory the tool reads in order (config-design.md)
Reusing a version-pinned example without checking the libraryThe same code is safe on PyYAML 6 and an RCE on SnakeYAML 1.xParser matrix before copying a loader (parsers.md)
Letting an emitter wrap long values at its default widthSilent 80-column folding turns a one-line diff into a whole-file diffSet the emitter width from max_line_width (Rule 9)
Hand-editing YAML that a controller writes backThe tool rewrites it, drops the comments, and the next human edit conflictsEdit the source of truth, not the rendered artifact

Where Experts Disagree

  • Quote everything vs quote what needs it. Quote-everything eliminates the entire coercion class and makes review mechanical; minimal quoting keeps the file readable and diffs small. Frontier: files consumed by more than one parser, or authored by people who will not read this table, get quote-everything; a single-consumer file in a linted repo does not (quote_style).
  • Anchors and merge keys. Real DRY-ness for hand-maintained files (CI configs read by one parser), real breakage anywhere the file gets re-emitted or diffed against a rendered version. Nobody argues for anchors in files a controller rewrites (anchor_policy).
  • YAML at all for large configs. The dissent is not aesthetic: significant whitespace plus implicit typing is a hazard surface JSON and TOML do not have, and CUE/Dhall/Jsonnet add types and functions. The counter is ecosystem gravity — Kubernetes, CI and Ansible take YAML, so a generator only moves the problem to render time (config-design.md).
  • Generate vs hand-write. Generated YAML is consistent and reviewable only as a diff of its source; hand-written YAML is reviewable directly and drifts. Frontier is the number of near-identical files: past roughly three environments of the same shape, generation wins.
  • --- at the top of every file. Required only for multi-document streams and for a directive; some linters demand it anyway. Pick one and enforce it, because a mixed repo makes every concatenation script wrong.

Security & Privacy

Credentials: this skill reads and writes YAML files the user points it at. It does NOT store, log, copy, or transmit any secret value found in them, and never writes a credential into ~/Clawic/data/yaml/. Values under key names matching password/token/secret/key/credential, PEM blocks, and Kubernetes Secret data:/stringData: payloads are replaced by their pointer before anything is saved.

Local storage: preferences, memory, the config-file inventory, the toolchain facts and generated artifacts stay in ~/Clawic/data/yaml/ on this machine, plus device rows in ~/Clawic/data/devices/ and project notes in ~/Clawic/data/projects/. File paths, key names, parser versions and schema URLs only.

Guardrails: loaders are emitted in safe mode by default (loader_policy); full-tag loaders and !!python/-style tags appear only in security.md, marked as the vulnerability they are. Destructive edits (in-place rewrite, yq -i, deleting a document from a stream) name the file and require explicit confirmation.

More Clawic skills, get them at https://clawic.com/skills/yaml (install if the user confirms):

  • json — JSON payload design, JSON Schema authoring, jq
  • k8s — what the manifests mean once they parse: pods, probes, rollouts, RBAC
  • github-actions — workflow design and CI semantics above the YAML layer
  • ansible — playbook semantics, variable precedence, idempotence
  • toml — the other config format, when YAML's implicit typing is the problem

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/yaml.

Questions people ask

Why does the same YAML load in Python but fail in Go?
Spec version and resolver differ between libraries — YAML 1.1 (PyYAML, Ruby Psych, go-yaml v2, SnakeYAML 1.x) resolves sexagesimals, octal, and y/n/yes/no booleans; YAML 1.2 leaves them as strings. The parsers.md matrix maps library to spec and resolver.
Does it preserve comments and key order when editing YAML from code?
Only with a round-trip API. Plain load returns data, not text — PyYAML dump sorts keys alphabetically and wraps long lines, and comments disappear. ruamel.yaml round-trip mode, yq in-place edits, or a text patch keep the file intact.
What's covered for tool-specific YAML like Kubernetes manifests or GitHub Actions?
Multi-doc streams and string-typed fields for Kubernetes/Helm/kustomize, `on:` truthiness and `${{ }}` quoting for CI workflows, and per-tool tags and templating for Ansible, Compose, CloudFormation, OpenAPI, Home Assistant, and cloud-init. Validation uses the consumer's own parser (kubeconform, helm template, etc.) as the final gate.

Related skills

Get Markdown that renders correctly in GitHub, MDX, Pandoc, docs sites, Slack, Notion, and other parsers.

296 installs7 stars

Diagnose and fix JSON failures across parse, encoding, schema, query, size, and contract layers.

by Iván112 installs3 stars

Produces correct, hardened Bash scripts — quoting, strict mode, traps, and macOS/Linux portability built in.

153 installs3 stars

Diagnose and fix web service failures by tracing requests hop-by-hop across proxy, app, and dependency.

69 installs2 stars

Diagnose and fix Visual Studio Code at the editor layer: settings scopes, debug, formatters, extensions, keybindings, remotes.

103 installs3 stars

Debug, write, and review Go code with grounded guidance on goroutines, errors, modules, and the standard library.

82 installs3 stars