Install

cli/cli

GitHub CLI (gh) - Developer Wiki

Last updated on Dec 12, 2025 (Commit: 51dfeea)

Overview

Relevant Files
  • README.md
  • cmd/gh/main.go
  • internal/ghcmd/cmd.go
  • docs/project-layout.md
  • go.mod

GitHub CLI (gh) is an official command-line tool that brings GitHub functionality directly to your terminal. It enables developers to manage pull requests, issues, repositories, and other GitHub concepts without leaving their shell environment.

What is GitHub CLI?

gh is a standalone tool (not a proxy to git) that provides a modern, user-friendly interface for GitHub operations. It supports GitHub.com, GitHub Enterprise Cloud, and GitHub Enterprise Server 2.20+ across macOS, Windows, and Linux.

Core Architecture

The project follows a modular Go architecture:

cmd/gh/main.go          → Entry point that initializes the CLI
internal/ghcmd/cmd.go   → Main command dispatcher and error handling
pkg/cmd/               → Individual command implementations
api/                   → GitHub API client and GraphQL queries
pkg/cmdutil/           → Shared command utilities and factories

Execution Flow

When you run a command like gh issue list --limit 5:

  1. The main() function in cmd/gh/main.go initializes the root command
  2. The root command dispatcher routes to the appropriate subcommand
  3. Command-specific logic executes (e.g., pkg/cmd/issue/list/list.go)
  4. Results are written to stdout/stderr via IO streams
  5. Exit codes indicate success or failure

Key Components

Command Structure: Commands follow the pattern pkg/cmd/<command>/<subcommand>/<subcommand>.go. Help text is embedded directly in command source code and auto-converted to manual pages.

API Layer: The api/ package handles GitHub GraphQL queries and REST API interactions. Query builders construct dynamic GraphQL requests for different data types (PRs, issues, repos, etc.).

Configuration & Auth: The internal/config/ package manages user settings and authentication tokens. The internal/authflow/ handles OAuth login flows.

Error Handling: Exit codes distinguish between different failure modes:

  • 0 = Success
  • 1 = General error
  • 2 = User cancellation
  • 4 = Authentication error
  • 8 = Pending operation

Extension System

gh supports extensions—external commands that integrate seamlessly with the CLI. Extensions are discovered and executed as subcommands, allowing community contributions without modifying core code.

Development Workflow

Loading diagram...

Testing Strategy

Tests use mocking and stubbing to avoid real API calls, filesystem operations, or git commands. Table-driven tests verify command behavior across different inputs. The pkg/httpmock/ package provides HTTP mocking for API testing.

Architecture & Command System

Relevant Files
  • pkg/cmd/root/root.go
  • pkg/cmd/factory/default.go
  • pkg/cmdutil/factory.go
  • internal/ghcmd/cmd.go

The GitHub CLI is built on a hierarchical command architecture powered by the Cobra framework. Understanding how commands are structured and initialized is essential for extending the CLI.

Command Hierarchy

The CLI uses a tree-based command structure with three levels:

  1. Root Command (gh) - The main entry point defined in pkg/cmd/root/root.go
  2. Top-level Commands (gh issue, gh pr, gh repo) - Organized by domain
  3. Subcommands (gh issue create, gh pr list) - Specific actions within a domain

Commands are registered in NewCmdRoot() and organized into logical groups: core commands, GitHub Actions commands, and extension commands.

The Factory Pattern

The cmdutil.Factory struct (in pkg/cmdutil/factory.go) is the dependency injection container for all commands. It provides:

  • IOStreams - Standard input/output/error handling
  • HttpClient - Authenticated HTTP requests to GitHub API
  • GitClient - Git operations and repository detection
  • Config - User configuration and authentication state
  • BaseRepo - Repository resolution logic
  • Prompter - Interactive user prompts
  • ExtensionManager - Third-party command management

The factory is initialized in pkg/cmd/factory/default.go with careful dependency ordering. Each field is lazily initialized as a function, allowing commands to request only what they need.

Repository Resolution

Two repository resolution strategies exist:

  • BaseRepoFunc - Selects the first remote in priority order: upstream > github > origin
  • SmartBaseRepoFunc - Uses the GitHub API to resolve repository networks and respects user-configured defaults

Commands requiring intelligent repository detection (PR, issue, repo operations) use SmartBaseRepoFunc, while simpler commands use the basic resolver.

Command Initialization Flow

Loading diagram...

Adding New Commands

To add a new command:

  1. Create a package at pkg/cmd/newcommand/
  2. Implement NewCmdNewCommand(f *cmdutil.Factory) *cobra.Command
  3. Register in NewCmdRoot() by calling cmd.AddCommand(newcommandCmd.NewCmdNewCommand(f))
  4. For commands needing repository context, use the repoResolvingCmdFactory variant

Commands should encapsulate their logic within their package and avoid polluting global packages like api or utils.

Error Handling

The CLI defines specific exit codes in internal/ghcmd/cmd.go:

  • 0 - Success
  • 1 - General error
  • 2 - User cancellation
  • 4 - Authentication failure
  • 8 - Pending operation

Authentication checks are enforced via PersistentPreRunE in the root command, with exceptions for commands that don't require authentication (e.g., auth login, version).

GitHub API Integration

Relevant Files
  • api/client.go
  • api/http_client.go
  • api/query_builder.go
  • api/queries_pr.go
  • api/queries_issue.go

The GitHub API integration layer provides a unified interface for communicating with GitHub's GraphQL and REST APIs. It abstracts authentication, error handling, and query construction into reusable components.

Core Architecture

The layer consists of three main components:

  1. HTTP Client (http_client.go) - Manages HTTP transport, authentication tokens, and caching
  2. API Client (client.go) - Wraps GraphQL and REST operations with error handling
  3. Query Builders (query_builder.go, queries_*.go) - Constructs type-safe GraphQL queries
Loading diagram...

HTTP Client Setup

The NewHTTPClient function initializes an HTTP client with:

  • Authentication token injection via AddAuthTokenHeader
  • Optional response caching with configurable TTL
  • Debug logging support
  • Custom User-Agent headers
client, err := NewHTTPClient(HTTPClientOptions{
    AppVersion:     "2.0.0",
    EnableCache:    true,
    CacheTTL:       5 * time.Minute,
    Config:         configProvider,
})

GraphQL Operations

The Client type provides three GraphQL methods:

  • Query() - Execute a named query with struct-based type safety
  • Mutate() - Execute mutations for write operations
  • GraphQL() - Execute raw GraphQL strings with variables

All methods return GraphQLError on API errors while partially populating response data.

Query Building

The query_builder.go file defines reusable GraphQL fragments for common entities:

  • Pull Requests - PullRequestGraphQL() constructs PR field selections
  • Issues - IssueGraphQL() constructs issue field selections
  • Repositories - RepositoryGraphQL() constructs repo field selections

These builders handle complex nested fields like reviews, commits, and status checks:

fields := []string{"title", "reviews", "statusCheckRollup"}
fragment := PullRequestGraphQL(fields)

Error Handling

The layer provides structured error types:

  • HTTPError - REST API errors with OAuth scope suggestions
  • GraphQLError - GraphQL response errors

The ScopesSuggestion() method helps users understand missing OAuth permissions.

REST API Support

For operations not available in GraphQL, the REST() method provides direct access:

err := client.REST(hostname, "POST", "repos/owner/repo/pulls", body, &result)

The RESTWithNext() variant handles pagination via Link headers.

Authentication & Configuration

Relevant Files
  • pkg/cmd/auth/login/login.go
  • internal/authflow/flow.go
  • internal/config/config.go
  • pkg/cmd/auth/shared/login_flow.go
  • internal/keyring/keyring.go
  • pkg/cmd/auth/shared/oauth_scopes.go

Overview

GitHub CLI authentication is built on a multi-layered system that supports OAuth flows, personal access tokens, environment variables, and secure credential storage. The system handles multiple GitHub hosts (including Enterprise instances) and supports switching between authenticated users on the same host.

Authentication Flow

Loading diagram...

Key Components

Login Command (pkg/cmd/auth/login/login.go)

  • Entry point for authentication with flags for hostname, scopes, git protocol, and storage options
  • Supports three authentication modes: interactive browser flow, token input, or environment variables
  • Validates tokens have minimum required scopes: repo, read:org, and gist

OAuth Flow (internal/authflow/flow.go)

  • Implements GitHub's OAuth device flow using the embedded GitHub CLI OAuth app
  • Handles browser opening, clipboard code copying, and success page rendering
  • Retrieves authenticated user login via GraphQL query

Configuration System (internal/config/config.go)

  • Manages host-specific and global settings through YAML config files
  • Supports per-user token storage for multi-account scenarios
  • Provides AuthConfig interface for token resolution with fallback chain: environment variables > keyring > plain text config

Credential Storage (internal/keyring/keyring.go)

  • Wraps system keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Implements 3-second timeout to prevent hangs on unresponsive keyring services
  • Falls back to plain text storage if keyring is unavailable or times out

Scope Validation

Tokens are validated against minimum required scopes using the X-Oauth-Scopes header from GitHub API responses. The validation logic (pkg/cmd/auth/shared/oauth_scopes.go) checks for:

  • repo scope (required)
  • read:org OR write:org OR admin:org (at least one required)

Additional scopes can be requested during login for specific features like SSH key management (admin:public_key) or git credential helper setup.

Multi-Account Support

The configuration system supports multiple authenticated users per host:

  1. Tokens are stored per-user in the keyring or config file
  2. One user is marked as active for each host
  3. gh auth switch allows switching between users without re-authentication
  4. gh auth logout removes a user and automatically switches to another if available

Configuration Storage

Config files are stored in platform-specific directories:

  • macOS: ~/.config/gh/
  • Linux: ~/.config/gh/
  • Windows: %APPDATA%\GitHub CLI\

The default config includes settings for git protocol, editor, pager, browser, and accessibility options. Host-specific settings override global defaults.

Git Repository Integration

Relevant Files
  • git/client.go
  • git/command.go
  • git/url.go
  • context/remote.go
  • git/objects.go
  • git/errors.go

The Git integration layer provides a high-level abstraction over Git operations, enabling the CLI to interact with repositories programmatically. The Client struct serves as the main entry point, wrapping Git command execution with context awareness, authentication, and error handling.

Core Architecture

Loading diagram...

Client Structure

The Client struct manages Git operations with the following key fields:

  • GhPath: Path to the gh executable for credential handling
  • RepoDir: Working directory for Git commands (passed via -C flag)
  • GitPath: Path to the Git executable, resolved lazily on first use
  • Stderr, Stdin, Stdout: I/O streams for command execution
  • commandContext: Allows dependency injection of command creation for testing

Command Execution

Commands are executed through two main methods:

  • Command(): Creates a standard Git command with optional repository context
  • AuthenticatedCommand(): Wraps commands with credential helper configuration, using gh auth git-credential for authentication

The CredentialPattern type ensures type-safe credential helper configuration, preventing accidental credential leakage by requiring explicit patterns (e.g., github.com) rather than matching all hosts.

Remote Management

Remotes are parsed from git remote -v output and enriched with resolved URLs via Git config. The Remote struct contains:

  • Name: Remote identifier (e.g., origin, upstream)
  • FetchURL and PushURL: Parsed *url.URL objects
  • Resolved: Optional custom resolution for GitHub Enterprise or URL translation

Remote names are sorted by priority: upstream > github > origin > others.

URL Parsing

The ParseURL() function normalizes Git remote URLs, supporting multiple formats:

  • SSH URLs: [email protected]:owner/repo.git → normalized to ssh:// scheme
  • HTTPS URLs: https://github.com/owner/repo.git
  • Git protocol: git://github.com/owner/repo.git
  • Custom schemes: git+ssh://, git+https://

Branch and Commit Operations

The Client provides methods for repository introspection:

  • CurrentBranch(): Reads the checked-out branch via symbolic-ref
  • Commits(): Retrieves commits between refs using null-byte-delimited format for safe parsing
  • ReadBranchConfig(): Parses branch tracking configuration (remote, merge, push remote, merge base)
  • ShowRefs(): Resolves fully-qualified refs to commit hashes

Error Handling

Git errors are wrapped in a GitError type that captures:

  • ExitCode: The Git command's exit code
  • Stderr: Standard error output for diagnostics
  • err: The underlying error

Special cases include ErrNotOnAnyBranch for detached HEAD state and NotInstalled for missing Git executable.

Network Operations

Network-dependent operations use AuthenticatedCommand() to ensure credentials are available:

  • Fetch(): Retrieves updates from remote
  • Pull(): Fetches and merges with --ff-only
  • Push(): Pushes branch with --set-upstream
  • Clone(): Clones repository with target directory inference

All network commands support CommandModifier functions for customizing I/O streams or repository context.

Terminal I/O & Output Formatting

Relevant Files
  • pkg/iostreams/iostreams.go
  • pkg/iostreams/color.go
  • pkg/markdown/markdown.go
  • internal/tableprinter/table_printer.go
  • pkg/jsoncolor/jsoncolor.go

The CLI uses a unified I/O abstraction layer to handle terminal output, color management, and formatted data display. This system adapts output based on terminal capabilities and user preferences.

IOStreams: Core I/O Abstraction

IOStreams is the central hub for all terminal I/O operations. It manages stdin, stdout, and stderr with awareness of terminal capabilities:

type IOStreams struct {
    In     fileReader    // stdin
    Out    fileWriter    // stdout
    ErrOut fileWriter    // stderr
    // ... terminal detection, color, pager, progress tracking
}

Key capabilities:

  • TTY Detection: Methods like IsStdoutTTY(), IsStdinTTY(), IsStderrTTY() detect if output is connected to a terminal
  • Terminal Width: TerminalWidth() returns the terminal width for responsive formatting (default 80 chars)
  • Pager Support: StartPager() and StopPager() pipe output through external pagers (e.g., less)
  • Progress Indicators: StartProgressIndicatorWithLabel() displays spinners or textual progress
  • Alternate Screen Buffer: StartAlternateScreenBuffer() enables full-screen terminal applications

ColorScheme: Intelligent Color Management

ColorScheme adapts colors based on terminal capabilities and user preferences:

type ColorScheme struct {
    Enabled       bool   // Color enabled at all
    EightBitColor bool   // 256-color support
    TrueColor     bool   // 24-bit RGB support
    Accessible    bool   // Use accessible base-16 colors
    ColorLabels   bool   // Color labels with RGB hex values
    Theme         string // "light", "dark", or "none"
}

Methods provide semantic coloring: Bold(), Red(), Green(), Cyan(), Muted(), TableHeader(), and Label() for custom RGB colors. Colors gracefully degrade when disabled.

TablePrinter: Formatted Data Output

TablePrinter renders aligned, colored tables with TTY-aware formatting:

tp := tableprinter.New(ios, tableprinter.WithHeader("ID", "NAME", "STATUS"))
tp.AddField(id, tableprinter.WithColor(cs.Cyan))
tp.AddField(name)
tp.AddTimeField(now, createdAt, cs.Muted)  // Fuzzy time in TTY, RFC3339 otherwise
tp.EndRow()
tp.Render()

In TTY mode, headers are uppercase and underlined. In non-TTY mode, output is plain text suitable for scripting.

JSON Colorization

jsoncolor.Write() outputs syntax-highlighted JSON with ANSI escape codes:

if ios.ColorEnabled() {
    jsoncolor.Write(ios.Out, jsonReader, "  ")  // indent for pretty-printing
}

Colors: delimiters (bright white), keys (bright blue), strings (green), booleans (yellow), null (cyan).

Markdown Rendering

markdown.Render() converts Markdown to terminal-friendly output using Glamour:

rendered, err := markdown.Render(text, 
    markdown.WithTheme(ios.TerminalTheme()),
    markdown.WithWrap(ios.TerminalWidth()),
)

Respects GLAMOUR_STYLE environment variable and terminal theme detection.

Platform-Specific Handling

On Windows without virtual terminal support, colorable translates ANSI escape sequences to console syscalls. The system preserves original file descriptors while wrapping writers for compatibility.

Usage Pattern

Commands typically follow this pattern:

ios := iostreams.System()  // Detect terminal capabilities
cs := ios.ColorScheme()    // Get color scheme
if ios.IsStdoutTTY() {
    // Interactive output: colors, tables, pagers
} else {
    // Scripting output: plain text, JSON
}

Extension System & Plugins

Relevant Files
  • pkg/cmd/extension/manager.go
  • pkg/cmd/extension/extension.go
  • pkg/extensions/extension.go
  • pkg/cmd/extension/command.go

The extension system allows users to create and install custom commands that extend GitHub CLI functionality. Extensions are standalone repositories prefixed with gh- that provide additional executable commands.

Architecture Overview

Loading diagram...

Extension Types

Extensions come in three varieties:

  1. Binary Extensions - Precompiled binaries distributed via GitHub releases. The manager detects platform-specific assets (e.g., linux-amd64, darwin-arm64) and downloads the appropriate binary. A manifest.yml file tracks metadata including owner, repository, tag, and pin status.

  2. Git Extensions - Script-based extensions cloned directly from a repository. These contain executable scripts (Bash, Python, Ruby, etc.) in the repository root. Version tracking uses git commit SHAs and remote HEAD references.

  3. Local Extensions - Development extensions installed from a local directory via symlink (or file path on Windows). Useful during development; cannot be upgraded automatically.

Manager Operations

The Manager type orchestrates all extension lifecycle operations:

  • Install - Clones or downloads an extension, optionally pinning to a specific version (tag for binaries, commit SHA for git extensions)
  • List - Enumerates installed extensions by scanning the extensions directory for gh-* prefixed entries
  • Upgrade - Updates extensions to latest versions, with special handling for pinned and local extensions
  • Remove - Deletes extension directories and cleans up update metadata
  • Dispatch - Executes an extension by name, forwarding all arguments to the executable
  • Create - Scaffolds new extensions with templates (script, Go binary, or other precompiled)

Extension Interface

The Extension interface provides a unified API for querying extension metadata:

  • Name() - Extension name without gh- prefix
  • Path() - Filesystem path to the executable
  • URL() - Repository URL (parsed from manifest or git remote)
  • CurrentVersion() - Installed version (tag for binary, commit SHA for git)
  • LatestVersion() - Latest available version (fetched from releases or remote HEAD)
  • IsPinned() - Whether extension is locked to a specific version
  • UpdateAvailable() - Boolean indicating if an upgrade is possible
  • IsBinary() / IsLocal() - Type predicates

Version Management

Binary extensions store metadata in manifest.yml (owner, name, host, tag, pin status). Git extensions track versions via git commit SHAs and use .pin-{SHA} marker files to indicate pinned versions. The manager checks for updates once every 24 hours by default and displays upgrade notices to users.

Platform Support

The system supports 40+ platform combinations (Linux, macOS, Windows, BSD, etc.) across multiple architectures. On ARM-based Macs, the manager falls back to amd64 binaries with Rosetta 2 emulation if native ARM binaries are unavailable. Windows extensions are dispatched through sh.exe to support shebang lines.

Core Command Implementations

Relevant Files
  • pkg/cmd/pr/pr.go
  • pkg/cmd/issue/issue.go
  • pkg/cmd/repo/repo.go
  • pkg/cmd/api/api.go
  • pkg/cmdutil/factory.go
  • pkg/cmd/factory/default.go

The CLI is built on a hierarchical command structure using Cobra, with a Factory pattern providing dependency injection. Each major command (PR, Issue, Repo, API) is a parent command that groups related subcommands.

Command Hierarchy

Commands follow a two-level structure: parent commands with grouped subcommands. For example, gh pr is the parent with subcommands like create, list, view, and merge. The cmdutil.AddGroup() helper organizes subcommands into logical groups displayed in help text.

cmdutil.AddGroup(cmd, "General commands",
    cmdList.NewCmdList(f, nil),
    cmdCreate.NewCmdCreate(f, nil),
)
cmdutil.AddGroup(cmd, "Targeted commands",
    cmdView.NewCmdView(f, nil),
    cmdMerge.NewCmdMerge(f, nil),
)

The Factory Pattern

The cmdutil.Factory is the central dependency injection container. It provides:

  • IO Streams – stdin, stdout, stderr with TTY detection
  • HTTP Client – authenticated API requests with caching
  • Git Client – local repository operations
  • Config – user authentication and settings
  • BaseRepo – current repository resolution
  • Remotes – git remote discovery and filtering

The factory is initialized once in pkg/cmd/factory/default.go with lazy-loaded dependencies. Commands receive the factory and call its methods to access services.

Repository Resolution

Two strategies exist for determining the base repository:

  1. BaseRepoFunc – Returns the first remote in priority order (upstream, github, origin)
  2. SmartBaseRepoFunc – Uses the GitHub API to resolve repository networks and respects user-configured defaults via gh repo set-default

Commands requiring intelligent repo resolution use SmartBaseRepoFunc, while simpler commands use the basic resolver.

Core Commands

PR Command manages pull requests with subcommands for create, list, view, checkout, merge, review, and more. Supports both number and URL arguments.

Issue Command handles GitHub issues with similar structure: create, list, view, comment, close, edit, and lifecycle operations.

Repo Command operates on repositories: clone, fork, create, view, sync, and configuration. Includes specialized subcommands for deploy keys, licenses, and gitignore templates.

API Command makes authenticated GraphQL and REST requests. Supports field interpolation, pagination, response filtering, and custom headers. Enables direct API access without CLI abstractions.

Command Options Pattern

Each command defines an Options struct capturing user input and dependencies. The NewCmd* function creates the Cobra command and binds flags to the options struct. A run function executes the command logic using the populated options.

type Options struct {
    IO     *iostreams.IOStreams
    Config func() (gh.Config, error)
    // ... command-specific fields
}

func NewCmdExample(f *cmdutil.Factory) *cobra.Command {
    opts := &Options{}
    cmd := &cobra.Command{
        RunE: func(cmd *cobra.Command, args []string) error {
            return exampleRun(opts)
        },
    }
    // Bind flags to opts fields
    return cmd
}

Error Handling

Commands use cmdutil.FlagErrorf() for flag validation errors and return standard Go errors for runtime failures. The factory's IOStreams provides color-aware output for error messages in TTY environments.