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.

FlagPurpose
--json f1,f2Return 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.

HelperEffect
{{range .}}…{{end}}Iterate an array
{{.field}}, {{.a.b}}Field access, nested with dots
printf "fmt" argsGo 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
tablerenderRender 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, regexMatchSprig 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.

  • --jq when the answer is data: counting, aggregating, reshaping, or piping into another program. It has real control flow, arithmetic, and group_by.
  • --template when 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.