How do gh extensions work and why create one?
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)
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 withoutgh-. ghregisters installed extensions as commands.- When you run
gh stack view,ghdispatches 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:
- A shell script
- A Python/Ruby/Node/etc script
- A Go/Rust/C++/etc compiled binary
- 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/apigo-gh/pkg/repositorygo-gh/pkg/promptergo-gh/pkg/browsergo-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
| Concern | gh extension | Standalone CLI |
|---|---|---|
| Install | gh extension install owner/gh-foo | You build your own install story |
| Invocation | gh foo ... | foo ... |
| Auth | Can reuse gh auth via go-gh/gh conventions | You handle auth yourself |
| Repo context | Can use go-gh repository helpers | You implement it |
| Updates | gh extension upgrade | You implement/package it |
| Discoverability | GitHub CLI extension ecosystem | Your own docs/distribution |
| Runtime model | External process | External process |
| Control | Some gh conventions | Full control |
| Best for | GitHub workflow tools | General-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