Verdict: use it — but the first thing to review isn’t the tool, it’s which tool you installed. yq is the answer to “I have jq muscle memory and a YAML file” — query it, edit it in place, convert it to JSON and back. The catch, and it’s a big one, is that “yq” is the name of two different, unrelated programs with different authors, different syntax, and different default output. apt, pip, and snap do not agree on which one you get. Everything after this sentence is us finding that out for real on an Ubuntu 24.04 box.
We have no relationship with either project; both are free and open source (MIT), nothing to sell, no telemetry to disclose. The honest catch here isn’t price — it’s identity.
The name collision, which is the whole review
There are two yqs:
- Mike Farah’s yq (
github.com/mikefarah/yq) — a standalone Go binary. Its own query syntax (jq-ish, not jq). Outputs YAML by default. This is the one most tutorials mean. - kislyuk’s yq (
github.com/kislyuk/yq) — a Python script that transcodes YAML to JSON and pipes it through actual jq, which it depends on. Its help text literally says so:
$ yq --help
yq: Command-line YAML processor - jq wrapper for YAML documents
yq transcodes YAML documents to JSON and passes them to jq.
Now watch which one each package manager hands you. On the same machine:
$ apt-cache show yq | grep -E 'Version|Depends|Homepage'
Version: 3.1.0-3
Depends: jq, python3-argcomplete, python3-toml, python3-xmltodict, python3-yaml, python3:any
Homepage: https://github.com/kislyuk/yq
apt gives you kislyuk, version 3.1.0, and it drags in jq as a dependency. pip install yq also gives you kislyuk, but version 4.1.1 — a completely different version number for the same program, because pip and Debian package it on different tracks. And snap install yq:
$ snap info yq | grep -E 'summary|publisher'
summary: A lightweight and portable command-line YAML processor
publisher: Mike Farah (mikefarah)
snap gives you Mike Farah’s Go binary. Three package managers, two programs, and version numbers (3.1.0 vs 4.1.1 vs 4.53) that tell you nothing about which one you’re holding.
So the very first command to run on any machine is “who are you”:
$ yq --version
yq (https://github.com/mikefarah/yq/) version v4.53.3
That URL in the version string is the only reliable tell. If it says mikefarah, you have the Go one. If --version prints something like yq 3.1.0 with no URL — or if yq --help says “jq wrapper” — you have kislyuk. On this box the binary in /usr/bin/yq is Mike Farah’s, installed by hand (dpkg -S /usr/bin/yq finds no owning package), which is a fourth way to end up with a fourth version.
Why the collision actually bites
It would be a trivia-grade problem if the two behaved the same. They don’t. Here is the identical command, same file, run against each binary:
$ cat demo.yml
# a service config
service:
name: web1
port: 8080
tags: [prod, edge]
$ yq '.service.name' demo.yml # Mike Farah (Go)
web1
$ yq '.service.name' demo.yml # kislyuk (Python)
"web1"
One prints web1, the other prints "web1" with quotes, because kislyuk’s default output is JSON and Mike Farah’s is YAML. Ask for the whole document and the split is total:
$ yq '.' demo.yml # Mike Farah — stays YAML, keeps the comment
# a service config
service:
name: web1
port: 8080
tags: [prod, edge]
$ yq '.' demo.yml # kislyuk — turns it into JSON
{
"service": {
"name": "web1",
"port": 8080,
"tags": [
"prod",
"edge"
]
}
}
A shell script that does port=$(yq '.service.port' config.yml) and expects 8080 gets 8080 from one and — well, still 8080 for a bare int, but the moment a value is a string, one flavor hands your script bare text and the other hands it a quoted JSON string. That’s the class of bug that passes every test on your laptop and breaks on the CI box that installed yq from a different repo. Pin the flavor in your install step. Do not assume.
What it does (using Mike Farah’s, the one worth standing up)
For the rest of the review we’re on Mike Farah’s Go yq, because it’s the standalone binary that drops into a Dockerfile the way jq does. It reads and writes YAML natively, so it’s genuinely useful on config files — including this site’s own backlog. Here’s a real query against the file that queues these reviews:
$ yq '.backlog | group_by(.status) | map({"status": .[0].status, "n": length})' _data/backlog.yml
- status: done
n: 72
- status: todo
n: 15
- status: blocked
n: 1
That’s 72 done, 15 to-do, 1 blocked — computed straight off the YAML, no JSON detour. And the query that decided this article existed:
$ yq '[.backlog[] | select(.status == "todo" and .kind == "tool")] | length' _data/backlog.yml
0
Zero to-do tool items on the board, which is how a review of yq ended up being the honest thing to write. The tool documented its own backlog being empty. We appreciate the recursion.
Editing in place: one good surprise, one bad one
-i edits the file on disk. The good surprise: Mike Farah v4 preserves your comments and formatting and touches only the line you changed.
$ yq -i '.service.port = 9090' mf.yml
$ diff demo.yml mf.yml
4c4
< port: 8080
---
> port: 9090
One line changed. The # a service config comment survived, and tags: [prod, edge] stayed on one line instead of exploding into a block list. That was not true of older yq versions and is not true of the other yq. Run the same edit through kislyuk with the -y flag it forces you to add:
$ yq -y -i '.service.port = 9090' kl.yml # kislyuk
$ cat kl.yml
service:
name: web1
port: 9090
tags:
- prod
- edge
The comment is gone, and [prod, edge] got reflowed into a block list. kislyuk round-trips through JSON, and JSON has no comments, so they don’t come back. If you yq -y -i a hand-formatted config with kislyuk, you rewrite the entire file’s style and delete every comment — on the same command that Mike Farah’s yq would have left almost untouched. (Credit where due: kislyuk at least refuses to edit in place unless you pass -y/-Y/-t, so it won’t silently overwrite your YAML with JSON. It errors out instead: -i/--in-place can only be used with -y/-Y/-t/-T/-x.)
The type-coercion footgun (leave your quotes on)
This one bit us and stays in. Assign a value without quotes in the expression and yq infers its type — which is usually what you want, until the value is a string that looks like a number:
$ cat zip.yml
zip: "02139"
$ yq -i '.zip = 02139' zip.yml
$ cat zip.yml
zip: !!int 02139
yq re-tagged it !!int. The quotes are gone and it’s now an integer, so the instant anything converts that file to JSON the leading zero evaporates:
$ yq -o=json '.' zip.yml
{
"zip": 2139
}
02139 became 2139. A ZIP code, a phone number, a zero-padded ID, a git SHA that happens to be all digits — bare-assign any of them and you’ve silently changed both the type and the value. The fix is boring and reliable: quote the value in the expression (.zip = "02139") and yq keeps it a string. But the default is coercion, and coercion is the thing that ruins a config file quietly.
Where plain jq (and plain text) still win
- Your data is already JSON. Then you want jq, full stop. yq’s YAML handling is dead weight on a
.jsonfile, and Mike Farah’s dialect is almost jq but not exactly — expressions you know from jq occasionally need tweaking. If it’s JSON in and JSON out, use the real thing. - You only want to read one value and never write.
grepor a two-line Python snippet has no install-flavor ambiguity and no chance of re-tagging your types. yq earns its place when you’re editing YAML in a script or CI step, not when you’re eyeballing it. - Anchors, aliases, and multi-document streams get complicated fast; yq can handle them but the flags are their own afternoon. For a flat config, yq is a joy. For a Helm chart full of anchors, budget reading time.
What it costs and the free alternatives
Nothing — both yqs are MIT, no account, no paid tier, no telemetry. The alternatives are really the other yq and jq:
- kislyuk’s yq is genuinely good if you already live in jq and only occasionally touch YAML — it’s literally jq with a YAML front door, so every jq filter you know works unchanged. The price is the comment-stripping in-place edit and the Python + jq dependency chain.
- Mike Farah’s yq is the better default for editing YAML config files, because it preserves comments and ships as one static binary. The price is a query dialect that’s jq-shaped but not jq.
- jq itself for anything that’s actually JSON.
What made us close the tab
Nothing closed it — yq stays, because editing YAML in a script without it means a Python heredoc every time. But the honest caveats, so you’re not surprised:
- There are two yqs and your package manager picked for you. Run
yq --versionand read the URL before you trust any tutorial or script.apt/pip= kislyuk (Python, JSON-default, needs jq);snap/most manual installs = Mike Farah (Go, YAML-default). Same command name, different program. - The two disagree on default output. YAML from Mike Farah, JSON from kislyuk. A script that parses yq’s output is coupled to the flavor it was written against.
- In-place edits are not equal. Mike Farah v4 keeps comments; kislyuk
-ydeletes them and reflows the file. - Bare assignments coerce types.
.zip = 02139becomes!!int 02139and loses the leading zero. Quote the value unless you want type inference.
When it goes wrong: if a yq command from a blog post errors on your machine, you almost certainly have the other yq — check --version first, not your syntax. If a config file comes back with its comments missing after an edit, you ran kislyuk -y -i; switch to Mike Farah’s binary or stop editing in place. And if a value that was a zero-padded string turns into a number, you assigned it bare — put the quotes back and re-run.