How do gh extensions work and why create one?

HEAD
d3be1b5prune merged branches (#94)
This post might have stale content, as HEAD is 11 commits ahead.

tell me about gh extensions - how do they work? why would i create a gh extension? (in that, what functionality does it give me vs rolling out my custom cli)

Avatar of openai/gpt-5.5
GPT 5.5
commented

A gh extension is basically a user-installed executable that GitHub CLI discovers and dispatches as a gh <name> subcommand.

In this repo, github/gh-stack is one: install with:

gh extension install github/gh-stack

and then run:

gh stack ...

The repo name is gh-stack, but the command becomes gh stack because gh strips the gh- prefix.

##How gh extensions work

At runtime, gh does not load your code as an in-process plugin. It executes your extension as a separate process.

From the GitHub CLI source:

  • An extension has a Name(), Path(), URL(), version info, etc. The name is the extension name without gh-.
  • gh registers installed extensions as commands.
  • When you run gh stack view, gh dispatches to the installed executable and forwards the remaining args.

Conceptually:

gh stack view
   ↓
gh finds installed extension named "stack"
   ↓
execs path/to/gh-stack with args ["view"]

The GitHub CLI code literally builds a Cobra command for each extension, disables flag parsing, and calls the extension manager’s Dispatch. In pkg/cmd/root/extension.go, the run path prepends the extension command name, then dispatches:

args = append([]string{ext.Name()}, args...)
em.Dispatch(args, io.In, io.Out, io.ErrOut)

Then in the extension manager, it finds the executable path and runs it as an external command.

So: a gh extension is just a CLI program with conventions around naming, installation, execution, and upgrades.

##Naming convention

Remote extension repositories are expected to be named like:

gh-foo

They are invoked as:

gh foo

Example from this repo:

github/gh-stack → gh stack

The GitHub CLI source validates this: extension names must start with gh-, and the gh- prefix is trimmed to produce the command name.

##Installation model

gh extension install supports remote and local installs.

###Remote install

gh extension install github/gh-stack

For remote repositories, GitHub CLI first checks if the extension has release artifacts for the current platform. If so, it treats it as a binary extension and downloads the matching prebuilt binary.

If no suitable release artifact is found, it falls back to treating the repo as a script extension and clones the repository, expecting an executable script/binary at the repo root.

The gh source says this explicitly in the install command help:

For remote repositories, the GitHub CLI first looks for the release artifacts assuming that it's a binary extension... In the absence of a release, the repository itself is cloned assuming that it's a script extension...

###Local install

For local development:

gh extension install .

In this repo’s CONTRIBUTING.md, local extension testing is:

go build -o gh-stack .
gh extension remove stack
gh extension install .

Then:

gh stack

will use your locally built gh-stack binary.

For local extensions, gh creates a symlink/equivalent to the local repo. The executable has to exist at the repo root and have the same name as the repository, e.g.:

gh-stack/gh-stack

That matches this repo’s development instructions.

##What an extension actually contains

It can be:

  1. A shell script
  2. A Python/Ruby/Node/etc script
  3. A Go/Rust/C++/etc compiled binary
  4. Anything executable on the user’s machine

This repo is a Go binary. main.go is tiny:

func main() {
    cmd.Execute()
}

Then cmd/root.go defines the actual stack command tree using Cobra:

Use:   "stack <command>",
Short: "Manage stacked branches and pull requests",

and registers subcommands like:

init
add
view
checkout
submit
sync
rebase
push
link
up
down
top
bottom

So gh-stack is not “special” inside gh. It is a standalone Go CLI that gh knows how to install and invoke as gh stack.

##What you get vs shipping your own custom CLI

You can absolutely ship your own binary, e.g.:

stackctl submit

instead of:

gh stack submit

The benefit of a gh extension is not magical runtime capability. The benefit is integration with the GitHub CLI ecosystem.

###1. Built-in distribution and install flow

With a custom CLI, users need instructions like:

brew install your/tap/stackctl
npm install -g stackctl
curl | sh
download this release artifact

With a gh extension:

gh extension install owner/gh-yourtool

That is a very low-friction install path for users who already have gh.

You also get:

gh extension list
gh extension remove yourtool
gh extension upgrade yourtool
gh extension upgrade --all

So gh gives you a package-manager-ish lifecycle.

###2. Familiar command namespace

If your tool is GitHub-related, users already think in terms of:

gh repo ...
gh pr ...
gh issue ...
gh auth ...

An extension fits naturally:

gh stack ...
gh copilot ...
gh some-internal-tool ...

That can be nicer than asking users to remember a new top-level binary name.

###3. GitHub authentication piggybacking

Your extension can use the user’s existing gh auth.

In this repo, gh-stack uses github.com/cli/go-gh/v2/pkg/api to create REST and GraphQL clients:

api.NewGraphQLClient(opts)
api.NewRESTClient(opts)

That means it can rely on the GitHub CLI auth/config environment instead of asking users to configure another token separately.

With a standalone CLI, you often have to answer:

  • Where does the token come from?
  • How does the user log in?
  • How do you support GHES?
  • How do you store credentials?
  • How do you handle multiple accounts/hosts?

A gh extension can lean on gh auth login and the go-gh helpers.

###4. Current repository context

Extensions can use GitHub CLI helpers to infer the current repository.

In internal/config/config.go, this repo uses:

repository.Current()

to determine the current GitHub repo, then creates a GitHub API client for that host/owner/repo.

That is a big ergonomic win for GitHub-centered tools. Users can run:

gh stack submit

inside a repo without passing --repo owner/name every time.

###5. Terminal, browser, prompts, API helpers

Extensions can use GitHub CLI libraries such as:

  • go-gh/pkg/api
  • go-gh/pkg/repository
  • go-gh/pkg/prompter
  • go-gh/pkg/browser
  • go-gh/pkg/term

This repo uses those for things like API access, prompts, terminal detection, and browser opening.

You can do all this in a custom CLI too, but gh extensions make it feel native to users already living in gh.

###6. Discoverability

Extensions can be discovered through the gh-extension topic and GitHub CLI extension browsing/listing flows.

The install help in GitHub CLI points users to:

https://github.com/topics/gh-extension

That gives public extensions a known discovery channel.

###7. Update notifications and upgrades

For installed extensions, gh can check release information and tell users an update is available. It can also upgrade extensions with:

gh extension upgrade stack

or:

gh extension upgrade --all

For binary extensions, gh tracks release tags in a manifest. For git/script extensions, it tracks the git state.

Again: you could build this yourself, but with gh extensions, you inherit a lot of it.

##What you do not get

Important caveat: a gh extension is not a deep plugin API into GitHub CLI internals.

You do not get to call arbitrary internal gh functions in-process.

You do not get automatic integration into every gh subsystem.

You do not get magical permissions beyond whatever the user’s GitHub auth token already has.

At execution time, it is still your process. You are responsible for:

  • parsing your flags
  • rendering output
  • calling GitHub APIs
  • handling errors
  • shipping binaries/scripts
  • maintaining compatibility
  • testing on platforms you support

So the difference is less “plugin SDK” and more “standardized install, dispatch, auth/context conventions, and UX”.

##Why create a gh extension?

Create a gh extension when your CLI is primarily about GitHub workflows.

Good fits:

  • PR/issue/repo workflow automation
  • internal developer tooling around GitHub
  • GitHub API wrappers
  • release/deployment helpers that operate on GitHub state
  • code review helpers
  • repo maintenance tools
  • branch/PR tools like gh-stack

gh-stack is a good example because its entire domain is GitHub pull requests and local git branches. The README describes it as:

A GitHub CLI extension for managing stacked branches and pull requests.

It automates:

  • creating stack branches
  • keeping branches rebased
  • setting correct PR base branches
  • creating/updating PRs
  • navigating stack layers

That belongs naturally under:

gh stack

rather than some unrelated top-level binary.

##When should you just ship a standalone CLI?

Ship a standalone CLI if:

  • the tool is not specifically GitHub-centered
  • users may not have or want gh
  • you need a top-level brand/command
  • you need very custom install/update behavior
  • you need to integrate with many services equally, not primarily GitHub
  • you need more control over telemetry, update checks, packaging, or enterprise distribution
  • the command name would be awkward under gh

For example, a general build system, database tool, formatter, linter, or cloud provider CLI probably should not be a gh extension unless its primary entry point is GitHub workflow automation.

##Practical comparison

Concerngh extensionStandalone CLI
Installgh extension install owner/gh-fooYou build your own install story
Invocationgh foo ...foo ...
AuthCan reuse gh auth via go-gh/gh conventionsYou handle auth yourself
Repo contextCan use go-gh repository helpersYou implement it
Updatesgh extension upgradeYou implement/package it
DiscoverabilityGitHub CLI extension ecosystemYour own docs/distribution
Runtime modelExternal processExternal process
ControlSome gh conventionsFull control
Best forGitHub workflow toolsGeneral-purpose tools

##Rule of thumb

If your tool’s natural sentence is:

“I want to do something with GitHub from the terminal”

make it a gh extension.

If its natural sentence is:

“I want a general-purpose CLI that happens to sometimes touch GitHub”

ship a standalone CLI.

For gh-stack, the extension model is a strong fit: it is GitHub PR workflow tooling, it benefits from gh auth and repo context, and users reasonably expect to run it as part of their gh pr/gh repo workflow.


END OF POST

How do gh extensions work and why create one? — github/gh-stack