
Golang Cli Cobra Viper
- 318 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
golang-cli-cobra-viper is an agent skill (version 1.0.0) that wires Go Cobra command trees with Viper configuration precedence for developers building multi-command production CLIs with flags, env vars, and completions.
About
golang-cli-cobra-viper (version 1.0.0) in bobmatnyc/claude-mpm-skills teaches the industry-standard Go CLI stack used by kubectl, Docker CLI, and GitHub CLI. Agents define POSIX-style root and subcommands with Cobra, then integrate Viper so configuration resolves in clear precedence: command-line flags, environment variables, config files, and defaults. The skill documents viper.BindPFlag and BindPFlags patterns, PersistentPreRunE hooks for centralized config loading, SetEnvPrefix mapping, and RunE error-returning handlers instead of silent Run failures. Shell completion generation covers bash, zsh, fish, and PowerShell. Worked examples include deploy subcommands with region and force flags, log-level overrides, and config file discovery in home directories. Developers reach for golang-cli-cobra-viper when scaffolding DevOps utilities, internal developer tools, or any Go binary needing subcommands plus flexible configuration without rewriting plumbing. Triggers include multi-command CLI, Cobra, Viper, shell completion, and Go developer tool requests.
- Cobra subcommand hierarchies
- Viper config and env binding
- persistent and local flags
- config file loading
- structured CLI UX
Golang Cli Cobra Viper by the numbers
- 318 all-time installs (skills.sh)
- Ranked #168 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-cli-cobra-viperAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 318 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
How do you wire Cobra and Viper in Go?
Build robust Go command-line tools with Cobra subcommands, Viper configuration, persistent flags, and consistent terminal UX.
Who is it for?
Go developers building production CLIs that need subcommands, multi-source configuration, and auto-generated shell completions.
Skip if: Single-file Go scripts without subcommands, Rust or Python CLI projects, or teams avoiding third-party CLI frameworks.
When should I use this skill?
User builds a Go CLI with Cobra subcommands, Viper config files, persistent flags, or shell completion scripts.
What you get
Go cmd package with Cobra command tree, Viper config binding, and generated shell completion scripts.
- Cobra command tree in cmd/
- Viper config binding code
- Shell completion scripts
By the numbers
- Skill version 1.0.0 in the Claude MPM toolchain category
- Documents 4-level Viper configuration precedence rules
- Covers shell completion for 4 shells: bash, zsh, fish, PowerShell
Files
Go CLI Development with Cobra & Viper
Overview
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:
- 🎯 Cobra Commands: POSIX-compliant CLI with subcommands (
app verb noun --flag) - ⚙️ Viper Config: Unified configuration from flags, env vars, and config files
- 🔄 Integration: Seamless Cobra + Viper plumbing patterns
- 🐚 Shell Completion: Auto-generated completions for bash, zsh, fish, PowerShell
- ✅ Production Ready: Battle-tested by kubectl, docker, gh, hugo
Used By: Kubernetes (kubectl), Docker CLI, GitHub CLI (gh), Hugo, Helm, and 100+ major projects
When to Use This Skill
Activate this skill when:
- Building multi-command CLI tools with subcommands
- Creating developer tools, project generators, or scaffolding utilities
- Implementing admin CLIs for services or infrastructure
- Requiring flexible configuration (flags > env vars > config files > defaults)
- Adding shell completion for frequently-used CLIs
- Building DevOps automation tools or deployment scripts
Cobra Framework
Command Structure Pattern
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())
}
}
}Subcommands with Arguments
// 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 vs. Local Flags
// 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")PreRun/PostRun Hooks
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 Configuration Management
Configuration Priority
Viper follows a strict precedence order (highest to lowest):
1. Explicit Set (viper.Set("key", value)) 2. Command-line Flags (bound with viper.BindPFlag) 3. Environment Variables (MYAPP_KEY=value) 4. Config File (~/.myapp.yaml, ./config.yaml) 5. Key/Value Store (etcd, Consul - optional) 6. Defaults (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)
}Environment Variable Mapping
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_REGIONManual 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 conventionConfig File Formats
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-bucketAccessing 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
}Cobra + Viper Integration
Critical Integration Pattern
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"))PersistentPreRun for Config Loading
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
},
}Shell Completion
Cobra generates shell completion scripts for bash, zsh, fish, and PowerShell.
Adding Completion Command
// 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)
}Custom Completion Functions
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
})CLI Best Practices
User-Friendly Error Messages
// ❌ 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")
}Progress Indicators
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
}Output Formatting
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)
}
}Logging vs. User Output
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
}Testing CLI Applications
Testing Command Execution
// 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)
})
}
}Testing with Viper Configuration
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")
}Capturing Output
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")
}Decision Trees
When to Use Cobra
Use Cobra when:
- ✅ Building multi-command CLI with subcommands (e.g.,
git clone,docker run) - ✅ Need POSIX-compliant flag parsing (
--flag,-f) - ✅ Want built-in help generation (
--help) - ✅ Require shell completion support
- ✅ Building professional CLI used by other developers
Don't use Cobra when:
- ❌ Simple single-command script (use
flagpackage) - ❌ Internal-only tool with 1-2 flags
- ❌ Prototyping or throwaway scripts
When to Use Viper
Use Viper when:
- ✅ Need configuration from multiple sources (flags, env vars, files)
- ✅ Want clear configuration precedence rules
- ✅ Support multiple config file formats (YAML, JSON, TOML)
- ✅ Require environment variable mapping with prefixes
- ✅ Need live config reloading (watch config file changes)
Don't use Viper when:
- ❌ Only using command-line flags (Cobra alone is sufficient)
- ❌ Hardcoded configuration values
- ❌ Simple scripts with no configuration
When to Add Shell Completion
Add shell completion when:
- ✅ CLI used frequently by developers (daily/hourly)
- ✅ Many subcommands or complex flag combinations
- ✅ Arguments have known valid values (e.g., environments, regions)
- ✅ Building professional developer tools
Skip shell completion when:
- ❌ CLI used rarely (monthly or less)
- ❌ Simple commands with few options
- ❌ Internal-only tools
When to Use Persistent Flags
Use persistent flags when:
- ✅ Flag applies to ALL subcommands (e.g.,
--verbose,--config) - ✅ Common configuration shared across commands
- ✅ Global behavior modifiers (e.g.,
--dry-run,--output)
Use local flags when:
- ✅ Flag only relevant to specific command
- ✅ Command-specific parameters (e.g.,
--regionfor deploy command)
Anti-Patterns
❌ Not Handling Errors in PreRunE/RunE
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
},
}❌ Mixing Configuration Sources Without Clear Precedence
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❌ Forgetting to Bind Flags to Viper
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
}❌ Not Testing CLI Commands
Wrong:
// No tests for CLI commands - bugs slip throughCorrect:
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")
}❌ Poor Error Messages
Wrong:
return fmt.Errorf("connection failed") // UnhelpfulCorrect:
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)❌ Using Run Instead of RunE
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
},
}Production Example
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.modmain.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/myappResources
Official Documentation:
- Cobra User Guide - Official framework documentation
- Viper Documentation - Configuration management guide
Learning Resources:
- "Building CLI Apps in Go with Cobra & Viper" (November 2025) - Comprehensive tutorial
- "The Cobra & Viper Journey" - Learning path for CLI development
- Cobra Generator - Scaffolding tool for new CLIs
Production Examples:
Related Skills:
golang-testing-strategies- Testing CLI commands comprehensivelygolang-http-servers- Building API servers with configurationgolang-concurrency-patterns- Async operations in CLI tools
Success Criteria
You know you've mastered Cobra + Viper when:
- ✅ Commands follow POSIX conventions (
VERB NOUN --FLAG) - ✅ Configuration precedence is clear: flags > env > config > defaults
- ✅ All flags bound to Viper for unified config access
- ✅ Shell completion generated for all major shells
- ✅ Error messages are actionable and user-friendly
- ✅ CLI commands have comprehensive tests
- ✅ Help text auto-generated and accurate
- ✅ PersistentPreRunE used for global setup/validation
- ✅ Separation of concerns: user output (stdout) vs. logging (stderr)
- ✅ Config files optional - CLI works with flags/env vars alone
{
"name": "golang-cli-cobra-viper",
"version": "1.0.0",
"category": "toolchain",
"toolchain": "golang",
"framework": "cli",
"tags": [
"cli",
"golang",
"cobra",
"viper",
"configuration",
"shell-completion"
],
"entry_point_tokens": 140,
"full_tokens": 6894,
"related_skills": [
"golang-testing-strategies"
],
"author": "Claude MPM",
"license": "MIT"
}
Related skills
How it compares
Pick golang-cli-cobra-viper for Cobra plus Viper production CLIs; pick bare flag package skills when a single-command script needs no config file layer.
FAQ
What config precedence does golang-cli-cobra-viper use?
golang-cli-cobra-viper follows Viper precedence where command-line flags beat environment variables, which beat config files, which beat defaults, bound via viper.BindPFlag in Cobra init hooks.
Which shells get completion scripts?
golang-cli-cobra-viper documents auto-generated completion scripts for bash, zsh, fish, and PowerShell using Cobra's built-in completion command pattern.
When should developers use golang-cli-cobra-viper?
golang-cli-cobra-viper fits Go projects needing multi-command CLIs, flexible configuration from flags and env vars, and production-grade terminal UX beyond a single main package.