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.
.spec.volumes[0].secret.defaultMode
apiVersion: v1
kind: Pod
metadata:
name: web
spec:
volumes:
- name: tls
secret:
secretName: web-tls
defaultMode: 0640
$ yqr '.spec.volumes[0].secret.defaultMode' pod.yaml 0640
$ 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.
$ 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.
=, 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
$ yqr -i '.spec.replicas = 5' deploy.yaml # edit $ yqr validate --strict deploy.yaml # verify -- silent, exit 0When 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.
| Filter | Meaning |
|---|---|
. | Identity |
.foo | Field access |
.a.b | Nested 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 | b | Pipe |
f? | Suppress runtime errors from f (e.g. iterating a field that turns out to be missing or the wrong shape) |
to_entries | A 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.