How to Use kubectl Output With jq and jsonpath the Safe Way
Extract data from kubectl output with jsonpath and jq: single fields, lists, filters, base64 secrets, restarts and image versions, with quoting that survives.

Kubernetes objects are deep JSON documents, and scripts need one field out of them. Parsing the human-readable table is brittle; asking for the field directly is not. Two tools do it: jsonpath inside kubectl, and jq outside. Knowing which to pick keeps scripts short. This follows scripting kubectl, and the examples assume kubectl 1.31 and jq 1.7.
jsonpath for simple fields
kubectl get pod web-6d9f7c8b5-4xk2p -o jsonpath='{.status.phase}{"\n"}'
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.kubeletVersion}{"\n"}{end}'
kubectl get deploy web -o jsonpath='{.spec.template.spec.containers[*].image}'Running
node-1 v1.31.2
node-2 v1.31.2
registry.example.test/web:1.4.2
Expressions use curly braces; [*] selects all items and range ... end loops. There is no trailing newline unless you add {"\n"}. The syntax supports simple filters, as in {.items[?(@.status.phase=="Running")].metadata.name}, but nothing like arithmetic, sorting or string functions.
jq for everything else
kubectl get pods -A -o json | jq -r '
.items[]
| select(.status.containerStatuses != null)
| select(any(.status.containerStatuses[]; .restartCount > 3))
| [.metadata.namespace, .metadata.name, (.status.containerStatuses | map(.restartCount) | max)]
| @tsv'shop web-6d9f7c8b5-4xk2p 7
jobs cleaner-5b8c9d6f4-x2z9q 12
The filter finds pods with more than three restarts and prints a tab-separated report. -r outputs raw strings without JSON quotes. Reading JSON is more stable than tables and keeps working when kubectl adds columns. More jq patterns are in jq for API responses.
Secrets
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
kubectl get secret db-credentials -o json | jq -r '.data | map_values(@base64d)'Secrets are base64-encoded, which is encoding and offers no protection. Anyone who can get secrets in the namespace can read them, so permissions matter more than the encoding. Do not print them in CI logs; see secrets in CI logs. The first form uses base64 -d, which is -D on older macOS.
Quoting traps
name=web
kubectl get deploy "$name" -o jsonpath="{.metadata.name}" # shell sees braces: fine here
kubectl get pods -o jsonpath='{.items[?(@.metadata.labels.app=="'"$name"'")].metadata.name}'
kubectl get pods -l "app=$name" -o name # simpler: a label selectorInterpolating shell variables into a jsonpath filter forces awkward quote switching. A label selector with -l does the filtering on the server and avoids the quoting entirely. With jq, pass values using --arg: jq --arg n "$name" '.items[] | select(.metadata.name==$n)', which also prevents injection from a variable. The general risk is in preventing command injection.
Choosing the output format by who reads it
The same object can be shown four ways, and each suits a different reader. The default table is for a person at a terminal and should never be parsed, because its columns, widths and even its existence of a header change between kubectl versions and between resource types. -o name is for loops, since it prints kind/name pairs that every other subcommand accepts back. -o jsonpath and -o custom-columns are for pulling a few known fields. -o json or -o yaml is for anything that needs the whole object, including a pipe into jq.
When a script needs one value, ask for that value. Fetching the entire list of pods in a large cluster just to read one field moves megabytes through the API server and the network, and the work grows with cluster size even though the answer does not. Selectors narrow the request before it leaves the server: -l filters by label and --field-selector filters by a small set of fields such as status.phase or spec.nodeName.
Empty results and missing fields
Two failures catch scripts that were only run against a healthy cluster. The first is the empty list. A loop over -o name output runs zero times and exits cleanly, so a deploy script that waits for pods to appear can pass when nothing exists. Check the count explicitly, or use kubectl wait, which fails when no resource matches.
The second is the missing field. A pod that has not been scheduled has no status.containerStatuses, so a jq filter that indexes it directly fails with an error about iterating over null. Guard with the optional operator, .status.containerStatuses[]?, or supply a default with // []. In jsonpath a missing field prints nothing and still exits 0, which is convenient for display and dangerous for a check, so a script that depends on a value should test that the result is non-empty before using it.
Exit codes and pipelines
A pipe from kubectl into jq reports the exit status of the last command only, unless set -o pipefail is on. Without it, an authentication failure or a wrong context produces an empty stream, jq happily prints nothing, and the script carries on as if the cluster were empty. Turn on pipefail at the top of any script that pipes kubectl output, and let jq's -e flag set the exit status from the last output value when the script branches on a result.
Keep the command and its parsing in separate steps when debugging. Saving kubectl get ... -o json to a file once lets you iterate on the jq filter against a fixed document, with no cluster calls and no changing data. Once the filter is right, put the two back in a pipeline.
Watching and reacting to changes
Scripts often need to wait for a state, not just read it. kubectl wait handles conditions on a single resource, but a loop over kubectl get -w or --watch-only reacts to a stream of events. The stream stays open until interrupted or until the API server closes it, which happens routinely after several minutes, so a script that watches must be ready to restart the watch and must not assume it saw every event in between.
For anything beyond a quick wait, the right tool is a controller using the API's watch mechanism with a resource version, which resumes without gaps. In shell, a pragmatic alternative is polling at a modest interval with a hard deadline: query the field you care about, compare, sleep, and give up after a fixed time. It wastes a few requests but is simple to reason about, easy to log, and does not break when a connection drops.
Whichever you choose, put a timeout on it. A pipeline step that waits forever for a pod that can never start, because the image tag is wrong, holds a runner for hours. Print the last observed state when the timeout fires, so the failure message says what the object looked like instead of only that time ran out.
A last habit pays off in every kubectl script: print the exact command before running anything that changes state. When a deploy misbehaves, the log then shows the selector, namespace and context that were used, which is usually the whole explanation. It costs one line per call and removes the need to reproduce the run to learn what it did.
Where this does not apply
For listing columns for a person, -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName and -o wide are easier. For large clusters, -o json of all pods is heavy: filter with selectors and field selectors (--field-selector status.phase=Running) first. Custom resources have their own schema; check with kubectl explain. See the kubectl JSONPath reference and the jq manual.


