go-script
Skill0 starsChanged 3 months agoMITView on GitHub
Write self-contained, portable, testable Go command-line tools — a small Go module with pure-logic/side-effect separation (boundaries as interfaces) that `go test` can drive with fakes, a cobra CLI, and a build/test/vet/gofmt gate. Use as soon as the user wants a Go script, CLI utility, automation tool, or "glue" code (orchestrating git, curl/http, file manipulation, parsing) in Go, even if they never say "module". Also triggers on adding tests to an existing Go tool, making one portable/testable, or porting a shell/Python script to Go.
In this repository7 other files it gives its agents.
- kotlin-scriptSkill
- python-scriptSkill
- rust-scriptSkill
- scripteratopsSkill
- shell-scriptSkill
- swift-scriptSkill
- typescript-scriptSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 79
Every boundary takes a context.Context as its first parameter, and DoWork passes one down.
- Very specific: line 202
Every tool ships *_test.go files in internal/core (same package, white-box).
- Very specific: line 209
Use plain testing (t.Errorf) and reflect.DeepEqual for slices.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Go script
Standard for all Go command-line tools: a small Go module whose logic lives in
a testable internal/core package, driven by a thin cobra CLI in main.go,
verified by the compiler + go test + go vet + gofmt. The goal is a
self-contained tool (one static binary, no runtime to install) whose logic is
trivially testable — the Go counterpart of the python-script, swift-script,
kotlin-script, and typescript-script skills.
Three decisions are already settled. Don't second-guess them, apply them:
- Distribution → a Go module compiled to a single static binary
(
go build) — Go's best-in-class story: no runtime, trivial cross-compile. - Interface → cobra (
github.com/spf13/cobra). - Tests →
go test(the built-intestingpackage), made easy by an architecture that isolates side effects behind interfaces.
This file carries the decisions; the code implementing them is a complete,
gate-passing module in assets/template/. Copy and adapt that module — don't
retype it from here. Then delete what your tool doesn't earn.
1. Module layout
tool/
├── go.mod # module path + the one dependency (cobra), pinned
├── go.sum # integrity-pinned (go mod tidy)
├── main.go # cobra wiring → thin entry point (package main)
├── internal/core/ # pure logic + boundaries → testable package
│ ├── errors.go # typed errors, each with ExitCode()
│ ├── logic.go # pure Transform
│ ├── boundaries.go # CommandRunner / HTTPFetcher / EnvironmentReading
│ ├── config.go # Config + precedence + JSON file read
│ ├── color.go # ColorMode + ShouldColor (pure)
│ ├── log.go # net-verbosity → level; stderr Logger
│ ├── runner.go # os/exec subprocess boundary
│ ├── http.go # net/http boundary
│ ├── work.go # orchestration (DoWork)
│ ├── signals.go # SIGTERM → 143, SIGINT → 130
│ └── *_test.go # go tests, same package (white-box)
└── README.md- Give the module a path-style name (
module example.com/tool), not a bare word — a baremodule tooltripsgo mod tidy. Rename to your real path. - Logic lives in
internal/coresomainstays a thin wiring layer and the tests import the package directly.internal/keeps it unexportable. - cobra is the only dependency. The stdlib gives subprocess (
os/exec), HTTP (net/http), JSON (encoding/json), files, and env for free — reach for a dependency only for a real gain. Pin everything viago.mod/go.sum(go mod tidyrecords the exact versions and hashes).
2. Structure
The structure is designed to make tests trivial: pure logic never touches the outside world directly. Everything that leaves the process (subprocess, network, filesystem, environment) sits behind an interface boundary, injected into the orchestration. Tests pass a fake implementation; production passes the real one.
- Boundaries are interfaces —
CommandRunner,HTTPFetcher,EnvironmentReading. These are the single mock hooks. A fake is a tiny struct with one method. - Pure logic (
Transform) — input → output, no I/O, tested by direct call. - Config is a struct with
jsontags and pointer fields (so "absent" is distinct from "empty"); precedence is one pure function,ResolveConfig. - Orchestration (
DoWork) assembles boundary calls + pure logic: takes acontext.Contextfirst, data positional, behaviour flags last, returns(int, error). It takes the boundaries as interfaces, so a test drives it with no process and no socket. main.gois thin: parse flags, build the real boundaries, callDoWork, map the error to an exit code. Nothing worth testing lives here.
2.1 Context threads the timeouts
Every boundary takes a context.Context as its first parameter, and DoWork
passes one down. Subprocess and HTTP derive a context.WithTimeout from it, so a
deadline (or cancellation) propagates uniformly — Go's idiom for bounding I/O.
2.2 Subprocess boundary
ProcessRunner routes all subprocess calls through exec.CommandContext:
- Never a shell. Pass the program + an args slice — no
sh -c, so no injection from a filename or argument. - Always a timeout. A
context.WithTimeoutkills the child on deadline; detectctx.Err() == context.DeadlineExceededand returnTimeoutError(exit 124). - On non-zero exit, return
CommandError{Command, Code, Stderr}(extract the code viaerrors.Ason*exec.ExitError).
2.3 Config and precedence
Resolve settings with an explicit, documented precedence — higher wins:
CLI flags > environment variables > config file > built-in defaultsResolveConfig is pure (coalesces the pointer layers so an absent value never
clobbers a lower layer). Each source is read by one boundary — ReadConfigFile
(JSON), ConfigFromEnvironment (through the EnvironmentReading hook).
Secrets get no CLI flag (argv is world-readable via ps); a token comes from
the environment or config file. Never log a raw Config — route it through
Redacted(…). The template plumbs the token (precedence + redaction) but does
not attach it to a request — the auth scheme is tool-specific. Wire it where your
API needs it, e.g. in HTTPClient.Fetch:
req.Header.Set("Authorization", "Bearer "+token) (thread config.Token in).
2.4 HTTP requests
HTTPClient wraps net/http in one boundary:
- Set an explicit timeout (via the context).
- Raise on non-2xx yourself.
net/httpdoes not error on 4xx/5xx, so checkresp.StatusCodeand returnHTTPError; a 404 page would otherwise sail through as a "successful" body. - Inject a fake
HTTPFetcherto test offline.
2.5 Colorized logging
Logging uses the template's own Logger, constructed once in main.go and
writing to stderr (never stdout — stdout is for program data). Color:
ShouldColor(mode, isTTY, env)is a pure decision — precedence--color=always/never>NO_COLOR/FORCE_COLOR> TTY (and notTERM=dumb).- TTY detection in
main.gouses the char-device bit onos.Stderr.Stat()— no dependency (avoidsgolang.org/x/term).
2.6 Robustness & signals
- Bound every subprocess and HTTP call with a timeout (§2.2, §2.4).
- Signals: a goroutine maps SIGTERM →
os.Exit(143)and SIGINT →os.Exit(130)(signals.go). Go surfaces a closed downstream pipe as a write error, not a SIGPIPE kill — no handler needed. - Preflight external tools for glue scripts (a
exec.LookPath-style check up front), returningConfigErrorlisting every missing tool.
2.7 Subcommands (if needed)
cobra handles this natively: a root *cobra.Command with
root.AddCommand(sub), each its own *cobra.Command with a RunE. No manual
dispatch.
2.8 Man page (optional)
cobra can emit man pages via spf13/cobra/doc, but the simplest path (no extra
dep) is help2man from the built binary's --help:
help2man --no-info --name "tool" "./tool" > tool.1
sudo install -m 644 tool.1 /usr/local/share/man/man1/tool.12.9 Build & install (optional, for tools you distribute)
The README carries the environment set-up, not just the gates: a
Prerequisites section with per-platform install of the Go toolchain
(macOS/Linux/Windows) and the dependency list, pointing to go.mod + go.sum as
the pinned source. For a distributed binary, an optional --dependencies flag
printing the pinned versions is a nice touch — skip it for source-run tools.
- Single binary:
go build -o tool .→ one static, self-contained executable; install it onto PATH. - Cross-compile:
GOOS=linux GOARCH=arm64 go build -o tool .— Go's headline feature; no toolchain gymnastics. - Run from source:
go run . <args>(no build step).
3. CLI contract
cobra gives most of the baseline for free:
-h/--helpis automatic;--versionfrom the command'sVersionfield.- Verbosity:
Flags().CountVarP(&verbose, "verbose", "v", …)counts occurrences (-vv= 2); same forquiet. Map the net count withLogLevelForVerbosity(verbose - quiet). --coloris a string flag defaulting toauto, parsed intoColorMode.--dry-runshort-circuits before output, announced at warning level.- Stream discipline: program data → stdout (
fmt.Println); logs, progress, diagnostics → stderr (theLogger). - Grouped help caveat: unlike the other skills, cobra groups subcommands,
not flags —
--helplists all flags under one "Flags:" heading. Live with it, or set a custom usage template if grouping matters.
Exit codes
Map typed errors to a code in main: errors.As(err, &ec) where ec is an
ExitCoder, then os.Exit(ec.ExitCode()).
| Code | Meaning |
0 | Success (also --help/--version) |
1 | Runtime error (a tool error); cobra also uses 1 for usage errors |
124 | A subprocess exceeded its timeout (TimeoutError) |
130 | Interrupted — SIGINT / Ctrl-C (signals.go) |
143 | Terminated — SIGTERM / kill (signals.go) |
Set SilenceUsage/SilenceErrors on the root command and print the error
yourself, so a runtime failure doesn't dump the usage text.
4. Tests with go test
Every tool ships *_test.go files in internal/core (same package, white-box).
- Never test subprocess or network for real — inject a fake implementing the boundary interface. That's what the interface is for.
- Fakes are tiny structs (
type fakeRunner struct{…}with one method), not a mocking framework. - Pure logic and precedence are tested by direct call — no fakes at all.
- Use plain
testing(t.Errorf) andreflect.DeepEqualfor slices; table maps for parameterized cases.
Coverage priority: pure logic and orchestration first, then config precedence, then arg parsing.
5. Quality gates
Four gates, all from the Go toolchain — no extra installs:
gofmt -l . # formatting: empty output = clean
go vet ./... # static checks
go build ./... # compiles — the compiler IS the type gate
go test ./... # tests- Indentation is tabs.
gofmtis non-configurable — this is the one place Go overrides the 2-space house style; don't fight it.gofmt -w .fixes layout. go vetis the baseline static analyzer; addstaticcheckfor a stricter pass if the tool earns it.
Summary of what to produce
Faced with a Go tool request, produce:
- A Go module copied from
assets/template/: logic + boundaries ininternal/core, a thin cobramain.go, a path-style module name, deps pinned bygo mod tidy, the §2 structure, the §3 CLI contract and exit codes, and the robustness rules the tool's job earns (shelling out earns the timeout + preflight; the network earns the HTTP boundary + timeout; holding config/ secrets earns precedence + redaction). Delete what it doesn't earn. *_test.gocovering pure logic, config precedence, orchestration (with fakes), and any boundary the tool ships.- In one line: how to run it (
go run . <args>), and that the four gates —gofmt+vet+build+test— are green.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

