# GitHub - gh CLI and API Usage
Scripted work with `gh` and the GitHub API fails in predictable ways: tables parsed as text, rate-limit buckets confused, and state guessed instead of checked. Watch exit codes, respect separate rate-limit buckets and pagination caps, and verify state through the API. Account selection is its own concern: `gh` keeps one active account per host, and some enterprise tokens can read but not write.
## Key points
- Gate scripts on `gh pr checks` exit codes, not table text: 0 pass, 8 pending, 1 fail. Aggregate gates park at pending on draft PRs by design.
- `gh pr checks <PR>` aggregates all check-run statuses in one call. Loop over PR numbers for the CI picture across many PRs.
- `gh pr checks --watch` blocks until CI settles but exhausts the 5,000-call GraphQL quota. The REST check-runs endpoint returns the same data without the limit.
- GraphQL rate-limit errors are transient and use a bucket separate from the REST core limit of 5,000 per hour. A healthy core limit says nothing about the GraphQL bucket.
- `gh pr checks` conflates cancelled jobs with failures. Per-job `conclusion` from `gh run view --json jobs` separates red jobs from fail-fast kills. One platform green and the other red on the same commit points to a nondeterministic test.
- `gh run rerun <run-id> --failed` retries only the failed and cancelled jobs. Use it for infra flakes, not the whole matrix.
- On a 403 or 429 containing "rate limit": query `gh api rate_limit`, sleep until `resources.core.reset`, retry once, and abort before writing state.
- The Actions runs API treats any `exclude_pull_requests` value, including `false`, as true, silently returning no PR feedback. Omit the parameter and pin that with a test. It also caps at 1,000 results per query and returns an empty page past it.
- Read a repo file without cloning: `gh api repos/<owner>/<repo>/contents/<path> --jq '.content' | base64 -d`. `gh search code` misses dotted numeric literals, so negative evidence needs the file read.
- Scan an org's maintainer lists without cloning: `gh api /orgs/<org>/repos --paginate --jq '.[].name'`, then fetch each MAINTAINERS.md via the contents API.
- `has_security_policy` is set by an org-level SECURITY.md, so a repo can show `true` with no local policy file. Check with `gh api repos/<owner>/<repo> --jq '.has_security_policy'`.
- The PR creation cap (`interaction-limits/pulls/creation-cap`) is forward-looking: lowering it never closes open PRs, and drafts never count. Change it with a PATCH that sends `-F enabled=true -F max_open_pull_requests=N`.
- Compare action tags before you request a pin bump. A workflow file can be byte-identical across two versions, so the bump is noise.
- `gh release edit` on an untagged draft regenerates the URL slug and preserves the body. The changed link is normal, not corruption. Verify with a body diff.
- Actions run logs expire (HTTP 410) faster than PR comments. Recover posted benchmark numbers with `gh api repos/<org>/<repo>/issues/<n>/comments --paginate`.
- GitHub auto-redirects `/pull/` and `/issues/`, so either path links a number when you do not know its type.
- `gh issue view <N> --repo <org>/<repo> --json title,body,comments` is the fastest authoritative issue-state check. Fetch issues sequentially. When the body truncates, fall back to the issues API plus its comments endpoint.
- `gh api "search/issues?q=author:<user>+org:<org>+is:merged"` returns a count plus a search URL a skeptic can re-derive in a browser.
- The `/events` endpoint returns events from followed users and watched repos, not only your own. Filter on actor equality. `PullRequestReviewEvent` may take its actor from the review author.
- `author_association` is REST-only and per PR, not a `gh pr list` field.
- `gh` keeps exactly one active account per host, globally. The credential helper always serves that token, so private-org repos fail with "Repository not found", not an auth error.
- A manual `gh auth switch` is not durable. The global active account flips back on its own.
- Run one command under another account with `GH_TOKEN=$(gh auth token -u <account>) gh <command>`, or `gh auth switch -u <account> -h github.com` when a switch is right.
- Enterprise Managed User (EMU) tokens can read the API but not write: PR creation, replies, and review resolution fail with 403 through GraphQL and REST. Interactions must go out as the account that opened the PR.
- Some repositories resolve only under a specific account. Switch accounts rather than retrying the failing slug.
- Set private tap remotes to their SSH URL with `git remote set-url`. SSH authenticates with the key and ignores the active gh account, so `brew update` stops failing on org taps.
- Verify OAuth App SSO with `gh api orgs/<org> --jq .login`: it returns the login when the token scope and the OAuth App SSO grant are both present, and 403 with "SAML enforcement" when the grant is missing, regardless of token validity.
- Setup shortcuts: `gh auth setup-git` wires the gh credential helper for git HTTPS. `gh repo create --source=. --push` sets the remote, upstream, and pushes. When `gh pr create` hits a transient block, retry once with the body passed via a file.
- Post a batched review with `gh api repos/<org>/<repo>/pulls/<n>/reviews --method POST --input <file>`. Every inline entry needs `path`, `line`, and `side:"RIGHT"`. One mismatched anchor fails the whole request.
- Count Gerrit changes by owner domain: query the changes API with `owner:*<domain>+status:merged`, `n=500`, and `DETAILED_ACCOUNTS` for per-author and per-subsystem breakdowns with citable URLs.
- The DevStats ranking call is a POST with api `DevActCnt`, a project, a range, the Contributions metric, and a GitHub id. The response holds parallel `rank`, `login`, and `number` arrays. An empty `login` array means unranked.
## How to apply
1. Gate scripts on `gh pr checks` exit codes. Treat pending on draft PRs as normal.
2. On a rate-limit 403 or 429, sleep until `resources.core.reset`, retry once, then abort before writing state.
3. Triage CI with per-job conclusions, then retry only failed jobs with `gh run rerun <run-id> --failed`.
4. Reach a second account for one command with `GH_TOKEN=$(gh auth token -u <account>) gh <command>`.
## Observations
- [ci] Exit codes, not table text, are the reliable `gh pr checks` signal. #gh-cli (source: agent sessions, 2026-07)
- [api] GraphQL and REST rate limits use separate buckets. #rate-limits (source: agent sessions, 2026-05)
- [api] The runs API silently drops PR feedback when `exclude_pull_requests` has any value. #api (source: agent sessions, 2026-09)
- [auth] A missing-repo error from `gh` can mean the wrong active account. #gh-cli (source: agent sessions, 2026-10)
- [auth] EMU tokens can read but not write, through GraphQL and REST. #gh-cli (source: agent sessions, 2026-11)
## Relations
- relates_to [[resources/documentation/GitHub - Pull Request Workflow]] (PR creation and account behavior)
- relates_to [[resources/documentation/GitHub Actions - Semantics and Caching]] (check-runs and runs API caps)
- relates_to [[resources/documentation/CI - Failure Triage and Silent Non-Runs]] (exit-code and per-job triage)
- relates_to [[resources/documentation/SSH - Debugging Config, Agents, and Forwards]] (SSH remotes bypass the gh account)
## Sources
Personal agent-session notes, January 2026 to November 2026.