Go Modules skill for Codex
Clean go.mod, honest versions, no laptop paths. Codex treats go.mod as a release contract: it explains minimal version selection before upgrading, keeps replace directives out of libraries, handles /v2 path moves and retract lines instead of re-tagging, and wires private modules through GOPRIVATE and git auth without touching tokens. A bundled checker flags local replaces and tag/path major mismatches before you tag.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- go-modules Installs to ~/.agents/skills/go-modules/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install go-modules The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project go-modules Installs to .agents/skills/go-modules/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/go-modules.zipunzip -o go-modules.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) go-modules PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $go-modules, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Manages Go modules end to end - go.mod and go.sum hygiene, go get upgrades and downgrades, major-version /v2 paths, replace and retract directives, go.work multi-module workspaces, tool dependencies, and private modules via GOPRIVATE, GOPROXY and git auth. Use when go build fails with missing go.sum entries or ambiguous imports, when bumping or pinning a dependency, releasing a v2, wiring a monorepo with go.work, or fetching modules from a private GitHub or GitLab org.
Use it when
- go build or go test fails on module resolution, go.sum or import path errors
- You are bumping, pinning or removing a Go dependency
- A Go library is about to be tagged, especially a new major version
- Several Go modules in one repo need to be developed together
- Modules live in a private GitHub, GitLab or internal proxy
Use something else when
- The question is package layout, naming or error style inside the code: use go-idioms
- You are tuning a CI pipeline rather than the module graph: use github-actions-workflows
How to check the result
go mod tidy -diff prints nothing, go build and go test pass with -mod=readonly, and scripts/check_gomod.py reports release-ready before any tag is created.
The mistake it prevents
Shipping a library whose go.mod relies on a replace directive; every consumer ignores it and gets a different, often broken, dependency.
Done well means: go mod tidy -diff prints nothing, go build ./... and go test ./...
pass with GOFLAGS=-mod=readonly, every version in go.mod is a tagged release or a
deliberate pseudo-version, and nothing in go.mod points at a path on someone’s laptop.
Before changing anything
cat go.mod; ls go.work go.work.sum 2>/dev/null # workspace mode changes resolution
go env GOVERSION GOTOOLCHAIN GOPROXY GOPRIVATE GOFLAGS GOWORK
go list -m # main module(s)
git tag --list 'v*' --sort=-v:refname | head # released versions, if this is a library
Read AGENTS.md for the project’s Go version policy and release process. Module downloads
need network; in a Codex sandbox with network off, go build can still work from the
module cache (go env GOMODCACHE) or a vendor/ directory, so check those before asking
for approval. Never set GOSUMDB=off or GOINSECURE just to get past a checksum or TLS
failure; find the cause.
How resolution works (the part people get wrong)
- Go uses minimal version selection: the build gets the highest version any requirement asks for, not the newest available. Upgrading one dependency can raise others.
- The
goline is a minimum language and toolchain version (Go 1.21+). If the local toolchain is older,GOTOOLCHAIN=autodownloads a newer one (network). The optionaltoolchainline is only a preference for the main module. replaceandexcludeapply only in the main module. A library’s replaces are ignored by everyone who imports it. Never ship a library that only builds because of a replace.go.sumholds hashes for every module version the build may load. Commit it. It is not a lockfile of chosen versions; go.mod is.
Common tasks
| Goal | Command |
|---|---|
| Add or upgrade one dependency | go get example.com/[email protected] (or @latest, @patch) |
| Upgrade direct and indirect deps of a package | go get -u ./... then go mod tidy |
| Patch-level upgrades only | go get -u=patch ./... |
| Downgrade | go get example.com/[email protected] (may downgrade dependents too) |
| Remove a requirement | go get example.com/lib@none |
| Pin to an unreleased commit | go get example.com/lib@<commit-sha> (records a pseudo-version) |
| List available updates | go list -m -u all |
| See why a module is needed | go mod why -m example.com/lib and go mod graph | grep lib |
| List versions of a module | go list -m -versions example.com/lib |
| Check go.mod/go.sum without writing | go mod tidy -diff (Go 1.23+; non-zero exit if dirty) |
| Verify cache integrity | go mod verify |
| Track a dev tool (Go 1.24+) | go get -tool golang.org/x/tools/cmd/stringer@latest, run with go tool stringer |
| Upgrade toolchain line | go get [email protected] toolchain@none (raises the minimum for all users) |
After any change: go mod tidy && go build ./... && go test ./.... Review the go.mod and
go.sum diff; an upgrade that pulled in unrelated major bumps deserves a second look.
Raising the go line is a breaking change for library consumers on older toolchains. Do
it on purpose, never as a side effect of go mod tidy run with a newer local Go.
Fixing common errors
| Error | Cause | Fix |
|---|---|---|
missing go.sum entry for module providing package | go.sum incomplete | go mod tidy (or go mod download <mod>) |
ambiguous import: found package X in multiple modules | A package moved between a module and its submodule | Upgrade both to versions where the split is complete; go get mod@latest submod@latest |
module declares its path as A but was required as B | Fork or renamed module | replace B => fork vX in the app only, or fix the import path |
invalid version: module contains a go.mod file, so module path must match major version | Tagged v2+ without /v2 in module path | See major versions below |
410 Gone / verifying module: checksum mismatch | Tag moved or private module checked against sum.golang.org | Never retag; set GOPRIVATE for private paths |
go: updates to go.mod needed; to update it: go mod tidy | Running with -mod=readonly (default) after edits | Run tidy locally and commit; CI should fail, not fix |
go: downloading go1.x then failure | GOTOOLCHAIN=auto with no network | Install the toolchain or ask for network approval |
Major versions and releasing a library
Semantic import versioning: v0 and v1 share the base path; v2+ must change it.
# Major-branch layout (most common): same repo, path gets /v2
go mod edit -module github.com/acme/widget/v2
# rewrite imports inside the module
grep -rl 'github.com/acme/widget' --include='*.go' . | xargs sed -i '' 's#github.com/acme/widget\([/"]\)#github.com/acme/widget/v2\1#g'
go build ./... && go test ./...
git tag v2.0.0 # tag only after user confirmation; pushing tags is public and permanent
(sed -i '' is BSD/macOS; use sed -i on GNU.) Check the rewrite did not double-apply
(/v2/v2).
Release rules:
-
Tags are immutable. The proxy and checksum DB cache them forever. A bad release gets a new version plus a
retractline, never a re-tag:retract ( v1.4.0 // Panics on empty config; use v1.4.1. [v1.5.0, v1.5.2] // Wrong module path in generated code. )Publish the retraction in a newer version (for example v1.5.3) so
go list -m -usees it. -
Submodules in a monorepo are tagged with a prefix:
tools/lint/v0.3.0for modulegithub.com/acme/repo/tools/lint. -
Ask the user before creating or pushing any tag; it publishes the version to the public proxy as soon as someone requests it.
Before tagging, run the bundled checker, which flags local-path replaces, major-version mismatches between tags and the module path, pseudo-version requirements and a committed go.work:
python3 scripts/check_gomod.py path/to/module # exit 1 on errors, 0 when release-ready
Workspaces (go.work)
Use a workspace to develop several modules together without replace directives.
go work init ./api ./worker ./shared
go work use ./tools/lint # add another module
go work sync # push workspace build list back into each go.mod
GOWORK=off go build ./... # build one module exactly as consumers will
| Situation | Choice |
|---|---|
| Local hacking across repos | go.work, not committed (add to .gitignore) |
| Monorepo where all modules always release together | Commit go.work; CI must still run GOWORK=off per module |
| One module, needs a forked dependency | replace in the app’s go.mod, never in a library |
| Want a single version for everything | Consider one module instead of many; multiple modules add release cost |
Each module still needs a correct go.mod on its own. CI that only builds with go.work hides
missing requirements; run GOWORK=off go mod tidy -diff in each module directory.
Private modules
go env -w GOPRIVATE='github.com/acme/*,gitlab.example.com'
# GOPRIVATE sets GONOPROXY and GONOSUMDB defaults: no public proxy, no public checksum DB
Authentication for the underlying git fetch (pick one, documented in AGENTS.md or README):
- HTTPS with a token in
~/.netrc(machine github.com login x-access-token password <token>); ask the user to create it, never write a token yourself. - SSH rewrite:
git config --global url."[email protected]:".insteadOf "https://github.com/". - Go 1.24+ also supports
GOAUTHfor credential helpers; seego help goauth. - Corporate proxy (Athens, Artifactory, GOPROXY-compatible):
GOPROXY=https://goproxy.corp,directand leaveGONOSUMDBcovering private paths.
In CI, set GOPRIVATE in the job env and inject the token as a secret; do not commit
.netrc or tokens into Dockerfiles. For Docker builds use BuildKit secrets
(RUN --mount=type=secret,id=netrc,target=/root/.netrc go mod download).
Vendoring
go mod vendor copies dependencies into vendor/; the go command then uses it automatically
when the go line is 1.14+. Choose it only for hermetic or air-gapped builds. Re-run after
every go.mod change; go build fails with “inconsistent vendoring” otherwise. Workspaces
use go work vendor.
Gotchas
go getin a directory outside any module, or withGOFLAGS=-mod=modin CI, silently rewrites go.mod. CI should use the default readonly mode.go install pkg@versionignores the current module’s go.mod; use it for one-off tools, and thetooldirective for tools the repo pins.// indirectcomments are maintained by tidy; do not hand-edit them.- A pseudo-version like
v0.0.0-20250612093011-4f1c2a9e8b7dis fine during development, but releasing a library that requires one forces it on every consumer. go mod tidyconsiders all build tags and platforms; a dependency used only under//go:build windowsis still kept. That is correct; do not delete it by hand.- Module path case matters, and the proxy escapes uppercase letters (
!acme); mismatched case between import and go.mod causes “case-insensitive import collision”.
Verification
go mod tidy -diff # empty output
go mod verify # all modules verified
GOFLAGS=-mod=readonly go build ./... && go test ./...
govulncheck ./... # if installed; needs network for the vuln DB
If tidy keeps changing files between runs, look for build-tag-specific imports or a
go.work overriding versions (GOWORK=off and re-run).
Report template
**Change:** upgraded github.com/jackc/pgx/v5 v5.6.0 -> v5.7.4
**Side effects:** golang.org/x/crypto v0.31.0 -> v0.36.0 (required by pgx)
**go line:** unchanged (1.23)
**Checks:** go mod tidy -diff clean; go test ./... pass; govulncheck: no findings
**Needs decision:** none / tag v2.0.0 once you confirm What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Go Modules"
short_description: "Clean go.mod, honest versions, no laptop paths"
default_prompt: "$go-modules go build says missing go.sum entry and ambiguous import, sort out our go.mod" scripts/check_gomod.py
#!/usr/bin/env python3
"""Pre-release lint for a Go module directory.
Usage: python3 check_gomod.py [module_dir]
Errors (exit 1):
- replace directive pointing at a local filesystem path
- module path major suffix does not match the highest git tag for this module
- missing module or go directive
Warnings (exit 0):
- requirements pinned to pseudo-versions
- go.work present in the module directory (check it is meant to be committed)
- replace/exclude present (ignored by consumers of a library)
Standard library only; uses `git tag` when the directory is in a git repo.
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
PSEUDO = re.compile(r"v\d+\.\d+\.\d+-(?:\d+\.)?(?:0\.)?\d{14}-[0-9a-f]{12}")
SEMVER_TAG = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
def strip_comment(line: str) -> str:
return line.split("//", 1)[0].strip()
def parse_gomod(text: str):
module = go = None
requires, replaces, excludes = [], [], []
block = None
for raw in text.splitlines():
line = strip_comment(raw)
if not line:
continue
if block:
if line == ")":
block = None
continue
entry = line
kind = block
else:
m = re.match(r"^(\w+)\s*\($", line)
if m:
block = m.group(1)
continue
parts = line.split(None, 1)
kind, entry = parts[0], parts[1] if len(parts) > 1 else ""
if kind == "module":
module = entry.strip('"')
elif kind == "go":
go = entry
elif kind == "require":
requires.append(entry)
elif kind == "replace":
replaces.append(entry)
elif kind == "exclude":
excludes.append(entry)
return module, go, requires, replaces, excludes
def git_tags(mod_dir: Path) -> list[tuple[int, int, int]]:
try:
top = subprocess.run(["git", "-C", str(mod_dir), "rev-parse", "--show-toplevel"],
capture_output=True, text=True, check=True).stdout.strip()
out = subprocess.run(["git", "-C", str(mod_dir), "tag", "--list"],
capture_output=True, text=True, check=True).stdout
except (OSError, subprocess.CalledProcessError):
return []
rel = mod_dir.resolve().relative_to(Path(top).resolve()).as_posix()
tag_prefix = "" if rel == "." else rel + "/"
versions = []
for tag in out.split():
if not tag.startswith(tag_prefix):
continue
m = SEMVER_TAG.match(tag[len(tag_prefix):])
if m:
versions.append(tuple(int(x) for x in m.groups()))
return sorted(versions)
def main() -> int:
mod_dir = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
gomod = mod_dir / "go.mod"
if not gomod.is_file():
print(f"error: {gomod} not found")
return 1
module, go, requires, replaces, excludes = parse_gomod(gomod.read_text())
errors, warnings = [], []
if not module:
errors.append("no module directive")
if not go:
errors.append("no go directive (add one with: go mod edit -go=<version>)")
for r in replaces:
target = r.split("=>", 1)[1].strip() if "=>" in r else ""
if target.startswith(("./", "../", "/")) or re.match(r"^[A-Za-z]:\\", target):
errors.append(f"local-path replace: {r}")
if replaces:
warnings.append(f"{len(replaces)} replace directive(s): consumers of this module ignore them")
if excludes:
warnings.append(f"{len(excludes)} exclude directive(s): consumers of this module ignore them")
for r in requires:
if PSEUDO.search(r):
warnings.append(f"pseudo-version requirement: {r}")
if (mod_dir / "go.work").exists():
warnings.append("go.work present: build each module with GOWORK=off before release")
if module:
m = re.search(r"/v(\d+)$", module)
path_major = int(m.group(1)) if m else 1
tags = git_tags(mod_dir)
if tags:
tag_major = max(1, tags[-1][0])
if path_major == tag_major + 1:
warnings.append(f"module path is v{path_major} but no v{path_major} tag yet "
"(expected while preparing a new major release)")
elif tag_major != path_major:
errors.append(
f"highest tag is v{'.'.join(map(str, tags[-1]))} but module path "
f"'{module}' implies major v{path_major}")
print(f"latest tag: v{'.'.join(map(str, tags[-1]))}")
print(f"module: {module} go: {go} requires: {len(requires)}")
for w in warnings:
print(f"warn: {w}")
for e in errors:
print(f"error: {e}")
print("result:", "NOT release-ready" if errors else "release-ready")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.