How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-cliBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Go CLI Design
A good CLI is a well-behaved Unix citizen: flags before magic, stdout for data, stderr for diagnostics, exit codes that scripts can trust, and Ctrl+C that actually stops it.
1. Structure: Testable main
func main() {
ctx, stop := signal.NotifyContext(context.Background(),
os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
fs := flag.NewFlagSet("mytool", flag.ContinueOnError)
fs.SetOutput(stderr)
verbose := fs.Bool("v", false, "verbose output")
out := fs.String("o", "-", "output file (- for stdout)")
if err := fs.Parse(args); err != nil {
return err
}
// ...
_ = verbose
_ = out
return nil
}signal.NotifyContextmakes Ctrl+C cancel the context — every
long operation takes ctx and stops cleanly.
runreceives args and streams — tests call it directly with
strings.Reader/bytes.Buffer, no subprocess needed.
os.Exitonly inmain(it skips defers).
2. stdout vs stderr
- stdout: the program's output — data, results, the thing you pipe.
- stderr: logs, progress, warnings, usage errors.
--jsonor detecting a pipe (!term.IsTerminal(int(os.Stdout.Fd())))
should silence decorations, never change the data.
// ✅ Good — result to stdout, progress to stderr
fmt.Fprintf(stderr, "processed %d files\n", n)
fmt.Fprintln(stdout, result)
// ❌ Bad — mixing both into stdout breaks every pipe
fmt.Printf("processing...\ndone: %s\n", result)3. Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic runtime failure |
| 2 | Usage error (bad flags/arguments) — flag package's convention |
| >2 | Tool-specific, documented meanings (e.g. grep's 1 = no match) |
Map errors to codes in one place (main), not scattered os.Exit calls. If scripts will branch on distinct failures, define sentinel errors and translate: errors.Is(err, ErrNoMatch) → 1.
4. Flags and Arguments
- Flags for options, positional args for the primary operands:
mytool -v convert input.yaml, not mytool --input=input.yaml.
- Every flag has a usage string;
-h/-helpoutput is your primary UX. - Accept
-as "stdin/stdout" for file arguments. - Defaults must be safe: destructive behavior behind explicit flags
(--force), never default-on.
- Read secrets from env or files, never from flags (
psleaks argv).
5. Subcommands
Standard library, fine up to a handful of commands:
switch fs.Arg(0) {
case "serve":
return runServe(ctx, fs.Args()[1:], stdout, stderr)
case "migrate":
return runMigrate(ctx, fs.Args()[1:], stdout, stderr)
default:
fmt.Fprintln(stderr, usage)
return fmt.Errorf("unknown command %q", fs.Arg(0))
}Adopt Cobra when you need nested commands, generated help/completions, and many flags — the structure pays for the dependency:
var rootCmd = &cobra.Command{Use: "mytool", SilenceUsage: true}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the server",
RunE: func(cmd *cobra.Command, args []string) error {
return serve(cmd.Context(), addr) // RunE returns errors; no os.Exit
},
}
func init() {
serveCmd.Flags().StringVar(&addr, "addr", ":8080", "listen address")
rootCmd.AddCommand(serveCmd)
}Cobra rules: always RunE (never Run + os.Exit), set SilenceUsage: true so runtime errors don't dump help, pass cmd.Context() down. Add Viper only when layered config (flags > env > file) is a real requirement — for most tools flag + os.Getenv is enough.
6. Output for Humans and Machines
--jsonflag for machine consumption; table/text default for humans.- Never emit ANSI colors when stdout is not a terminal or
NO_COLOR
is set.
- Progress bars/spinners go to stderr and only when it's a terminal.
Verification Checklist
run(ctx, args, stdin, stdout, stderr)pattern — logic testable without subprocesssignal.NotifyContextwired; long operations respect ctx cancellation- Data on stdout, diagnostics on stderr — verified by piping
- Exit codes: 0 success, 2 usage, documented codes otherwise;
os.Exitonly in main - Every flag has usage text;
-houtput reviewed -accepted for stdin/stdout where files are taken- Destructive actions require explicit flags
- No secrets via argv
- Cobra (if used): RunE everywhere, SilenceUsage, context propagated
- Colors/spinners disabled for non-TTY and NO_COLOR



