GitHub CLI (gh) — API Surface#
API types#
CLI (primary) + Library (secondary, for extension authors via go-gh)
gh is a user-facing CLI tool. It has no inbound HTTP or gRPC server. Its “API surface” is the command-line interface itself: commands, subcommands, flags, and environment variables. Internally it consumes GitHub’s REST and GraphQL APIs, but does not expose them.
CLI#
Framework#
Cobra (github.com/spf13/cobra) with a hierarchical command tree.
Command registration#
All commands are registered synchronously in pkg/cmd/root/root.go:NewCmdRoot(). Commands are divided into two groups based on how the base repository is resolved:
- Standard factory commands — repo-agnostic (auth, config, gist, search, extension, etc.)
repoResolvingCmdFactorycommands — receiveSmartBaseRepoFunc, which calls the GitHub API to resolve fork networks (pr, issue, repo, release, run, workflow, ruleset, browse, label, cache, api, org, agent-task)
Three Cobra groups are registered at the root level:
core— user-facing GitHub object commandsactions— CI/CD workflow commandsextension— dynamically discoveredgh-*extension executables
Command structure (full tree)#
gh
├── [core] pr # Pull requests
│ ├── list / create / status
│ ├── view / diff / checkout / checks
│ ├── review / merge / update-branch / ready
│ ├── comment / close / reopen / revert / edit
│ └── lock / unlock
│
├── [core] issue # Issues
│ ├── list / create / status
│ ├── view / comment / close / reopen / edit / develop
│ ├── lock / unlock / pin / unpin / transfer / delete
│
├── [core] repo # Repositories
│ ├── list / create
│ ├── view / clone / fork / setdefault / sync / edit
│ ├── deploy-key / license / gitignore
│ └── rename / archive / unarchive / delete / credits / garden / autolink
│
├── [core] release # Releases
│ ├── list / create
│ └── view / edit / upload / download / delete / delete-asset / verify / verify-asset
│
├── [core] gist # GitHub Gists
│ └── clone / create / list / view / edit / delete / rename
│
├── [core] auth # Authentication
│ └── login / logout / status / refresh / git-credential / setup-git / token / switch
│
├── [core] config # Configuration
│ └── get / set / list / clear-cache
│
├── [core] secret # Actions secrets
│ └── list / set / delete
│
├── [core] variable # Actions variables
│ └── list / set / delete / get
│
├── [core] ssh-key # SSH keys
│ └── list / add / delete
│
├── [core] gpg-key # GPG keys
│ └── list / add / delete
│
├── [core] org # Organizations
│ └── list
│
├── [core] project # GitHub Projects (v2)
│ ├── list / create / copy / close / delete / edit / link / view / template / unlink
│ ├── item-list / item-create / item-add / item-edit / item-archive / item-delete
│ └── field-list / field-create
│
├── [core] codespace # GitHub Codespaces
│ └── code / create / edit / delete / jupyter / list / view / logs / ports / ssh / cp / stop / select / rebuild
│
├── [core] label # Repository labels
│ └── list / create / delete / edit / clone
│
├── [core] cache # Actions cache
│ └── list / delete
│
├── [core] ruleset # Branch rulesets
│ └── list / view / check
│
├── [core] attestation # Artifact attestations
│ └── (verify, download, etc.)
│
├── [core] search # GitHub search
│ └── code / commits / issues / prs / repos
│
├── [core] browse # Open in browser
├── [core] status # Account activity summary
├── [core] copilot # GitHub Copilot
├── [core] accessibility # Accessibility settings
├── [core] preview # Preview features
├── [core] licenses # License templates
│
├── [actions] run # Workflow runs
│ └── list / view / rerun / download / watch / cancel / delete
│
├── [actions] workflow # Workflow definitions
│ └── list / enable / disable / view / run
│
├── api # Raw REST/GraphQL API access
├── alias # Command aliases
│ └── list / set / delete / import
├── extension # Extension management
│ └── list / install / remove / upgrade / create / exec / browse
├── completion # Shell completion
├── version # Show version
├── agent-task # GitHub agent tasks (new)
├── credits
│
└── [extension] <gh-*> # Dynamically discovered extension executablesTotal: ~35 top-level command groups, ~150+ leaf commands.
Global flags#
| Flag | Description |
|---|---|
--help | Show help for command |
--version | Show gh version (root only) |
Per-command repo override (repo-aware commands)#
Commands that use repoResolvingCmdFactory also accept:
| Flag | Description |
|---|---|
-R, --repo [HOST/]OWNER/REPO | Override repository for the command |
Applied via cmdutil.EnableRepoOverride(cmd, f) on pr, issue, repo, release, and similar groups.
Output format flags (most list/view commands)#
Added via cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields):
| Flag | Description |
|---|---|
--json <fields> | Output JSON with specified fields (comma-separated, tab-completable) |
-q, --jq <expr> | Filter JSON output with a jq expression |
-t, --template <tmpl> | Format JSON output with a Go template |
--web (where applicable) | Open in browser instead of terminal output |
gh api — raw API access flags#
| Flag | Description |
|---|---|
-X, --method METHOD | HTTP method (default GET) |
--hostname HOST | Override GitHub hostname |
-F, --field key=value | Typed parameter (auto-converts bool, int, @file) |
-f, --raw-field key=value | String parameter |
-H, --header key:value | Extra HTTP request header |
-p, --preview name | Opt into API preview |
-i, --include | Include response headers in output |
--paginate | Fetch all pages |
--slurp | Collapse paginated JSON arrays into one |
--input file | Request body from file or stdin (-) |
--silent | Suppress response body |
-t, --template tmpl | Go template for output |
-q, --jq expr | jq filter for output |
--cache duration | Cache response (e.g. 1h, 60m) |
--verbose | Show full HTTP request/response |
Flag patterns#
- Persistent flags (inherited):
--help,--repo - Local flags: per-command, set via
cmd.Flags().StringVarP()etc. with direct struct binding - No env-var binding in flags: env vars are read separately in the factory, not via Cobra’s env binding. Commands receive already-resolved values through
Factory. - Functional injection for tests:
NewCmdList(f, runF)— therunFparameter allows tests to inject a custom run function, bypassing Cobra and flag parsing entirely.
Flag pattern for commands (canonical template)#
// pkg/cmd/<topic>/<verb>/<verb>.go
type ListOptions struct {
Limit int
Assignee string
// ...
Exporter cmdutil.Exporter // --json/--jq/--template
IO *iostreams.IOStreams
HttpClient func() (*http.Client, error)
// ...
}
func NewCmdList(f *Factory, runF func(*ListOptions) error) *cobra.Command {
opts := &ListOptions{...}
cmd := &cobra.Command{
Use: "list",
RunE: func(cmd *cobra.Command, args []string) error {
if runF != nil { return runF(opts) }
return listRun(opts)
},
}
cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum number of results")
cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields)
return cmd
}Authentication flow#
PersistentPreRunEon the root command callscmdutil.CheckAuth(cfg)before every command that has auth enabled (all exceptauth,version,completion,help, and a few others)- Auth is disabled per-subtree via
cmdutil.DisableAuthCheck(cmd)(used onauthitself) - Token lookup order:
GH_TOKENenv →GITHUB_TOKENenv → system keyring →~/.config/gh/hosts.yml - Failed auth returns
AuthError→ mapped toexitAuth=4exit code
Environment variables (API surface for scripting)#
gh provides a rich set of env vars that scripts and CI can use to control behavior without flags:
| Variable | Purpose |
|---|---|
GH_TOKEN, GITHUB_TOKEN | Auth token (GH_TOKEN takes precedence) |
GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN | GHES auth token |
GH_HOST | Override GitHub hostname |
GH_REPO | Override repository ([HOST/]OWNER/REPO) |
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOR | Editor for interactive input (in precedence order) |
GH_BROWSER, BROWSER | Browser for --web commands |
GH_DEBUG | Enable verbose logging; api for API-only debug |
GH_PAGER, PAGER | Paging program for long output |
GH_FORCE_TTY | Force terminal-style output even when piped |
GH_COLOR_LABELS | Show label colors with RGB hex codes |
GH_ACCESSIBLE_COLORS | Use 4-bit accessible colors (preview) |
GH_NO_UPDATE_NOTIFIER | Disable update notifications |
GH_NO_EXTENSION_UPDATE_NOTIFIER | Disable extension update notifications |
GH_CONFIG_DIR | Override config directory |
GH_PROMPT_DISABLED | Disable interactive prompting |
GH_PATH | Override path to gh executable |
GH_MDWIDTH | Max line width for markdown rendering |
GH_ACCESSIBLE_PROMPTER | Use accessible prompts (preview) |
GH_SPINNER_DISABLED | Replace spinner with static output |
GH_COBRA | Opt out of gh’s custom Cobra overrides |
Plugin / Extension system#
Mechanism#
Interface-based with subprocess execution (os/exec). Extensions are Git repositories or pre-built binaries installed under ~/.local/share/gh/extensions/ as gh-<name> executables.
Registration#
At startup, root.NewCmdRoot calls em.List() and for each extension registers a Cobra command:
for _, e := range em.List() {
extensionCmd := NewCmdExtension(io, em, e, nil)
// skip if conflicts with core command
cmd.AddCommand(extensionCmd)
}Extensions appear as peers of core commands in gh --help, with their own group (extension).
Dispatch#
ExtensionManager.Dispatch(args, stdin, stdout, stderr) runs the matching gh-<name> binary. On Unix, calls are routed through sh to support script files. On Windows, uses findsh to locate a shell.
Env vars injected into extension subprocess:
GH_TOKEN— auth token for the current user/hostGH_HOST— current GitHub hostGH_REPO— current repo context
ExtensionManager interface (pkg/extensions/extension.go)#
type ExtensionManager interface {
List() []Extension
Install(ghrepo.Interface, string) error
InstallLocal(dir string) error
Upgrade(name string, force bool) error
Remove(name string) error
Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error)
}Extension points#
gh-*executables: any binary onPATHor in~/.local/share/gh/extensions/that starts withgh-- Language support: any language; the
go-ghlibrary provides a Go SDK for readingGH_TOKEN, calling GitHub APIs, and inheritingghconfig - Tab completion: extensions can provide their own completions by implementing specific subcommand arguments
- First-class CLI citizens: extensions appear in
gh extension list,gh --help, and tab-complete alongside core commands
Library API (secondary)#
While gh itself is not published as a library, its companion package github.com/cli/go-gh/v2 provides a public Go API for extension authors:
go-gh/v2/pkg/api— authenticated HTTP and GraphQL clientsgo-gh/v2/pkg/config— readghconfig from extensionsgo-gh/v2/pkg/repository— parse and resolve repositoriesgo-gh/v2/pkg/jq/template— JSON filtering utilities
The gh codebase keeps commands in pkg/ (not internal/) intentionally to allow the companion go-gh SDK and extension authors to reference command-level types.
Backward compatibility#
ghdoes not publish a stable Go library API — thepkg/exposure is an ecosystem affordance, not a versioned contract.- The CLI itself follows semantic versioning for flag/command compatibility; breaking changes to flags or output format are treated as breaking changes.
Key design observations#
Zero-framework CLI architecture: Cobra used purely as a dispatch/parsing layer. All business logic lives in
runFfunctions that are fully testable without invoking Cobra. This is a textbook pattern for testable CLI design.--json/--jq/--templateuniformity: Thecmdutil.AddJSONFlagsutility is applied consistently across all list/view commands (50+ commands), creating a uniform machine-readable output surface. Anyghcommand that prints a table also accepts--json— makingghscriptable without special-casing per command.Extensions as first-class commands: Discovered at boot, registered in the same command tree, receiving the same auth tokens — not an escape hatch but a real extensibility tier. Combined with
go-gh, this makesgha platform, not just a CLI.Structured exit codes:
0=OK,1=error,2=cancel,4=auth,8=pending— a typed exit-code API that CI scripts can use to distinguish auth failures from general errors without parsing stderr.Environment variable API: The ~20
GH_*variables form a scripting API parallel to the flag API. In CI environments where flags can’t be passed (e.g. GitHub Actions scripts), env vars are the primary configuration mechanism.