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.)
  • repoResolvingCmdFactory commands — receive SmartBaseRepoFunc, 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 commands
  • actions — CI/CD workflow commands
  • extension — dynamically discovered gh-* 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 executables

Total: ~35 top-level command groups, ~150+ leaf commands.

Global flags#

FlagDescription
--helpShow help for command
--versionShow gh version (root only)

Per-command repo override (repo-aware commands)#

Commands that use repoResolvingCmdFactory also accept:

FlagDescription
-R, --repo [HOST/]OWNER/REPOOverride 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):

FlagDescription
--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#

FlagDescription
-X, --method METHODHTTP method (default GET)
--hostname HOSTOverride GitHub hostname
-F, --field key=valueTyped parameter (auto-converts bool, int, @file)
-f, --raw-field key=valueString parameter
-H, --header key:valueExtra HTTP request header
-p, --preview nameOpt into API preview
-i, --includeInclude response headers in output
--paginateFetch all pages
--slurpCollapse paginated JSON arrays into one
--input fileRequest body from file or stdin (-)
--silentSuppress response body
-t, --template tmplGo template for output
-q, --jq exprjq filter for output
--cache durationCache response (e.g. 1h, 60m)
--verboseShow 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) — the runF parameter 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#

  • PersistentPreRunE on the root command calls cmdutil.CheckAuth(cfg) before every command that has auth enabled (all except auth, version, completion, help, and a few others)
  • Auth is disabled per-subtree via cmdutil.DisableAuthCheck(cmd) (used on auth itself)
  • Token lookup order: GH_TOKEN env → GITHUB_TOKEN env → system keyring → ~/.config/gh/hosts.yml
  • Failed auth returns AuthError → mapped to exitAuth=4 exit 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:

VariablePurpose
GH_TOKEN, GITHUB_TOKENAuth token (GH_TOKEN takes precedence)
GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKENGHES auth token
GH_HOSTOverride GitHub hostname
GH_REPOOverride repository ([HOST/]OWNER/REPO)
GH_EDITOR, GIT_EDITOR, VISUAL, EDITOREditor for interactive input (in precedence order)
GH_BROWSER, BROWSERBrowser for --web commands
GH_DEBUGEnable verbose logging; api for API-only debug
GH_PAGER, PAGERPaging program for long output
GH_FORCE_TTYForce terminal-style output even when piped
GH_COLOR_LABELSShow label colors with RGB hex codes
GH_ACCESSIBLE_COLORSUse 4-bit accessible colors (preview)
GH_NO_UPDATE_NOTIFIERDisable update notifications
GH_NO_EXTENSION_UPDATE_NOTIFIERDisable extension update notifications
GH_CONFIG_DIROverride config directory
GH_PROMPT_DISABLEDDisable interactive prompting
GH_PATHOverride path to gh executable
GH_MDWIDTHMax line width for markdown rendering
GH_ACCESSIBLE_PROMPTERUse accessible prompts (preview)
GH_SPINNER_DISABLEDReplace spinner with static output
GH_COBRAOpt 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/host
  • GH_HOST — current GitHub host
  • GH_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 on PATH or in ~/.local/share/gh/extensions/ that starts with gh-
  • Language support: any language; the go-gh library provides a Go SDK for reading GH_TOKEN, calling GitHub APIs, and inheriting gh config
  • 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 clients
  • go-gh/v2/pkg/config — read gh config from extensions
  • go-gh/v2/pkg/repository — parse and resolve repositories
  • go-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#

  • gh does not publish a stable Go library API — the pkg/ 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#

  1. Zero-framework CLI architecture: Cobra used purely as a dispatch/parsing layer. All business logic lives in runF functions that are fully testable without invoking Cobra. This is a textbook pattern for testable CLI design.

  2. --json/--jq/--template uniformity: The cmdutil.AddJSONFlags utility is applied consistently across all list/view commands (50+ commands), creating a uniform machine-readable output surface. Any gh command that prints a table also accepts --json — making gh scriptable without special-casing per command.

  3. 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 makes gh a platform, not just a CLI.

  4. 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.

  5. 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.