README
ΒΆ
gotools.sh
A robust, strategy-driven bash script to manage Go build
tools (like golangci-lint, task, or mockgen) using
Go 1.24+ tool directives.
Why does this exist?
Prior to Go 1.24, managing tool versions meant tracking
them in a dummy tools.go file. With Go 1.24, you can
track tools directly in go.mod. However, installing
tools directly into your project's main go.mod pollutes
your dependency graph.
gotools.sh solves this by automating the creation and
management of a separate tools/ environment. It provides
different "strategies" to isolate tool dependencies,
preventing version conflicts and ensuring your CI pipeline
runs the exact same binaries as your local machine.
β οΈ Things You Should Know
The Risks of Source-Based Tool Installation
Installing tools via go get -tool compiles the binary
locally from source. You must be aware of the following:
- Local Go Version Dependency: The compiled tool's behavior depends on the Go version installed on your machine.
- Dependency Bleed: If you use the
workspacestrategy (a sharedgo.mod), one tool's dependencies can force version changes on another tool. This can result in untested dependency combinations. - Transitive Replacements Ignored: If the tool's
authors used
replacedirectives in their originalgo.mod, those are ignored when compiling via the tools pattern. - Build Time: Compiling from source is slower than downloading a pre-built binary.
Recommendation: For projects with many tools or
complex dependency graphs, use the module strategy
for physical isolation. For most projects, isolated
(the default) strikes the best balance of simplicity and
safety.
Features
- Three Isolation Strategies: Choose between
isolated(default),module(safest), orworkspace(simplest). - Go Version Parity: Tool environments automatically
sync to the Go version defined in your project's root
go.mod. - Seamless Migration: Move between strategies
dynamically with the
migratecommand without losing your pinned versions. - Reproducibility: Commit the
tools/directory to guarantee environment parity across teams and CI. - Self-Update: Update
gotools.shitself with a single command.
Requirements
- Go 1.24 or higher
- Bash
Installation
Option 1: Global install (recommended)
Run the installer to download gotools.sh into your Go
bin directory (GOBIN, GOPATH/bin, or ~/go/bin).
This makes gotools.sh available system-wide, just like
any other Go tool.
Install the latest release:
curl -fsSL \
https://raw.githubusercontent.com/piusalfred/gotools.sh/main/install.sh \
| bash
Install a specific version:
Set the VERSION environment variable to a release tag.
Both v0.2.0 and 0.2.0 are accepted:
curl -fsSL \
https://raw.githubusercontent.com/piusalfred/gotools.sh/main/install.sh \
| VERSION=v0.2.0 bash
When VERSION is omitted or set to latest, the
installer queries the
GitHub Releases API
to resolve the most recent tag. If the API is unreachable
it falls back to the main branch.
Note: Make sure your Go bin directory is in your
PATH. The installer will warn you if it isn't.
Option 2: Per-project vendored script
Download the script directly into your repository and make it executable. This is useful when you want to pin the exact script version alongside your source code.
Latest (from main branch):
curl -fsSL \
https://raw.githubusercontent.com/piusalfred/gotools.sh/main/gotools.sh \
-o gotools.sh
chmod +x gotools.sh
Specific version:
Replace the branch name with a release tag:
curl -fsSL \
https://raw.githubusercontent.com/piusalfred/gotools.sh/v0.2.0/gotools.sh \
-o gotools.sh
chmod +x gotools.sh
Browse all available versions on the releases page.
Strategies
Running ./gotools.sh init creates a .gotools.env
config file. You configure the isolation level using the
--strategy flag.
1. Isolated (--strategy=isolated) β Default
All files live in the tools/ directory, but each tool
gets its own strictly named .mod and .sum file.
tools/
βββ addlicense.mod
βββ addlicense.sum
βββ golangci-lint.mod
βββ golangci-lint.sum
βββ mockgen.mod
βββ mockgen.sum
- Pros: Logical isolation without subdirectories. Lightweight. Each tool's dependencies are fully independent.
- Cons: Can lead to a cluttered directory with many
tools. Uses the
-modfileflag under the hood.
2. Module (--strategy=module) π Safest
Each tool gets its own dedicated subdirectory with a
standard go.mod and go.sum file.
tools/
βββ addlicense/
β βββ go.mod
β βββ go.sum
βββ golangci-lint/
β βββ go.mod
β βββ go.sum
βββ mockgen/
βββ go.mod
βββ go.sum
- Pros: Physical and logical isolation. Zero chance
of dependency conflicts. Behaves exactly like standard
Go modules. This is the method
explicitly recommended
by complex tools like
golangci-lint. - Cons: Heaviest footprint on disk (multiple directories).
- Note on
go.work: Themodulestrategy does not create ago.workfile by default (--work=false). If you opt in with--work=true, each tool subdirectory is added as ausedirective. This is purely for IDE convenience βexecworks without it.
3. Workspace (--strategy=workspace)
All tools are added as tool directives in a single
shared go.mod file.
tools/
βββ go.mod
βββ go.sum
- Pros: Simplest file structure. Only two files to manage.
- Cons: Dependency Bleed. If Tool A and Tool B share a dependency, Go's Minimal Version Selection (MVS) will force them to use the same version. Upgrading Tool A might silently upgrade Tool B's sub-dependencies, potentially breaking it.
Quick Start
# Bootstrap with the default isolated strategy
./gotools.sh init
# Install some tools
./gotools.sh install github.com/google/addlicense
./gotools.sh install golangci-lint \
github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
# Run a tool
./gotools.sh exec addlicense -l mit -c "Your Name" .
./gotools.sh exec golangci-lint run ./...
# List all managed tools
./gotools.sh list
Usage
| Command | Description |
|---|---|
init [flags] |
Bootstrap the project. |
install [name] <pkg> |
Install a new tool. |
exec <name> [args] |
Run a managed tool. |
sync |
Sync state to .gotools.env. |
upgrade <name|all> |
Upgrade tools to @latest. |
list |
List all managed tools with Go version and modfile path. |
info <name> |
Show detailed information about a specific tool. |
remove <name...> |
Remove specific tools. |
migrate <strategy> |
Migrate to a different strategy. |
config [key [value]] |
View or edit config. |
purge |
Remove all tools and config. |
uninstall |
Remove the script itself. |
version |
Print the script version. |
self-update |
Update to the latest release. |
init Flags
| Flag | Default | Description |
|---|---|---|
--strategy= |
isolated |
Strategy to use. |
--dir= |
tools |
Tools directory path. |
--go= |
inherit |
Go version for tools. |
--work= |
false |
Create a go.work file. |
--prefix= |
(auto) | Module path prefix. |
Examples
Bootstrap a project:
# Isolated strategy (default)
./gotools.sh init
# Module strategy (safest)
./gotools.sh init --strategy=module
# Workspace strategy
./gotools.sh init --strategy=workspace
# Custom directory and explicit Go version
./gotools.sh init --strategy=isolated \
--dir=.build-tools --go=1.24
# Opt in to go.work
./gotools.sh init --strategy=module --work=true
# Explicit module prefix override
./gotools.sh init --prefix=github.com/myorg/myrepo
Install tools:
# Inferred name from package path
./gotools.sh install github.com/google/addlicense
# Explicit name
./gotools.sh install task \
github.com/go-task/task/v3/cmd/task
# Pin to a specific version
./gotools.sh install \
github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
Execute a tool:
./gotools.sh exec addlicense -check .
./gotools.sh exec golangci-lint run ./...
Upgrade tools:
# Upgrade all tools to their latest versions
./gotools.sh upgrade all
# Upgrade a single tool
./gotools.sh upgrade addlicense
Sync tool Go versions:
# After updating your root go.mod
./gotools.sh sync
View and edit config:
# Show all config
./gotools.sh config
# Get a single value
./gotools.sh config GOTOOLS_STRATEGY
# Set a value
./gotools.sh config GOTOOLS_STRATEGY module
./gotools.sh config GOTOOLS_MODULE_PREFIX \
github.com/myorg/myrepo
List tools and inspect details:
# List all managed tools (shows Go version and modfile path)
./gotools.sh list
# TOOL STRATEGY GO MODFILE PACKAGE@VERSION
# ---- -------- -- ------- ---------------
# addlicense isolated 1.24 tools/addlicense.mod github.com/google/addlicense@v1.2.0
# golangci-lint isolated 1.24 tools/golangci-lint.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
# Get detailed info about a specific tool
./gotools.sh info addlicense
# Tool: addlicense
# Package: github.com/google/addlicense
# Version: v1.2.0
# Go: 1.24
# Strategy: isolated
# Modfile: tools/addlicense.mod
Version and self-update:
./gotools.sh version
./gotools.sh self-update
Migration
If you started with workspace and want to upgrade to
module for better isolation, the migrate command
handles everything automatically. It reads your current
tools, extracts their pinned versions, wipes the old
structure, and rebuilds it using the new strategy.
# Migrate from any strategy to module
./gotools.sh migrate module
# Migrate to isolated
./gotools.sh migrate isolated
# Migrate to workspace
./gotools.sh migrate workspace
The migration process:
- Reads the current strategy from
.gotools.env. - Extracts the exact list of tools with their pinned versions.
- Cleans up the old tools directory structure
(including
go.workentries). - Updates
.gotools.envwith the new strategy. - Re-installs all tools at their exact previous versions under the new layout.
Configuration
Running init creates a .gotools.env file in the
project root:
GOTOOLS_STRATEGY=isolated
GOTOOLS_DIR=tools
GOTOOLS_GO_VERSION=inherit
GOTOOLS_USE_WORK=false
GOTOOLS_MODULE_PREFIX=
All subsequent commands read this file automatically.
You can edit it by hand, re-run init with different
flags, or use the config command.
| Variable | Description |
|---|---|
GOTOOLS_STRATEGY |
isolated, module, or workspace. |
GOTOOLS_DIR |
Tools directory path. |
GOTOOLS_GO_VERSION |
Go version or inherit. |
GOTOOLS_USE_WORK |
Create a go.work file. |
GOTOOLS_MODULE_PREFIX |
Module path prefix. |
Module Prefix
Every tool managed by gotools.sh lives in its own
go.mod file. The module directive in that file needs
a path. By default, the script reads your project's root
go.mod and uses its module path as the prefix, combined
with the full tools directory path:
| Root module | Dir | Tool | Module directive |
|---|---|---|---|
github.com/user/repo |
tools |
addlicense |
github.com/user/repo/tools/addlicense |
github.com/user/repo |
build/tools |
mockgen |
github.com/user/repo/build/tools/mockgen |
| (none) | tools |
addlicense |
tools/addlicense |
This makes tool modules proper sub-modules of your project β idiomatic and consistent with how multi-module Go repos work.
To override auto-detection, set GOTOOLS_MODULE_PREFIX
explicitly:
./gotools.sh config GOTOOLS_MODULE_PREFIX \
github.com/myorg/myrepo
Or pass --prefix= during init:
./gotools.sh init --prefix=github.com/myorg/myrepo
CI Integration
Commit the generated tool files and .gotools.env to
version control. This guarantees your CI pipeline uses
the exact same tool versions as your local environment.
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Sync tools
run: ./gotools.sh sync
- name: Run linter
run: ./gotools.sh exec golangci-lint run ./...
- name: Check license headers
run: ./gotools.sh exec addlicense -check .
Pre-commit Hook
You can use gotools.sh with pre-commit
to enforce checks before every commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.11.0.1
hooks:
- id: shellcheck
args: ["--severity=warning"]
- repo: local
hooks:
- id: addlicense
name: addlicense
language: system
entry: >-
./gotools.sh exec addlicense
-check -l mit -c "Your Name" .
pass_filenames: false
always_run: true
Cleanup
Remove specific tools:
./gotools.sh remove golangci-lint mockgen
Total purge (interactive, requires typing YES):
Deletes the tools/ directory and .gotools.env
entirely.
./gotools.sh purge
Uninstall gotools.sh itself (interactive):
./gotools.sh uninstall
License
MIT β Copyright (c) 2026 Pius Alfred