Claude-skill-registry cli-config
Manage CLI application configuration with Cobra and Viper. Use when implementing config files, environment variables, flags binding, or when user mentions Viper, configuration management, config files, or CLI settings.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cli-config" ~/.claude/skills/majiayu000-claude-skill-registry-cli-config-bba820 && rm -rf "$T"
skills/data/cli-config/SKILL.mdCLI Configuration with Cobra & Viper
Build flexible, hierarchical configuration systems for CLI applications using Cobra (commands/flags) and Viper (config management).
Your Role: Configuration Architect
You design configuration systems with proper precedence and flexibility. You:
✅ Implement config hierarchy - Flags > Env > Config > Defaults ✅ Bind flags to Viper - Seamless integration ✅ Support multiple formats - YAML, JSON, TOML ✅ Handle environment variables - With prefixes ✅ Provide config commands - init, show, validate ✅ Follow CLY patterns - Use project structure
❌ Do NOT hardcode paths - Use conventions ❌ Do NOT skip validation - Validate config ❌ Do NOT ignore precedence - Follow hierarchy
Configuration Precedence
Viper uses this precedence order (highest to lowest):
- Explicit
callsviper.Set() - Command-line flags
- Environment variables
- Config file values
- Defaults
viper.SetDefault("port", 8080) // 5. Default // config.yaml: port: 8081 // 4. Config file os.Setenv("APP_PORT", "8082") // 3. Environment cobra.Flags().Int("port", 0, "Port") // 2. Flag viper.Set("port", 8083) // 1. Explicit set
Basic Setup
Initialize Viper
package config import ( "fmt" "os" "github.com/spf13/viper" ) func Init() error { // Set config name (no extension) viper.SetConfigName("config") // Set config type viper.SetConfigType("yaml") // Add search paths viper.AddConfigPath(".") viper.AddConfigPath("$HOME/.myapp") viper.AddConfigPath("/etc/myapp") // Read config if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Config file not found; use defaults return nil } return fmt.Errorf("error reading config: %w", err) } return nil }
With Cobra Integration
package cmd import ( "fmt" "os" "github.com/spf13/cobra" "github.com/spf13/viper" ) var cfgFile string var rootCmd = &cobra.Command{ Use: "myapp", Short: "My application", } func Execute() { if err := rootCmd.Execute(); err != nil { os.Exit(1) } } func init() { cobra.OnInitialize(initConfig) // Global flags rootCmd.PersistentFlags().StringVar( &cfgFile, "config", "", "config file (default is $HOME/.myapp/config.yaml)", ) } func initConfig() { if cfgFile != "" { // Use explicit config file viper.SetConfigFile(cfgFile) } else { // Find home directory home, err := os.UserHomeDir() if err != nil { fmt.Println(err) os.Exit(1) } // Search config in home directory and current directory viper.AddConfigPath(home + "/.myapp") viper.AddConfigPath(".") viper.SetConfigType("yaml") viper.SetConfigName("config") } // Read environment variables viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP") // Read config file if err := viper.ReadInConfig(); err == nil { fmt.Println("Using config file:", viper.ConfigFileUsed()) } }
Configuration Patterns
Set Defaults
func setDefaults() { // Server viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost") viper.SetDefault("server.timeout", "30s") // Database viper.SetDefault("database.host", "localhost") viper.SetDefault("database.port", 5432) viper.SetDefault("database.name", "myapp") // Logging viper.SetDefault("log.level", "info") viper.SetDefault("log.format", "json") }
Bind Flags
Single flag:
cmd.Flags().IntP("port", "p", 8080, "Port to run on") viper.BindPFlag("server.port", cmd.Flags().Lookup("port"))
All flags:
cmd.Flags().Int("port", 8080, "Port") cmd.Flags().String("host", "localhost", "Host") viper.BindPFlags(cmd.Flags())
Persistent flags:
rootCmd.PersistentFlags().String("log-level", "info", "Log level") viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level"))
Environment Variables
Auto-map all env vars:
viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP") // MYAPP_SERVER_PORT → server.port // MYAPP_DATABASE_NAME → database.name
Custom env key replacer:
import "strings" viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP") // MYAPP_SERVER_PORT → server.port (. → _)
Bind specific env var:
viper.BindEnv("database.password", "DB_PASSWORD") // DB_PASSWORD → database.password
Read Config Values
Get typed values:
port := viper.GetInt("server.port") host := viper.GetString("server.host") enabled := viper.GetBool("feature.enabled") timeout := viper.GetDuration("server.timeout") tags := viper.GetStringSlice("tags")
Check if set:
if viper.IsSet("server.port") { port := viper.GetInt("server.port") }
Get with default:
port := viper.GetInt("server.port") if port == 0 { port = 8080 }
Unmarshal to Struct
Full config:
type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Log LogConfig `mapstructure:"log"` } type ServerConfig struct { Port int `mapstructure:"port"` Host string `mapstructure:"host"` Timeout string `mapstructure:"timeout"` } var config Config if err := viper.Unmarshal(&config); err != nil { return fmt.Errorf("unable to decode config: %w", err) }
Subsection:
var serverConfig ServerConfig if err := viper.UnmarshalKey("server", &serverConfig); err != nil { return fmt.Errorf("unable to decode server config: %w", err) }
Write Config
Create default config:
func createDefaultConfig(path string) error { viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost") return viper.WriteConfigAs(path) }
Save current config:
viper.Set("server.port", 9090) // Write to current config file viper.WriteConfig() // Write to specific file viper.WriteConfigAs("/path/to/config.yaml") // Safe write (won't overwrite) viper.SafeWriteConfig()
CLY Project Pattern
Config Package
pkg/config/config.go:
package config import ( "fmt" "os" "path/filepath" "github.com/spf13/viper" ) type Config struct { Server ServerConfig `mapstructure:"server"` Log LogConfig `mapstructure:"log"` } type ServerConfig struct { Port int `mapstructure:"port"` Host string `mapstructure:"host"` } type LogConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` } var cfg *Config // Init initializes the configuration func Init(cfgFile string) error { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { home, err := os.UserHomeDir() if err != nil { return err } viper.AddConfigPath(filepath.Join(home, ".cly")) viper.AddConfigPath(".") viper.SetConfigType("yaml") viper.SetConfigName("config") } setDefaults() viper.AutomaticEnv() viper.SetEnvPrefix("CLY") if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return err } } cfg = &Config{} if err := viper.Unmarshal(cfg); err != nil { return fmt.Errorf("unable to decode config: %w", err) } return nil } func setDefaults() { viper.SetDefault("server.port", 8080) viper.SetDefault("server.host", "localhost") viper.SetDefault("log.level", "info") viper.SetDefault("log.format", "text") } // Get returns the current config func Get() *Config { return cfg } // GetString returns a config value as string func GetString(key string) string { return viper.GetString(key) } // GetInt returns a config value as int func GetInt(key string) int { return viper.GetInt(key) } // GetBool returns a config value as bool func GetBool(key string) bool { return viper.GetBool(key) }
Root Command Integration
cmd/root.go:
package cmd import ( "fmt" "os" "github.com/spf13/cobra" "github.com/yurifrl/cly/pkg/config" ) var cfgFile string var RootCmd = &cobra.Command{ Use: "cly", Short: "CLY - Command Line Yuri", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return config.Init(cfgFile) }, } func Execute() { if err := RootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { RootCmd.PersistentFlags().StringVar( &cfgFile, "config", "", "config file (default is $HOME/.cly/config.yaml)", ) }
Config Command
modules/config/cmd.go:
package configcmd import ( "fmt" "github.com/spf13/cobra" "github.com/spf13/viper" ) func Register(parent *cobra.Command) { cmd := &cobra.Command{ Use: "config", Short: "Manage configuration", } cmd.AddCommand( initCmd(), showCmd(), validateCmd(), ) parent.AddCommand(cmd) } func initCmd() *cobra.Command { return &cobra.Command{ Use: "init", Short: "Initialize config file", RunE: func(cmd *cobra.Command, args []string) error { path, _ := cmd.Flags().GetString("path") if path == "" { path = "$HOME/.cly/config.yaml" } if err := viper.SafeWriteConfigAs(path); err != nil { return fmt.Errorf("failed to create config: %w", err) } fmt.Printf("Config created at: %s\n", path) return nil }, } } func showCmd() *cobra.Command { return &cobra.Command{ Use: "show", Short: "Show current configuration", RunE: func(cmd *cobra.Command, args []string) error { fmt.Println("Current configuration:") fmt.Println("Config file:", viper.ConfigFileUsed()) fmt.Println() for _, key := range viper.AllKeys() { fmt.Printf("%s: %v\n", key, viper.Get(key)) } return nil }, } } func validateCmd() *cobra.Command { return &cobra.Command{ Use: "validate", Short: "Validate configuration", RunE: func(cmd *cobra.Command, args []string) error { // Add validation logic fmt.Println("Configuration is valid") return nil }, } }
Advanced Patterns
Remote Config (etcd, Consul)
import _ "github.com/spf13/viper/remote" func initRemoteConfig() error { viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/myapp.json") viper.SetConfigType("json") if err := viper.ReadRemoteConfig(); err != nil { return err } return nil } // Watch for changes func watchRemoteConfig() { go func() { for { time.Sleep(time.Second * 5) err := viper.WatchRemoteConfig() if err != nil { log.Printf("unable to read remote config: %v", err) continue } } }() }
Watch Config File
viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { fmt.Println("Config file changed:", e.Name) // Reload config var newConfig Config if err := viper.Unmarshal(&newConfig); err != nil { log.Printf("error reloading config: %v", err) return } // Update application state updateAppConfig(newConfig) })
Multiple Config Instances
// Default instance viper.SetConfigName("config") viper.ReadInConfig() // Custom instance v := viper.New() v.SetConfigName("other-config") v.AddConfigPath(".") v.ReadInConfig() port := v.GetInt("port")
Config with Validation
type Config struct { Server ServerConfig `mapstructure:"server" validate:"required"` DB DBConfig `mapstructure:"database" validate:"required"` } type ServerConfig struct { Port int `mapstructure:"port" validate:"required,min=1,max=65535"` Host string `mapstructure:"host" validate:"required,hostname"` } func Load() (*Config, error) { var cfg Config if err := viper.Unmarshal(&cfg); err != nil { return nil, err } // Validate validate := validator.New() if err := validate.Struct(cfg); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } return &cfg, nil }
Nested Config Keys
// Dot notation viper.Set("server.database.host", "localhost") // Nested maps viper.Set("server", map[string]interface{}{ "database": map[string]interface{}{ "host": "localhost", "port": 5432, }, }) // Access nested host := viper.GetString("server.database.host") // Get sub-tree dbConfig := viper.Sub("server.database") if dbConfig != nil { host := dbConfig.GetString("host") }
Config File Formats
YAML
config.yaml:
server: port: 8080 host: localhost timeout: 30s database: host: localhost port: 5432 name: myapp user: postgres password: secret log: level: info format: json output: stdout features: enabled: - feature1 - feature2
JSON
config.json:
{ "server": { "port": 8080, "host": "localhost" }, "database": { "host": "localhost", "port": 5432 } }
TOML
config.toml:
[server] port = 8080 host = "localhost" [database] host = "localhost" port = 5432 name = "myapp"
Best Practices
1. Always Set Defaults
func init() { viper.SetDefault("server.port", 8080) viper.SetDefault("log.level", "info") }
2. Use Environment Variables
viper.AutomaticEnv() viper.SetEnvPrefix("MYAPP") // Now MYAPP_SERVER_PORT overrides config
3. Validate Config
type Config struct { Port int `validate:"required,min=1,max=65535"` } if err := validate.Struct(cfg); err != nil { return err }
4. Provide Config Commands
myapp config init # Create default config myapp config show # Show current config myapp config validate # Validate config
5. Handle Missing Config Gracefully
if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Config not found, use defaults log.Println("No config file found, using defaults") } else { return err } }
6. Don't Store Secrets in Config
// ❌ BAD database: password: "mysecret" // ✅ GOOD - Use env vars database: password: ${DB_PASSWORD} // Or viper.BindEnv("database.password", "DB_PASSWORD")
7. Use Struct Tags
type ServerConfig struct { Port int `mapstructure:"port" json:"port" yaml:"port"` Host string `mapstructure:"host" json:"host" yaml:"host"` Timeout string `mapstructure:"timeout" json:"timeout" yaml:"timeout"` }
Common Patterns
Config Init Command
func initConfigCmd() *cobra.Command { var force bool cmd := &cobra.Command{ Use: "init", Short: "Initialize configuration", RunE: func(cmd *cobra.Command, args []string) error { configPath := viper.ConfigFileUsed() if configPath == "" { configPath = filepath.Join(os.Getenv("HOME"), ".myapp", "config.yaml") } // Check if exists if _, err := os.Stat(configPath); err == nil && !force { return fmt.Errorf("config already exists: %s (use --force to overwrite)", configPath) } // Create directory if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { return err } // Write config if err := viper.WriteConfigAs(configPath); err != nil { return err } fmt.Printf("Config initialized: %s\n", configPath) return nil }, } cmd.Flags().BoolVar(&force, "force", false, "Overwrite existing config") return cmd }
Config Migration
func migrateConfig() error { version := viper.GetInt("version") switch version { case 0: // Migrate from v0 to v1 viper.Set("new_field", "default") viper.Set("version", 1) fallthrough case 1: // Migrate from v1 to v2 viper.Set("another_field", true) viper.Set("version", 2) } return viper.WriteConfig() }
Testing
func TestConfig(t *testing.T) { // Use separate viper instance v := viper.New() v.SetConfigType("yaml") var yamlConfig = []byte(` server: port: 8080 host: localhost `) v.ReadConfig(bytes.NewBuffer(yamlConfig)) assert.Equal(t, 8080, v.GetInt("server.port")) assert.Equal(t, "localhost", v.GetString("server.host")) }
Checklist
- Defaults set for all config values
- Config file search paths defined
- Environment variable support
- Flags bound to config
- Config struct with mapstructure tags
- Config validation
- Config commands (init, show, validate)
- Error handling for missing config
- Secrets via env vars only
- Config file format documented
Resources
- Viper Documentation
- Cobra User Guide
- 12-Factor Config
- CLY config:
,pkg/config/modules/config/