Claude Code Plugins

Community-maintained marketplace

Feedback

cli-config

@yurifrl/cly
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name cli-config
description 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.

CLI 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):

  1. Explicit viper.Set() calls
  2. Command-line flags
  3. Environment variables
  4. Config file values
  5. 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