GitHub CLI grows extension discovery and authoring tools

Since the GitHub CLI 2.0 release opened the door to extensions, the CLI team has been refining both sides of the experience: how users find and install extensions, and how developers build and ship them. The 2.20.0 release added two new discovery commands, the go-gh library hit 1.0 to share CLI internals with extension authors, and the gh-extension-precompile action now automates releasing prebuilt binaries.

Browsing and searching extensions from the terminal

Extension discovery traditionally meant browsing the web or knowing exactly which repository you wanted. The new gh extension browse command (aliased under gh ext) replaces that with a fully interactive terminal UI. Once launched, it loads all published extensions sorted by star count. Arrow keys or j/k move through the list, / focuses a filter box to search by term, and i installs or r removes the highlighted extension. Pressing w opens that extension’s repository page in your default web browser.

A screenshot of a terminal showing the user interface of "gh ext browse". A title and  search box are at the top. The left side of the screen contains a list of extensions and the right side a rendering of their readmes.

A screenshot of "gh ext browse" running in a terminal. At the top, a filter input box is active and a search term has been typed in. The left column shows a filtered list of extensions that match the search term. The right column is rendering an extension's readme.

For scripted scenarios, gh extension search provides a conventional CLI interface. With no arguments, it prints the first 30 extensions by star count; the initial display shows a green checkmark next to ones you already have installed. Any arguments narrow the search, and several flags shape the output further.

A screenshot of the "gh ext search" command after running it in a terminal. The output lists in columnar format a list of extension names and descriptions, as well as a leading checkmark for installed extensions. At the top is a summary of the results.

A screenshot of the

  • --limit fetches more results
  • --owner restricts results to a single author
  • --sort reorders results, e.g., by last updated
  • --license filters by software license
  • --web opens results in your browser
  • --json emits results as JSON

Since the output is pipe-friendly, you can chain commands. This example installs every extension from a specific author:

gh ext search --owner vilmibm | cut -f2 | while read -r extension; do gh ext install $extension; done

A screenshot of a terminal after running

Full usage details appear in gh help ext search.

Reusing CLI internals with go-gh

To encourage more extensions and raise their quality, the CLI team extracted a library, go-gh, that contains much of the GitHub CLI’s own Go code. Since the CLI itself runs on this library, it’s held to production-grade standards. Extension authors get the same HTTP clients, terminal helpers, and formatting utilities the official CLI uses.

A common need is presenting output that looks good in a terminal but also works when piped to another tool. go-gh includes a tableprinter package that handles this automatically in a modest amount of code:

        if len(matches) == 0 {
                fmt.Println("No matching discussion threads found :(")
        }

    // old for loop was here

        isTerminal := term.IsTerminal(os.Stdout)
        tp := tableprinter.New(os.Stdout, isTerminal, 100)

if isTerminal {
                fmt.Printf(
                        "Searching discussions in '%s/%s' for '%s'\n",
                        repo.Owner(), repo.Name(), search)
        }

        fmt.Println()
        for _, d := range matches {
                tp.AddField(d.Title)
                tp.AddField(d.URL)
                tp.EndRow()
        }

        err = tp.Render()
        if err != nil {
                return fmt.Errorf("could not render data: %w", err)
        }

The crucial piece is calling term.IsTerminal(os.Stdout). When true, the extension is being run by a human at an interactive session, so the tableprinter uses a display width, applies color, and formats columns accordingly. When false — a script or a pipe — values are printed raw, with no color. As a result, the command neither produces visually noisy tables in scripts nor strips formatting that helps humans.

A screenshot of running

A screenshot of running "go run . --repo cli/cli actions | cut -f1" in a terminal. The result is a list of discussion thread titles.

The same package set also includes a jq implementation. That means an extension can offer --json and --jq flags and let users process structured output without requiring jq to be installed separately. Building those options into an extension is relatively direct:

func main() {
    jsonFlag := flag.Bool("json", false, "Output JSON")
    jqFlag := flag.String("jq", "", "Process JSON output with a jq expression")

 
isTerminal := term.IsTerminal(os.Stdout)
        if *jsonFlag {
                output, err := json.Marshal(matches)
                if err != nil {
                        return fmt.Errorf("could not serialize JSON: %w", err)
                }

                if *jqFlag != "" {
                        return jq.Evaluate(bytes.NewBuffer(output), os.Stdout, *jqFlag)
                }

                return jsonpretty.Format(os.Stdout, bytes.NewBuffer(output), " ", isTerminal)
        }

A screenshot of running

A screenshot of running "go run . --repo cli/cli --json --jq '.[]|.Title' actions" in a terminal. The output is a list of discussion thread titles.

Releasing compiled extensions automatically

One friction point for authors is that compiled extensions need prebuilt binaries attached to every release before users can install them. The cli/gh-extension-precompile GitHub Action automates that process. For an extension started with gh ext create, the repository already includes its own .github/workflows definition.

name: release
on:
  push:
    tags:
      - "v*"
permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: cli/gh-extension-precompile@v1

A screenshot of running "git tag v1.0.0", "git push origin v1.0.0", and "gh run view" in a terminal. The output shows a tag being created, the tag being pushed, and then the successful result of the extension's release job having run.

Triggering a release is just a matter of pushing a tag with the vX.Y.Z format. The workflow compiles the code and attaches binaries:

A screenshot of running "gh release view" in a terminal for the extension created in the blog post. The output shows a list of assets. The list contains executable files for a variety of platforms and architectures.

By default the action understands how to build Go code for Linux (amd64, 386, arm, arm64), Windows (amd64, 386, arm64), macOS (amd64, arm64), FreeBSD (amd64, 386, arm64), and Android (amd64, arm64). Extensions written in another language can still use this action through a build_script_override configuration that points to a custom build script.

name: release
on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: cli/gh-extension-precompile@v1
        with:
          build_script_override: "script/build.sh"

The custom script must produce executables in a dist directory at the repository root, following the naming convention {os}-{arch}{ext}, where the extension is .exe for Windows and blank elsewhere:

  • dist/gh-my-ext_v1.0.0_darwin-amd64
  • dist/gh-my-ext_v1.0.0_windows-386.exe

Using the same OS/architecture naming as Go — the list is available at go.dev/doc/install/source — ensures the GitHub CLI can find the right asset for any user’s platform, regardless of the language used to write the extension.

Before releasing, authors should confirm the repository is marked with the gh-extension topic via gh repo edit --add-topic gh-extension. Without that marker, the extension will not surface in gh ext browse or gh ext search. Public repositories are generally desirable, although private repositories remain installable by anyone with read access.

What’s next for the extensions system

The GitHub CLI team has a few extensions-related improvements in the pipeline, focused on accessibility, usability, and developer tooling. A more screen-reader-friendly version of the extension browse command is planned, using a single-column interface. Nested extensions are also on the roadmap, which would allow an extension to be invoked as a subcommand of an existing gh command—for example, gh pr my-extension—so third-party tools fit more naturally into the existing command hierarchy. Documentation and flexibility for the gh-extension-precompile action are also slated for improvement.

If you have ideas for other features, the team welcomes feedback via a discussion or an issue in the cli/cli repository.

The takeaway

The goal of the extensions system is to let you build capabilities beyond what the core CLI offers. Whether that means something practical or purely for fun—like making gh run screensavers—you’re encouraged to experiment and share what you create.