Query and edit YAML from the command line.

Chart a path to any field in a manifest — and edit it without reformatting the file.

yqr is a jq-style filter for YAML. Point it at a manifest file, a kubectl get -o yaml dump, or a Helm-rendered bundle, and it walks straight to the field you asked for — as a value, not as JSON you have to decode back.

the filter .spec.volumes[0].secret.defaultMode
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  volumes:
    - name: tls
      secret:
        secretName: web-tls
        defaultMode: 0640
default
$ yqr '.spec.volumes[0].secret.defaultMode' pod.yaml
0640
--normalize
$ yqr --normalize '.spec.volumes[0].secret.defaultMode' pod.yaml
640

Kubernetes spells file permissions in octal — defaultMode: 0640 on a Secret or ConfigMap volume. Read that field through yqr and the value comes back exactly as written, because yqr never re-typed it in the first place. Only if you opt into the classic --normalize pipeline is the leading zero lost: 640 is a different number.

installed paths

Where the binary actually lives

Install from crates.io with cargo install yqr, or build any of the paths below from a source checkout.

On your machine

cargo install yqr

Pulls the published crate to ~/.cargo/bin/yqr — keep that directory on PATH so plain yqr resolves from any shell, including one already piping kubectl output.

cargo build --release

Building from source instead? The binary lands at target/release/yqr (or cargo install --path . to put a local checkout on PATH).

Inside a container image

/usr/local/bin/yqr

Build it in a multi-stage Dockerfile and copy just the binary into the runtime stage — no Rust toolchain, no source tree, in the image that actually ships.

Byte-preserving reads are the default. Untouched nodes come back as their original source bytes — comments, quoting, indentation, and line endings survive, and the identity filter reproduces the input byte-for-byte, no flag required.
$ yqr '.' pod.yaml
Pass --normalize (-N) to opt into the classic, re-serializing pipeline (comments dropped, scalars canonicalized). Byte-preserving reads are powered by noyalib's lossless CST — yqr's one and only YAML engine. See the runnable demo for an eight-step walkthrough of navigation, iteration, pipes, raw output, fidelity mode, and validation.
It edits, too — and only the bytes you target. Give it a mutating filter and yqr changes just that node, leaving every other byte — comments, indentation, quoting, key order — untouched, or refuses. Replace a value with =, append to a block sequence with +=, add a key, or drop an entry with del(…):
$ yqr '.spec.replicas = 5' deploy.yaml
$ yqr '.spec.ports += 9090' deploy.yaml
$ yqr 'del(.metadata.labels)' deploy.yaml
$ yqr 'del(.spec.template)' deploy.yaml   # a nested block, closed up cleanly
del removes multi-line and nested block entries as well as single-line ones, closing up the gap and leaving every surviving byte identical; removing the last entry of a block leaves the collection spelled out ({}), since a key with nothing under it reads back as null, and removing an item of an inline collection ([a, b]) takes exactly one separator with it. Add -i (--in-place) and the file is rewritten atomically — a git diff touches only the line you changed. An edit that would restructure the document is refused (exit 5) rather than emitted, and under -i the file is left untouched.
$ yqr -i '.spec.replicas = 5' deploy.yaml
$ git diff deploy.yaml   # one line
Validate after every edit. One command answers whether a file is still correct YAML — and a pass certifies more than "parses": the parsed documents must reproduce the input byte-for-byte, the same invariant behind yqr's fidelity reads. Failures are compiler-style diagnostics with a stable code, a clickable location whenever a position is known, and a suggested fix, so humans and agents can act on them. Edit, then verify:
$ yqr -i '.spec.replicas = 5' deploy.yaml   # edit
$ yqr validate --strict deploy.yaml        # verify -- silent, exit 0
When the file is not correct YAML — a hand edit gone wrong, a half-resolved merge, a truncated write — the verdict names the spot:
$ yqr validate deploy.yaml
error[Y001]: expected a node but found StreamEnd
  --> deploy.yaml:3:7
  |
3 | b: [1,
  |       ^
--strict also flags duplicate mapping keys (Y101) — accepted last-wins by ordinary reads, so a bad edit silently drops data — reporting every duplicate, << merge keys included, with the positions of both occurrences. Keys that collide after string conversion are refused outright (Y102), non-UTF-8 input is a coded finding (Y003), a mapping value that is not indented past its key is flagged by default (Y103) because yqr's engine reads such a file and other implementations refuse it, and a file containing unresolved merge-conflict markers gets a dedicated hint anchored at the first marker. Exit codes are scriptable: 0 all valid, 1 validation findings, 5 an input could not be read. Stdin is explicit (yqr validate -); an empty file list is a usage error, never a silent "all valid".

two ways to run it against a cluster

From an operator's shell, or from inside the image

Piped from kubectl

Standard operator loop: dump a resource as YAML, pull one field out of it, move on.

$ kubectl get pods -o yaml | yqr -r '.items[] | .metadata.name'

One pod name per line.

$ kubectl get pod web-0 -o yaml | yqr -r '.spec.containers[0].image'

The primary container's image, unquoted.

$ kubectl get pod web-0 -o yaml | yqr -r '.spec.initContainers[]? | .image'

Init container images when the pod has any — the trailing ? keeps pods with none from erroring the pipeline.

$ yqr validate --strict manifests/*.yaml

A gate before kubectl apply: every manifest must parse, round-trip byte-for-byte, and carry no duplicate keys. Exit 1 with a located diagnostic if not — and an empty file list is a loud usage error, so a glob that matches nothing never passes as “all valid”.

Inside a container image

Bake the binary in, then use it in an init container to read a mounted manifest or ConfigMap before the main container starts.

# -- build --
FROM rust:1.97-slim AS build
WORKDIR /src
COPY . .
RUN cargo build --release

# -- runtime --
FROM debian:bookworm-slim
COPY --from=build /src/target/release/yqr /usr/local/bin/yqr
ENTRYPOINT ["yqr"]
$ yqr -r '.data.enableBeta' /config/values.yaml

Read a flag out of a mounted ConfigMap and hand it to the next step — a common init-container job.

beyond the cluster

It's not just Kubernetes

Anything that's YAML takes the same filters. Three more places yqr earns its keep.

CI/CD pipelines

GitHub Actions workflows are YAML. Audit what a job actually runs without opening the file — these two ran against this repo's own ci.yml.

$ yqr -r '.jobs.test.["runs-on"]' ci.yml

ubuntu-latest — bracket syntax reaches keys a bareword can't spell, like runs-on.

$ yqr -r '.jobs.test.steps[1].with.toolchain' ci.yml

1.97 — confirm the pinned Rust version without scrolling past the cache step.

Docker Compose

Check what a compose file is about to pull and expose before you run it.

$ yqr -r '.services[] | .image' compose.yaml

yqr-demo:latest, postgres:16 — every image referenced, one per line.

$ yqr -r '.services.web.environment.LOG_LEVEL' compose.yaml

debug — one config value, no grep.

Ansible playbooks

A playbook is a YAML list of plays — walk it like any other sequence.

$ yqr -r '.[0].tasks[] | .name' playbook.yml

Install nginx, Start nginx — every task in the first play, at a glance.

$ yqr -r '.[0].hosts' playbook.yml

web — which hosts that play targets.

further afield

Three more, shown in full

Same grammar, different files — this time with the source shown, so nothing here has to be taken on faith.

OpenAPI specs

An OpenAPI document is plain YAML. Point yqr at it to pull a specific operation's details out of a spec someone else wrote, without loading it into an editor.

paths:
  /widgets/{id}:
    get:
      summary: Get a widget
      responses:
        "200":
          description: OK
$ yqr -r '.paths.["/widgets/{id}"].get.summary' openapi.yaml
Get a widget

Path keys have slashes and braces, so a bareword can't spell them — bracket syntax reaches them anyway, the same way .["runs-on"] did for the CI workflow above.

$ yqr -r '.paths.["/widgets"].get.responses.["200"].description' openapi.yaml
OK

Status codes are string keys, and an unquoted 200 in a filter would try to read a number. Bracket syntax reaches the string key "200" exactly as the spec wrote it.

Prometheus alerting rules

Alerting rules are a YAML list of groups, each holding a list of rules. Reading one back tells you exactly what will page someone, and at what threshold.

groups:
  - name: api-slos
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status="5xx"}[5m]) > 0.05
        for: 10m
        labels:
          severity: page
$ yqr -r '.groups[0].rules[] | .alert' rules.yaml
HighErrorRate
HighLatency

Every alert name in the first group, without reading through a file's worth of PromQL to find them.

$ yqr -r '.groups[0].rules[0].expr' rules.yaml
rate(http_requests_total{status="5xx"}[5m]) > 0.05

The exact expression for that alert — useful when you just need to confirm the number that pages someone, not re-read the whole rules file.

Application config

Most services ship a YAML config file alongside the binary — database targets, ports, feature flags. yqr reads it the same way it reads anything else.

database:
  host: db.internal
  port: 5432
featureFlags:
  newCheckout: true
  betaSearch: false
$ yqr -r '.database.host' application.yaml
db.internal

Confirm which database an environment's config actually points at before you run a migration against it.

$ yqr -r '.featureFlags.newCheckout' application.yaml
true

Read a single feature flag's value straight out of the file that ships with the deploy, instead of grepping for it.

filter grammar

What yqr can walk today

The whole grammar — every recipe on this page is built from it.

FilterMeaning
.Identity
.fooField access
.a.bNested field access
.["a.b"]Field access for a key holding a ., / or a space
."a.b"The same step, spelled the jq way; both forms read and edit the key
.[n]Array index (.[-1] counts from the end)
.[]Iterate sequence elements / mapping values
a | bPipe
f?Suppress runtime errors from f (e.g. iterating a field that turns out to be missing or the wrong shape)
to_entriesA mapping becomes {key, value} pairs, so the name travels with the value (guide)
+ - * / %Arithmetic, with the usual precedence; + also joins strings. Numbers keep their type — 4 / 2 is 2, 3 / 2 is 1.5

The write forms are the same paths with a verb: = replaces a value, |= computes a new one from the old (.replicas |= (. + 1)), += appends to a sequence, del(…) removes an entry, key(…) renames, line_comment(…) and head_comment(…) edit comments, and swap(…) / move(…) reorder a list. The Kubernetes guide works through each of them.

Not yet available: select(), map(), length, keys, comparisons and conditionals, and object/array construction — so a filter cannot yet pick entries by a condition or reshape them. Coming from jq? What transfers and what does not is the short version.