GitHub CLI (gh) Reference
Fence policy
Coding agents run fenced. Fence permits the everyday mutations:
git push, gh pr comment, gh-review-reply, gh issue create,
gh issue edit, gh issue comment, gh issue develop, gh project item-add,
gh project item-edit, gh run rerun, gh run cancel, gh pr update-branch,
and gh pr review --approve. Invoking a command that names a mutation
is the consent for that mutation, so run it rather than asking again.
Fence denies raw gh api, gh pr merge, gh workflow run, the
gh release mutations, gh repo create and gh repo edit, gh config,
gh secret, gh variable, and the other destructive namespaces. Output
those for the operator to run in an unfenced shell. Raw reads go through
gh-api-safe.
Body text policy
Every command below that carries a --body or --body-file publishes
under the user's name. Apply contribution-voice before writing that text.
Read it first unless its complete, current instructions are already in this context.
It governs the structure: length, layout,
sign-offs, and the cut pass.
This covers gh pr create, gh pr comment, gh pr review,
gh issue create, gh issue comment, and gh-review-reply.
Prefer the dedicated commands where one fits, because each already loads
the skill: make-pr, post-comment, post-issue, and
post-code-review. Reach for a bare gh call only when no command
covers the case.
Pull Requests
# List
gh pr list
gh pr list --state merged --limit 10
gh pr list --json number,title,headRefName,statusCheckRollup
# View
gh pr view 123
gh pr view 123 --comments
gh pr view 123 --json state,mergeable,mergeStateStatus | jq
# Create
gh pr create --fill # title/body from commits
gh pr create --title "feat: add X" --body "..." --draft
gh pr create --base main --reviewer alice,bob --label "needs-review"
# Merge — `gh pr merge` is denied under Fence. Output the command for
# the operator to run in an unfenced shell; do not execute it.
gh pr merge 123 --squash --delete-branch
gh pr merge 123 --auto --squash # merge once CI passes
# Review & comment
# --approve is allowed under Fence. GitHub refuses approval of your own
# pull request, so that case fails on GitHub's side. Prefer
# `post-code-review`, which drafts the review and then posts it.
gh pr review 123 --approve --body-file review.md
gh pr review 123 --comment --body "LGTM"
gh pr review 123 --request-changes --body "Please fix X"
# `gh pr comment` posts at the top level. To answer inside a review
# comment thread use `gh-review-reply` (see Raw API).
gh pr comment 123 --body "LGTM"
gh pr comment 123 --edit-last --body "Updated: LGTM"
# CI status
gh pr checks 123
gh pr checks 123 --watch # stream until complete
# Edit
gh pr edit 123 --add-label "bug" --add-reviewer charlie
gh pr edit 123 --base develop --title "Updated title"
# Other
gh pr checkout 123
gh pr diff 123
gh pr revert 123 --title "revert: undo X"
gh pr ready 123 # mark draft as ready
gh pr update-branch 123 # merge base into head
Issues
# List
gh issue list
gh issue list --assignee @me --state open
gh issue list --label "bug" --json number,title,state
# View
gh issue view 456
gh issue view 456 --comments
# Create
gh issue create --title "Bug: X fails" --body "Steps..." --label "bug" --assignee @me
# Manage
gh issue edit 456 --add-label "priority" --milestone "v2.0"
gh issue close 456
gh issue reopen 456
gh issue comment 456 --body "Fixed in #123"
gh issue develop 456 --name "fix/issue-456" # create linked branch
CI / Actions
# List runs
gh run list
gh run list --branch main --status failure --limit 5
gh run list --workflow build.yml --json name,status,conclusion,headBranch
# View a run
gh run view 12345678
gh run view 12345678 --verbose # all job steps
gh run view 12345678 --log-failed # logs for failed steps only
gh run view 12345678 --log # full log
gh run view 12345678 --exit-status # non-zero exit if failed (scripts)
# Get job IDs (required for --job flag)
gh run view 12345678 --json jobs --jq '.jobs[] | {name, databaseId}'
gh run view 12345678 --job 98765432
# Watch live
gh run watch 12345678
# Rerun
gh run rerun 12345678
gh run rerun 12345678 --failed # only failed jobs
gh run rerun 12345678 --debug # with debug logging
# Cancel
gh run cancel 12345678
# Trigger workflow_dispatch — `gh workflow run`, `enable`, and `disable`
# are denied under Fence. Output the command for operator consent.
gh workflow run deploy.yml --ref main
gh workflow run deploy.yml -f env=staging -f version=1.2.3
# List workflows
gh workflow list
gh workflow view build.yml
Releases
# Create / upload — the `gh release` family is denied under Fence except
# for `list`, `view`, and `download`. Mutations below require an unfenced
# shell with explicit operator consent.
gh release create v1.2.3 --generate-notes
gh release create v1.2.3 --title "v1.2.3" --notes "Fixes #123" dist/*.tar.gz
gh release create v1.2.3 --draft --prerelease
gh release create v1.2.3 --notes-from-tag # use annotated tag message
gh release upload v1.2.3 dist/binary.tar.gz
# List / view / download (allow-listed reads)
gh release list
gh release view v1.2.3
gh release download v1.2.3
Repository
# View
gh repo view
gh repo view owner/repo --json name,description,defaultBranchRef,isPrivate
# Clone / fork
gh repo clone owner/repo
gh repo fork owner/repo --clone
# Create / edit — denied under Fence (`gh repo create`, `gh repo edit`,
# `rename`, `set-default`, `sync`, `archive`, `unarchive`, `deploy-key`).
# Output for operator-run unfenced shell.
gh repo create my-project --private --clone
gh repo create my-project --public --source=. --push
gh repo edit --default-branch main --enable-auto-merge
gh repo edit --description "New description" --homepage "https://example.com"
# Cross-repo flag (works on most commands)
gh pr list -R owner/other-repo
Search
# Repositories
gh search repos "nix config" --language nix --stars ">100" --sort stars
# Issues and PRs across GitHub
gh search issues "memory leak" --repo owner/repo --state open
gh search prs "fix authentication" --author alice --merged
gh search prs --repo owner/repo --checks failure --state open # failing CI
# Code (legacy engine; for regex use `gh-api-safe 'search/code?q=...'`)
gh search code "sops.placeholder" --repo owner/repo --language nix
Raw API
Default to a dedicated gh subcommand. Use gh-api-safe only when no
subcommand fits. Raw gh api is denied under Fence. Reserve it for
mutations with no subcommand and for @file field input, and output the
command for the operator to run in an unfenced shell.
| Situation | Use |
|---|---|
| Read-only REST fetch | gh-api-safe <path> |
| GraphQL read (queries only) | gh-api-safe graphql -f query='…' |
| Dedicated subcommand exists | that subcommand (gh pr edit, gh issue edit, ...) |
| Reply inside a review comment thread | gh-review-reply <review-comment-url> |
| Other mutation (POST/PATCH/PUT/DELETE) | gh api -X ... in unfenced shell |
Field input from file (-F x=@file) |
raw gh api in unfenced shell |
gh-api-safe wraps gh api, enforces a read-only allow-list with a
defence-in-depth deny-list on the REST path, blocks
-X/--method/-f/-F/--field/--raw-field/--input (except query= value under graphql, where @file is still rejected), and runs a
best-effort GraphQL heuristic that rejects any query whose body contains
a surviving mutation or subscription keyword after comments and
string literals have been stripped. The heuristic is not a real GraphQL
parser; aliased mutations are out of scope and @file queries are
rejected outright. Policy rejections exit 64 with a single-line reason
on stderr; on rejection, switch to the matching dedicated subcommand or
escalate to an unfenced shell rather than retrying the same call. Run
gh-api-safe --help for the full policy summary.
Placeholders {owner}, {repo}, {branch} are replaced from current
git context. Default method is GET.
# GET with jq
gh-api-safe repos/{owner}/{repo}/actions/runs \
--jq '.workflow_runs[:5] | .[] | {name, conclusion, html_url}'
# Paginate all results
gh-api-safe repos/{owner}/{repo}/issues --paginate --jq '.[].title'
# GraphQL (heuristic-screened; mutations and subscriptions are rejected)
gh-api-safe graphql -f query='{ viewer { login } }'
# Rejected: surviving `mutation` keyword (exit 64 on stderr)
gh-api-safe graphql -f query='mutation { addStar(input: {starrableId: "X"}) { starrable { id } } }'
# Out of scope: `@file` query bodies are rejected outright (exit 64)
gh-api-safe graphql -f query=@query.graphql
# Notifications (read only; PUT mark-as-read is blocked by the wrapper)
gh-api-safe notifications --jq '.[] | {reason, subject: .subject.title}'
gh-review-reply is the only write path through the raw API surface
allowed under Fence; every other permitted mutation runs as a dedicated
gh subcommand. It takes the review comment URL and a body file,
nothing else. Owner, repository, pull request number, and comment id are
parsed out of the URL, and one endpoint is built from them,
POST repos/{owner}/{repo}/pulls/{n}/comments/{id}/replies. The reply
body is read from a file so quotes, backticks, and newlines survive
verbatim. {owner} placeholders are not expanded; pass the real URL.
Any other flag (-X, -f, -F, --input, or a glued
--body-file=PATH) exits 64 with a single-line reason on stderr.
The URL must begin with https://github.com/ and its path must be
<owner>/<repo>/pull/<number>, with an optional trailing segment such as
/files. Both anchor forms work: #discussion_r<id> from the
conversation tab and #r<id> from the files tab. An
#issuecomment-<id> fragment names a top-level comment, not a review
comment; use gh pr comment for that.
# Copy the review comment URL from the thread, or find the comment id
gh-api-safe repos/{owner}/{repo}/pulls/123/comments \
--jq '.[] | {id, path, user: .user.login, url: .html_url}'
gh-review-reply https://github.com/owner/repo/pull/123#discussion_r2109876543 \
--body-file reply.md
See home-manager/_mixins/agentic/fence/default.nix for the
authoritative command.allow / command.deny lists,
home-manager/_mixins/agentic/fence/README.md for the policy overview,
and home-manager/_mixins/development/github/gh-api-safe.sh for the
wrapper source.
Unsafe: requires unfenced shell
⚠️ The commands below mutate GitHub state. They use
gh apidirectly with-X/-F/--inputand are rejected bygh-api-safe. They must only be run in an unfenced shell with explicit operator consent. Prefer the dedicatedghsubcommands (gh issue edit,gh pr edit, etc.) wherever they exist.
# PATCH / POST
gh api repos/{owner}/{repo}/issues/456 -X PATCH -F state=closed
gh api repos/{owner}/{repo}/labels -F name="triage" -F color="e4e669"
# Typed fields (-F): true/false/null/integers become JSON types; @file reads file
gh api repos/{owner}/{repo}/issues -F title="Bug" -F body=@issue.md
JSON Output Pattern
Most commands accept --json fields with optional --jq expression:
# Named fields only
gh pr list --json number,title,state,headRefName
gh run list --json name,status,conclusion,headBranch,createdAt
# Filter inline
gh pr list --json number,title,statusCheckRollup \
--jq '.[] | select(.statusCheckRollup | any(.state == "FAILURE")) | .number'
# Check mergeability
gh pr view 123 --json mergeable,mergeStateStatus
# Combine with jq outside gh for complex transforms
gh issue list --json number,title,labels | jq '.[] | select(.labels | any(.name == "bug"))'
Status & Auth
gh status # cross-repo overview: assigned PRs, review requests, mentions
gh auth status # active account, token scopes, expiry
gh auth token # allowed under Fence; never echo the value
# `gh auth setup-git` and `gh auth login --with-token` stay denied.