GitHub CLI Cheatsheet
JSON Output and Templates
Use this GitHub CLI reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The Three Flags
Most gh list and view commands accept the same trio. --json is a prerequisite: --jq and --template do nothing without it.
| Flag | Purpose |
|---|---|
--json f1,f2 | Return JSON with exactly those fields, comma separated, no spaces |
-q, --jq <expr> | Filter with jq syntax, no jq binary needed |
-t, --template <tpl> | Format with a Go template |
gh pr list --json number,title,author gh pr list --json number,title --jq '.[].title' gh pr list --json number,title --template '{{range .}}{{.number}}{{"\n"}}{{end}}'
Available on gh pr list/view/status/checks, gh issue list/view/status, gh repo list/view, gh release list/view, gh run list/view, gh workflow list/view, gh cache list, gh search *, and gh auth status.
Discovering the Field Names
Run the command with --json and no value. It errors out and prints every valid field for that command.
gh pr list --json gh issue view --json gh run list --json gh repo view --json
Specify one or more comma-separated fields for `--json`: additions assignees author autoMergeRequest baseRefName ...
Field names are camelCase here (headRefName, createdAt, reviewDecision), unlike the snake_case that raw gh api returns. Fields differ per command, and list often exposes fewer than view, so check each one rather than assuming.
jq Filtering
--jq gets the full jq language. Output is pretty-printed on a terminal and compact when piped.
gh pr list --json number,title,author \ --jq '.[] | "#\(.number) \(.title) by \(.author.login)"' gh pr list --json number,isDraft --jq '[.[] | select(.isDraft)] | length' gh issue list --json number,labels \ --jq '.[] | select(.labels | map(.name) | index("bug")) | .number' gh pr list --json title,additions,deletions \ --jq 'sort_by(.additions) | reverse | .[0:5]' gh repo list --json name,pushedAt --jq 'map(.name) | join("\n")' gh run list --json conclusion --jq 'group_by(.) | map({(.[0]): length})' gh pr view 123 --json labels --jq '.labels[].name' gh release list --json tagName --jq '.[0].tagName'
Useful patterns: .[] explodes an array, select(...) filters, "\(.x)" interpolates, map(...) transforms, || empty drops nulls, and -r is unnecessary because gh already emits raw strings for a scalar result.
Go Templates
--template is for shaping terminal output. Ranges, field access, and printf are the base.
gh pr list --json number,title \ --template '{{range .}}#{{.number}} {{.title}}{{"\n"}}{{end}}' gh pr list --json number,title \ --template '{{range .}}{{printf "#%-5v %s\n" .number .title}}{{end}}' gh pr view 123 --json title,body --template '{{.title}}{{"\n\n"}}{{.body}}' gh issue list --json number,labels \ --template '{{range .}}{{.number}}: {{.labels | pluck "name" | join ", "}}{{"\n"}}{{end}}'
Whitespace is literal, so newlines and tabs come from {{"\n"}} and {{"\t"}}, or from printf.
Template Helpers
Beyond the Go standard library, gh adds these.
| Helper | Effect |
|---|---|
{{range .}}…{{end}} | Iterate an array |
{{.field}}, {{.a.b}} | Field access, nested with dots |
printf "fmt" args | Go fmt formatting, e.g. printf "#%-5v" |
join <sep> <list> | Join a list of values |
pluck <field> <list> | Collect one field from every item |
tablerow <fields>... | Queue a row, aligned into columns |
tablerender | Render the queued rows in place, then start a new table |
timeago <time> | Relative time, e.g. "about 2 days ago" |
timefmt <fmt> <time> | Go Time.Format layout, e.g. "2006-01-02" |
truncate <n> <input> | Cut to n characters |
color <style> <input> | ANSI color, always |
autocolor <style> <input> | ANSI color, only when attached to a terminal |
hyperlink <url> <text> | Terminal hyperlink escape sequence |
contains, hasPrefix, hasSuffix, regexMatch | Sprig string predicates |
Use autocolor, not color, in anything that might be piped, or the escape codes end up in the file. Only the four Sprig predicates above are wired in, so a broader Sprig helper like ternary fails with function "ternary" not defined.
Tables and Real Examples
tablerow collects rows and tablerender flushes them, which is how you get aligned columns without knowing the widths.
gh pr list --json number,title,headRefName,updatedAt --template \
'{{range .}}{{tablerow (printf "#%v" .number | autocolor "green") \
.title .headRefName (timeago .updatedAt)}}{{end}}'gh issue list --json number,title,labels --template \
'{{tablerow "ID" "TITLE" "LABELS"}}{{range .}}{{tablerow \
(printf "#%v" .number) (truncate 45 .title) \
(.labels | pluck "name" | join ",")}}{{end}}{{tablerender}}'gh issue list --json title,url \ --template '{{range .}}{{hyperlink .url .title}}{{"\n"}}{{end}}' gh release list --json tagName,publishedAt \ --template '{{range .}}{{.tagName}} {{timefmt "2006-01-02" .publishedAt}}{{"\n"}}{{end}}' gh run list --json conclusion,name,createdAt --template \ '{{range .}}{{tablerow .conclusion (truncate 30 .name) \ (timeago .createdAt)}}{{end}}{{tablerender}}'
Without a trailing {{tablerender}}, rows still render at the end of the template, but a second table in the same template needs an explicit render between them.
jq or Template
Both filter, so pick by intent.
--jqwhen the answer is data: counting, aggregating, reshaping, or piping into another program. It has real control flow, arithmetic, andgroup_by.--templatewhen the answer is terminal output: aligned columns, relative timestamps, color, and clickable links. It has the presentation helpers jq lacks.
# data question gh pr list --state merged --limit 200 --json mergedAt \ --jq 'group_by(.mergedAt[0:7]) | map({month: .[0].mergedAt[0:7], n: length})' # presentation question gh pr status --json number,title --template \ '{{range .needsReview}}{{tablerow (printf "#%v" .number) .title}}{{end}}{{tablerender}}'
One gotcha worth knowing: --json fields are fetched, so asking for heavy ones (commits, files, comments, statusCheckRollup) on a large --limit is slow and can hit secondary rate limits. Request only the fields you actually print.