toolchains/golang/cli/SKILL.md
Building production-quality CLI tools with Cobra command framework and Viper configuration management
npx skillsauth add bobmatnyc/claude-mpm-skills golang-cli-cobra-viperInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cobra and Viper are the industry-standard libraries for building production-quality CLIs in Go. Cobra provides command structure and argument parsing, while Viper manages configuration from multiple sources with clear precedence rules.
Key Features:
app verb noun --flag)Used By: Kubernetes (kubectl), Docker CLI, GitHub CLI (gh), Hugo, Helm, and 100+ major projects
Activate this skill when:
Cobra follows the APPNAME VERB NOUN --FLAG pattern popularized by git and kubectl.
// cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A powerful CLI tool for developers",
Long: `MyApp is a CLI tool that demonstrates best practices
for building production-quality command-line applications.
Complete documentation is available at https://myapp.example.com`,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Persistent flags (available to all subcommands)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Bind persistent flags to viper
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
viper.AddConfigPath(home)
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
viper.SetConfigName(".myapp")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
if viper.GetBool("verbose") {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
}
// cmd/deploy.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy application to specified environment",
Long: `Deploy the application to the specified environment.
Supports: dev, staging, production`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{"dev", "staging", "production"},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation logic runs before RunE
env := args[0]
if env == "production" && !viper.GetBool("force") {
return fmt.Errorf("production deploys require --force flag")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
env := args[0]
region := viper.GetString("region")
force := viper.GetBool("force")
fmt.Printf("Deploying to %s in region %s (force=%v)\n", env, region, force)
// Actual deployment logic
return deploy(env, region, force)
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup or notifications
fmt.Println("Deployment complete")
return nil
},
}
func init() {
rootCmd.AddCommand(deployCmd)
// Local flags (only for this command)
deployCmd.Flags().StringP("region", "r", "us-east-1", "AWS region")
deployCmd.Flags().BoolP("force", "f", false, "Force deployment without confirmation")
// Bind flags to viper
viper.BindPFlag("region", deployCmd.Flags().Lookup("region"))
viper.BindPFlag("force", deployCmd.Flags().Lookup("force"))
}
func deploy(env, region string, force bool) error {
// Implementation
return nil
}
// Persistent flags: Available to command and all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")
// Local flags: Only available to this specific command
deployCmd.Flags().String("region", "us-east-1", "deployment region")
deployCmd.Flags().Bool("force", false, "force deployment")
// Required flags
deployCmd.MarkFlagRequired("region")
// Flag dependencies
deployCmd.MarkFlagsRequiredTogether("username", "password")
deployCmd.MarkFlagsMutuallyExclusive("json", "yaml")
Cobra provides execution hooks for setup and cleanup:
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start API server",
// Execution order (all optional):
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before PreRunE, inherited by subcommands
return setupLogging()
},
PreRunE: func(cmd *cobra.Command, args []string) error {
// Validation and setup before RunE
return validateConfig()
},
RunE: func(cmd *cobra.Command, args []string) error {
// Main command logic
return startServer()
},
PostRunE: func(cmd *cobra.Command, args []string) error {
// Cleanup after RunE
return cleanup()
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
// Runs after PostRunE, inherited by subcommands
return flushLogs()
},
}
Important: Use RunE, PreRunE, PostRunE (error-returning versions) instead of Run, PreRun, PostRun.
Viper follows a strict precedence order (highest to lowest):
viper.Set("key", value))viper.BindPFlag)MYAPP_KEY=value)~/.myapp.yaml, ./config.yaml)viper.SetDefault("key", value))func initConfig() {
// 1. Set defaults (lowest priority)
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", 5432)
// 2. Config file locations (checked in order)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("/etc/myapp/")
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
// 3. Environment variables (prefix + automatic mapping)
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv() // MYAPP_PORT, MYAPP_DATABASE_HOST, etc.
// 4. Read config file (optional)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found - use defaults and env vars
} else {
// Config file found but error reading it
return err
}
}
// 5. Flags will be bound in init() functions (highest priority)
}
Viper automatically maps environment variables with prefix and dot notation:
viper.SetEnvPrefix("MYAPP") // Prefix for env vars
viper.AutomaticEnv() // Enable automatic mapping
// Config key → Environment variable
// "port" → MYAPP_PORT
// "database.host" → MYAPP_DATABASE_HOST
// "database.port" → MYAPP_DATABASE_PORT
// "aws.s3.region" → MYAPP_AWS_S3_REGION
Manual mapping for non-standard env var names:
viper.BindEnv("database.host", "DB_HOST") // Custom env var name
viper.BindEnv("database.password", "DB_PASSWORD") // Different naming convention
Viper supports multiple formats: YAML, JSON, TOML, HCL, INI, envfile, Java properties.
config.yaml:
port: 8080
log_level: info
database:
host: localhost
port: 5432
user: postgres
ssl_mode: require
aws:
region: us-east-1
s3:
bucket: my-app-bucket
Accessing config values:
port := viper.GetInt("port") // 8080
dbHost := viper.GetString("database.host") // "localhost"
s3Bucket := viper.GetString("aws.s3.bucket") // "my-app-bucket"
// Type-safe access
if viper.IsSet("database.ssl_mode") {
sslMode := viper.GetString("database.ssl_mode")
}
// Unmarshal into struct
type Config struct {
Port int `mapstructure:"port"`
LogLevel string `mapstructure:"log_level"`
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
SSLMode string `mapstructure:"ssl_mode"`
} `mapstructure:"database"`
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return err
}
The key to Cobra + Viper integration is binding flags to Viper keys:
// cmd/root.go
func init() {
cobra.OnInitialize(initConfig) // Load config before command execution
// Define flags
rootCmd.PersistentFlags().String("config", "", "config file")
rootCmd.PersistentFlags().String("log-level", "info", "log level")
rootCmd.PersistentFlags().Int("port", 8080, "server port")
// Bind flags to Viper (critical step!)
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
viper.BindPFlag("log_level", rootCmd.PersistentFlags().Lookup("log-level"))
viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}
func initConfig() {
// This runs BEFORE command execution via cobra.OnInitialize
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.AddConfigPath("$HOME/.myapp")
viper.AddConfigPath(".")
viper.SetConfigName("config")
}
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.ReadInConfig() // Ignore errors - config file is optional
}
Flag binding strategies:
// Strategy 1: Bind each flag individually (explicit)
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))
// Strategy 2: Bind all flags automatically (convenient)
viper.BindPFlags(rootCmd.Flags())
// Strategy 3: Hybrid approach (recommended)
// - Bind persistent flags globally
// - Bind local flags in each command's init()
rootCmd.PersistentFlags().String("config", "", "config file")
viper.BindPFlags(rootCmd.PersistentFlags())
deployCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("deploy.region", deployCmd.Flags().Lookup("region"))
Use PersistentPreRunE to load and validate configuration:
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My application",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Runs before ALL commands (inherited by subcommands)
// 1. Validate required config
if !viper.IsSet("api_key") {
return fmt.Errorf("API key not configured (set MYAPP_API_KEY or add to config file)")
}
// 2. Setup logging based on config
logLevel := viper.GetString("log_level")
if err := setupLogging(logLevel); err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
// 3. Initialize clients/services
apiKey := viper.GetString("api_key")
if err := initAPIClient(apiKey); err != nil {
return fmt.Errorf("failed to initialize API client: %w", err)
}
return nil
},
}
Cobra generates shell completion scripts for bash, zsh, fish, and PowerShell.
// cmd/completion.go
package cmd
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `Generate shell completion script for myapp.
To load completions:
Bash:
$ source <(myapp completion bash)
# To load automatically, add to ~/.bashrc:
$ echo 'source <(myapp completion bash)' >> ~/.bashrc
Zsh:
$ source <(myapp completion zsh)
# To load automatically, add to ~/.zshrc:
$ echo 'source <(myapp completion zsh)' >> ~/.zshrc
Fish:
$ myapp completion fish | source
# To load automatically:
$ myapp completion fish > ~/.config/fish/completions/myapp.fish
PowerShell:
PS> myapp completion powershell | Out-String | Invoke-Expression
# To load automatically, add to PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
Provide dynamic completions for command arguments:
var deployCmd = &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy to environment",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Return available environments
envs := []string{"dev", "staging", "production"}
return envs, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0])
},
}
// Custom flag completion
deployCmd.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
regions := []string{"us-east-1", "us-west-2", "eu-west-1"}
return regions, cobra.ShellCompDirectiveNoFileComp
})
// ❌ BAD: Technical jargon
return fmt.Errorf("db connection failed: EOF")
// ✅ GOOD: Actionable error message
return fmt.Errorf("cannot connect to database at %s:%d\nPlease check:\n - Database is running\n - Credentials are correct (MYAPP_DB_PASSWORD)\n - Network connectivity", host, port)
// ✅ GOOD: Suggest remediation
if !viper.IsSet("api_key") {
return fmt.Errorf("API key not configured\nSet environment variable: export MYAPP_API_KEY=your-key\nOr add to config file: ~/.myapp.yaml")
}
import "github.com/briandowns/spinner"
func deploy(env string) error {
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
s.Suffix = " Deploying to " + env + "..."
s.Start()
defer s.Stop()
// Deployment logic
if err := performDeployment(env); err != nil {
s.Stop()
return err
}
s.Stop()
fmt.Println("✓ Deployment successful")
return nil
}
import (
"encoding/json"
"github.com/olekukonko/tablewriter"
)
func displayResults(items []Item, format string) error {
switch format {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(items)
case "table":
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"ID", "Name", "Status"})
for _, item := range items {
table.Append([]string{item.ID, item.Name, item.Status})
}
table.Render()
return nil
default:
return fmt.Errorf("unknown format: %s (use json or table)", format)
}
}
import (
"log"
"os"
)
var (
// User-facing output (stdout)
out = os.Stdout
// Logging and errors (stderr)
logger = log.New(os.Stderr, "[myapp] ", log.LstdFlags)
)
func RunCommand() error {
// User output: stdout
fmt.Fprintln(out, "Processing files...")
// Debug/verbose logging: stderr
if viper.GetBool("verbose") {
logger.Println("Reading config from", viper.ConfigFileUsed())
}
// Errors: stderr
if err := process(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
// Success message: stdout
fmt.Fprintln(out, "✓ Complete")
return nil
}
// cmd/deploy_test.go
package cmd
import (
"bytes"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func executeCommand(root *cobra.Command, args ...string) (output string, err error) {
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(args)
err = root.Execute()
return buf.String(), err
}
func TestDeployCommand(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
wantOut string
}{
{
name: "deploy to dev",
args: []string{"deploy", "dev"},
wantErr: false,
wantOut: "Deploying to dev",
},
{
name: "deploy to production without force",
args: []string{"deploy", "production"},
wantErr: true,
wantOut: "production deploys require --force flag",
},
{
name: "deploy to production with force",
args: []string{"deploy", "production", "--force"},
wantErr: false,
wantOut: "Deploying to production",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, err := executeCommand(rootCmd, tt.args...)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Contains(t, output, tt.wantOut)
})
}
}
func TestCommandWithConfig(t *testing.T) {
// Reset viper state before each test
viper.Reset()
// Set test configuration
viper.Set("region", "eu-west-1")
viper.Set("api_key", "test-key-123")
output, err := executeCommand(rootCmd, "deploy", "staging")
require.NoError(t, err)
assert.Contains(t, output, "eu-west-1")
}
func TestOutputFormat(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
defer func() { os.Stdout = oldStdout }()
// Execute command
err := listCmd.RunE(listCmd, []string{})
require.NoError(t, err)
// Read output
w.Close()
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
assert.Contains(t, output, "ID")
assert.Contains(t, output, "Name")
}
Use Cobra when:
git clone, docker run)--flag, -f)--help)Don't use Cobra when:
flag package)Use Viper when:
Don't use Viper when:
Add shell completion when:
Skip shell completion when:
Use persistent flags when:
--verbose, --config)--dry-run, --output)Use local flags when:
--region for deploy command)Wrong:
var deployCmd = &cobra.Command{
Use: "deploy",
Run: func(cmd *cobra.Command, args []string) {
deploy(args[0]) // Ignores error!
},
}
Correct:
var deployCmd = &cobra.Command{
Use: "deploy",
RunE: func(cmd *cobra.Command, args []string) error {
return deploy(args[0]) // Proper error handling
},
}
Wrong:
// Confusing: Which takes precedence?
config.Port = viper.GetInt("port")
if os.Getenv("PORT") != "" {
config.Port = atoi(os.Getenv("PORT"))
}
if *flagPort != 0 {
config.Port = *flagPort
}
Correct:
// Clear: Viper handles precedence automatically
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.SetDefault("port", 8080)
config.Port = viper.GetInt("port") // Respects: flag > env > config > default
Wrong:
rootCmd.Flags().String("region", "us-east-1", "AWS region")
// Flag not bound to Viper - won't respect precedence!
func deploy() {
region := viper.GetString("region") // Always returns config file value
}
Correct:
rootCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("region", rootCmd.Flags().Lookup("region")) // Bind it!
func deploy() {
region := viper.GetString("region") // Respects flag > env > config
}
Wrong:
// No tests for CLI commands - bugs slip through
Correct:
func TestDeployCommand(t *testing.T) {
output, err := executeCommand(rootCmd, "deploy", "staging", "--region", "eu-west-1")
require.NoError(t, err)
assert.Contains(t, output, "Deploying to staging")
assert.Contains(t, output, "eu-west-1")
}
Wrong:
return fmt.Errorf("connection failed") // Unhelpful
Correct:
return fmt.Errorf("cannot connect to database at %s:%d\nCheck:\n - Database is running\n - Credentials (MYAPP_DB_PASSWORD)\n - Firewall rules", host, port)
Wrong:
var rootCmd = &cobra.Command{
Use: "myapp",
Run: func(cmd *cobra.Command, args []string) {
if err := execute(); err != nil {
fmt.Println(err) // Error not propagated
}
},
}
Correct:
var rootCmd = &cobra.Command{
Use: "myapp",
RunE: func(cmd *cobra.Command, args []string) error {
return execute() // Cobra handles error display and exit code
},
}
Minimal production-ready CLI structure:
myapp/
├── cmd/
│ ├── root.go # Root command + config loading
│ ├── deploy.go # Deploy subcommand
│ ├── status.go # Status subcommand
│ └── completion.go # Shell completion
├── main.go # Entry point
├── config.yaml # Example config file
└── go.mod
main.go:
package main
import "myapp/cmd"
func main() {
cmd.Execute()
}
cmd/root.go: See "Command Structure Pattern" section above
Building and installing:
# Development
go run main.go deploy staging --region us-west-2
# Production build
go build -o myapp
# Install globally
go install
# Enable shell completion
myapp completion bash > /etc/bash_completion.d/myapp
Official Documentation:
Learning Resources:
Production Examples:
Related Skills:
golang-testing-strategies - Testing CLI commands comprehensivelygolang-http-servers - Building API servers with configurationgolang-concurrency-patterns - Async operations in CLI toolsYou know you've mastered Cobra + Viper when:
VERB NOUN --FLAG)development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...