CLI tool
nao1215/gup avatar
nao1215/gup

gup: managing the Go binaries that `go install` forgets

Fast manager for Go-installed binaries in $GOBIN: update, export/import, and migrate toolsets across machines

601 stars28 forksGoApache-2.0

At a glance

What is it?
gup is an Apache-2.0 CLI that adds update, pin, list, remove, export/import and migrate commands on top of the binaries sitting in $GOBIN. It solves a real gap in the Go toolchain, but it only manages tools that were installed with a module path, and it still shells out to the go command.
Who is it for?
Adopt gup if you install Go CLI tools with `go install` and want one command to refresh the whole set, or if you rebuild machines often enough that `gup export` and `gup import` are worth the habit. Do not adopt it if your tools come from Homebrew, apt, asdf or a language-agnostic version manager, because gup only sees binaries under $GOBIN and only re-installs ones it can trace back to a module path; it cannot update a binary whose origin it cannot recover.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The gap gup fills in the Go toolchain

`go install` writes a program into `$GOBIN` (or `$GOPATH/bin`) and then stops paying attention. There is no record of what was installed, no command to refresh it, and no supported way to hold a tool at a version you depend on. The README states this plainly: `go install` "never updates it again, keeps no manifest of what it installed, and offers no way to hold a tool at a version you depend on." That is not a bug in Go, it is a deliberate scope decision, but it leaves anyone with a dozen linters, formatters and code generators in their bin directory doing the work by hand.

The audience is narrow and identifiable. If you install one Go tool a year, you do not need gup. If your `$GOBIN` holds the output of a dotfiles bootstrap, or you set up new machines regularly, or you maintain a shared toolset across a team, the manual loop is the thing gup replaces. The project's own topic list includes `dotfiles`, which is a fair signal about who it is for. It is a tool for people who treat their compiled binaries as configuration.

What gup actually does when you run update

The core command is `gup update`, which the README says "updates every binary under `$GOBIN`, in parallel." The parallelism is the interesting part, because each update is a separate `go install` invocation, and those are network-bound and slow. Running them concurrently is the difference between a two-minute wait and a twenty-second one.

To do this, gup has to answer a question Go does not: which module path produced this binary? The README's sample output shows the recovered identity in the progress line, for example `github.com/nao1215/gup (v0.7.0 to v0.7.1, go1.20.1 to go1.22.4)`. The version pair is the before and after, and the toolchain pair is the Go version the binary was built with against the one it will be built with now. That second pair is easy to overlook and matters: a `gup update` can silently move binaries from an older Go to the current one, which is a runtime change, not just a version bump.

When a tool is current, the output says so rather than reinstalling: `Already up-to-date: v1.0.2 / go1.22.4`. You can also narrow the scope by naming tools, as in `gup update subaru gup ubume`, which is useful when one binary is broken and you do not want to churn the rest. The README also references excluding binaries during updates, though the excerpt cuts off before the flag or config key is given, so check the documentation site for the exact mechanism.

The management commands go install never had

Update is the headline, but the surrounding commands are what make gup a toolset manager rather than a batch script. `gup list` and `gup check` report what is installed and what is behind. `gup remove` deletes binaries. `gup pin` holds selected tools at exact versions, which is the answer to the missing version-lock problem described in the README.

The two commands worth the most attention are `gup export` and `gup import`. Export writes out the set of installed tools so it can be reproduced on another machine; import reads that back. This is the mechanism that turns a hand-built `$GOBIN` into something closer to a lockfile, and it is the reason gup shows up in dotfiles-oriented workflows. `gup migrate` does the related job of moving the set to a new `$GOBIN`, which is what you want when you change `GOPATH` or reorganise where your binaries live.

One design consequence deserves stating. Because pinning exists, the export is not purely descriptive. A pinned tool will be reproduced at the pinned version, which is what you want for reproducibility and not what you want if you were expecting export to be a snapshot that tracks latest. The README does not spell out the interaction between pin state and export contents, so verify it against the documentation before you rely on the file as a lockfile.

Installing gup without touching Go

The obvious install is `go install github.com/nao1215/gup@latest`, and building from source needs Go 1.25 or newer per `go.mod`. But the README is explicit that gup "uses the go command internally, so the golang installation is required" regardless of how you install gup itself. The prebuilt binaries remove the build-time Go requirement, not the runtime one. That is a constraint worth internalising: gup is a front end for the go command, not a replacement for it.

If you would rather not build, the packaging coverage is unusually broad for a tool this size. It is in homebrew-core, so `brew install gup` needs no tap, with `brew install nao1215/tap/gup` still published as a GoReleaser-built alternative that installs the prebuilt binary instead of compiling. Windows users have `winget install --id nao1215.gup`. There is a mise entry (`mise use -g gup@latest`), a nixpkgs package (`nix profile install nixpkgs#gogup`), an aqua registry entry (`aqua g -i nao1215/gup`), and two AUR packages, `gup` for a source build and `gup-bin` for the release binary. Release-page packages cover .deb, .rpm and .apk for amd64 and arm64, plus tarballs and zips, and the packages also install bash, fish and zsh completions.

The macOS constraint is the sharp edge. Release binaries are built with the latest Go 1.27 patch, and because Go 1.27 dropped macOS 12 and earlier, those binaries require macOS 13 Ventura or newer. On an older macOS you must build from source with a Go that still supports it. The README is upfront about this, which is better than most projects manage.

Where gup stops being the right tool

gup can only update what it can identify. If a binary in `$GOBIN` was not produced by `go install` from a module path, or if the module path cannot be recovered from the binary, gup has nothing to re-install. The README's framing is that it manages the global Go command-line tools in `$GOBIN`, and that boundary is real. A vendored binary copied in by hand, a tool built from a local checkout, or anything installed via a different package manager is outside the model.

The second limitation is the dependency on the go command at runtime. Every update is a `go install`, which means network access to the module proxy, and it means gup inherits the proxy's behaviour and the toolchain's resolution rules. An air-gapped machine with a populated `$GOBIN` can list and remove, but updating is not going to work there without a module proxy arrangement that gup does not provide.

The third is the toolchain drift mentioned earlier. Because update rebuilds with whatever Go is current, a routine `gup update` can change the Go runtime under your binaries. For a formatter that is fine. For a binary whose behaviour depends on runtime version, it is a change you did not explicitly ask for, and the README's output format is the only place it surfaces.

Finally, the README excerpt does not document rollback. If an update breaks a tool, there is pinning to prevent the next update, but recovering the previous binary is not described. Treat that as unverified rather than absent, and check the documentation site.

How this differs from a general version manager

The natural comparison is a language-agnostic version manager such as mise or asdf, both of which gup is distributed through. The difference in approach is the direction of knowledge. A general version manager starts from a manifest you write: you declare the tools and versions, and it installs them. gup starts from what is already on disk: it reads the binaries in `$GOBIN`, works out what they are, and manages that set. One is declarative-first, the other discovery-first.

That distinction decides which tool fits. If you want a single manifest covering Node, Python and your Go tools, a general manager is the better fit, and gup's own README lists mise and aqua as install channels, which suggests the author sees them as complements rather than rivals. If your problem is specifically that `go install` left you with an unmanaged pile of binaries and you want to bring that pile under control without rewriting your bootstrap around a new manifest format, gup's discovery model is the shorter path. It also means gup works on a machine you inherited, where no manifest exists to declare.

The cost of the discovery model is that it is only as good as what it can read. A manifest-based manager knows what you asked for. gup knows what it can infer.

Maintenance, licensing and the cost of staying current

The repository is not archived, and the last push was on 2026-09-14, with v1.9.2 released on 2026-09-12 and v1.9.0 and v1.9.1 both on 2026-08-31. Releases are frequent and recent. The project is licensed Apache-2.0, which permits commercial use, modification and redistribution provided you keep the licence and notices and state significant changes; it also includes an explicit patent grant. That is a permissive licence with a patent clause, which is generally the least awkward category for corporate adoption. This is a description of the licence text, not legal advice, and if you are redistributing gup in a product you should have your own counsel read it.

The upgrade cost is low by design. gup is a single binary with no daemon and no config file that the README describes, so upgrading it is replacing the binary through whichever channel you installed from. The more meaningful maintenance question is the one gup creates for you: once you pin tools, you own the decision about when to unpin, and once you export a toolset, you own keeping that file current. The tool reduces manual work but does not eliminate the judgement about which versions you depend on.

One maintenance detail is worth noting because it is unusual. Releases ship signed checksums via cosign keyless signing, an SPDX SBOM per archive, and SLSA build provenance attested through GitHub OIDC, with verification commands documented for `cosign verify-blob` and `gh attestation verify`. For a tool that installs other binaries, having a documented path to verify what you downloaded is a meaningful property, and it is more than most projects at this scale provide.

Editorial conclusion

Adopt gup if you install Go CLI tools with `go install` and want one command to refresh the whole set, or if you rebuild machines often enough that `gup export` and `gup import` are worth the habit. Do not adopt it if your tools come from Homebrew, apt, asdf or a language-agnostic version manager, because gup only sees binaries under $GOBIN and only re-installs ones it can trace back to a module path; it cannot update a binary whose origin it cannot recover. Before rolling it out, run `gup list` on a machine that already has a populated $GOBIN and confirm every tool you care about shows a module path, then run `gup check` to see which ones are behind. The whole value proposition collapses to that one check.

Official sources

  1. License: Apache-2.0
  2. nao1215/gup on GitHub
  3. Project website
  4. README
  5. Releases
Community notes

Community notes