NYC
skills/geoffjay/claude-plugins/cli-configuration

cli-configuration

SKILL.md

CLI Configuration Skill

Patterns and best practices for managing configuration in command-line applications.

Configuration Precedence

The standard precedence order (lowest to highest priority):

  1. Compiled defaults - Hard-coded sensible defaults
  2. System config - /etc/myapp/config.toml
  3. User config - ~/.config/myapp/config.toml
  4. Project config - ./myapp.toml or ./.myapp.toml
  5. Environment variables - MYAPP_KEY=value
  6. CLI arguments - --key value (highest priority)
use config::{Config as ConfigBuilder, Environment, File};

pub fn load_config(cli: &Cli) -> Result<Config> {
    let mut builder = ConfigBuilder::builder()
        // 1. Defaults
        .set_default("port", 8080)?
        .set_default("host", "localhost")?
        .set_default("log_level", "info")?;

    // 2. System config (if exists)
    builder = builder
        .add_source(File::with_name("/etc/myapp/config").required(false));

    // 3. User config (if exists)
    if let Some(config_dir) = dirs::config_dir() {
        builder = builder.add_source(
            File::from(config_dir.join("myapp/config.toml")).required(false)
        );
    }

    // 4. Project config (if exists)
    builder = builder
        .add_source(File::with_name("myapp").required(false))
        .add_source(File::with_name(".myapp").required(false));

    // 5. CLI-specified config (if provided)
    if let Some(config_path) = &cli.config {
        builder = builder.add_source(File::from(config_path.as_ref()));
    }

    // 6. Environment variables
    builder = builder.add_source(
        Environment::with_prefix("MYAPP")
            .separator("_")
            .try_parsing(true)
    );

    // 7. CLI arguments (highest priority)
    if let Some(port) = cli.port {
        builder = builder.set_override("port", port)?;
    }

    Ok(builder.build()?.try_deserialize()?)
}

Config File Formats

TOML (Recommended)

Clear, human-readable, good error messages.

# config.toml
[general]
port = 8080
host = "localhost"
log_level = "info"

[database]
url = "postgresql://localhost/mydb"
pool_size = 10

[features]
caching = true
metrics = false

[[servers]]
name = "primary"
address = "192.168.1.1"

[[servers]]
name = "backup"
address = "192.168.1.2"
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct Config {
    general: General,
    database: Database,
    features: Features,
    servers: Vec<Server>,
}

#[derive(Debug, Deserialize, Serialize)]
struct General {
    port: u16,
    host: String,
    log_level: String,
}

YAML (Alternative)

More concise, supports comments, complex structures.

# config.yaml
general:
  port: 8080
  host: localhost
  log_level: info

database:
  url: postgresql://localhost/mydb
  pool_size: 10

features:
  caching: true
  metrics: false

servers:
  - name: primary
    address: 192.168.1.1
  - name: backup
    address: 192.168.1.2

JSON (Machine-Readable)

Good for programmatic generation, less human-friendly.

{
  "general": {
    "port": 8080,
    "host": "localhost",
    "log_level": "info"
  },
  "database": {
    "url": "postgresql://localhost/mydb",
    "pool_size": 10
  }
}

XDG Base Directory Support

Follow the XDG Base Directory specification for cross-platform compatibility.

use directories::ProjectDirs;

pub struct AppPaths {
    pub config_dir: PathBuf,
    pub data_dir: PathBuf,
    pub cache_dir: PathBuf,
    pub state_dir: PathBuf,
}

impl AppPaths {
    pub fn new(app_name: &str) -> Result<Self> {
        let proj_dirs = ProjectDirs::from("com", "example", app_name)
            .ok_or_else(|| anyhow!("Could not determine project directories"))?;

        Ok(Self {
            config_dir: proj_dirs.config_dir().to_path_buf(),
            data_dir: proj_dirs.data_dir().to_path_buf(),
            cache_dir: proj_dirs.cache_dir().to_path_buf(),
            state_dir: proj_dirs.state_dir()
                .unwrap_or_else(|| proj_dirs.data_dir())
                .to_path_buf(),
        })
    }

    pub fn config_file(&self) -> PathBuf {
        self.config_dir.join("config.toml")
    }

    pub fn ensure_dirs(&self) -> Result<()> {
        fs::create_dir_all(&self.config_dir)?;
        fs::create_dir_all(&self.data_dir)?;
        fs::create_dir_all(&self.cache_dir)?;
        fs::create_dir_all(&self.state_dir)?;
        Ok(())
    }
}

Directory locations by platform:

Platform Config Data Cache
Linux ~/.config/myapp ~/.local/share/myapp ~/.cache/myapp
macOS ~/Library/Application Support/myapp ~/Library/Application Support/myapp ~/Library/Caches/myapp
Windows %APPDATA%\example\myapp %APPDATA%\example\myapp %LOCALAPPDATA%\example\myapp

Environment Variable Patterns

Naming Convention

Use APPNAME_SECTION_KEY format:

MYAPP_DATABASE_URL=postgresql://localhost/db
MYAPP_LOG_LEVEL=debug
MYAPP_FEATURES_CACHING=true
MYAPP_PORT=9000

Integration with Clap

#[derive(Parser)]
struct Cli {
    /// Database URL (env: MYAPP_DATABASE_URL)
    #[arg(long, env = "MYAPP_DATABASE_URL")]
    database_url: Option<String>,

    /// Log level (env: MYAPP_LOG_LEVEL)
    #[arg(long, env = "MYAPP_LOG_LEVEL", default_value = "info")]
    log_level: String,

    /// Port (env: MYAPP_PORT)
    #[arg(long, env = "MYAPP_PORT", default_value = "8080")]
    port: u16,
}

Sensitive Data Pattern

Never put secrets in config files. Use environment variables instead.

#[derive(Debug, Deserialize)]
struct Config {
    pub host: String,
    pub port: u16,

    // Loaded from environment only
    #[serde(skip)]
    pub api_token: String,
}

impl Config {
    pub fn load() -> Result<Self> {
        let mut config: Config = /* load from file */;

        // Sensitive data from env only
        config.api_token = env::var("MYAPP_API_TOKEN")
            .context("MYAPP_API_TOKEN environment variable required")?;

        Ok(config)
    }
}

Configuration Validation

Validate configuration early at load time:

#[derive(Debug, Deserialize)]
struct Config {
    pub port: u16,
    pub host: String,
    pub workers: usize,
}

impl Config {
    pub fn validate(&self) -> Result<()> {
        // Port range
        if !(1024..=65535).contains(&self.port) {
            bail!("Port must be between 1024 and 65535, got {}", self.port);
        }

        // Workers
        if self.workers == 0 {
            bail!("Workers must be at least 1");
        }

        let max_workers = num_cpus::get() * 2;
        if self.workers > max_workers {
            bail!(
                "Workers ({}) exceeds recommended maximum ({})",
                self.workers,
                max_workers
            );
        }

        // Host validation
        if self.host.is_empty() {
            bail!("Host cannot be empty");
        }

        Ok(())
    }
}

Generating Default Config

Provide a command to generate a default configuration file:

impl Config {
    pub fn default_config() -> Self {
        Self {
            general: General {
                port: 8080,
                host: "localhost".to_string(),
                log_level: "info".to_string(),
            },
            database: Database {
                url: "postgresql://localhost/mydb".to_string(),
                pool_size: 10,
            },
            features: Features {
                caching: true,
                metrics: false,
            },
        }
    }

    pub fn write_default(path: &Path) -> Result<()> {
        let config = Self::default_config();
        let toml = toml::to_string_pretty(&config)?;

        // Add helpful comments
        let content = format!(
            "# Configuration file for myapp\n\
             # See: https://example.com/docs/config\n\n\
             {toml}"
        );

        fs::write(path, content)?;
        Ok(())
    }
}

CLI Command:

#[derive(Subcommand)]
enum Commands {
    /// Generate a default configuration file
    InitConfig {
        /// Output path (default: ~/.config/myapp/config.toml)
        #[arg(short, long)]
        output: Option<PathBuf>,
    },
}

fn handle_init_config(output: Option<PathBuf>) -> Result<()> {
    let path = output.unwrap_or_else(|| {
        AppPaths::new("myapp")
            .unwrap()
            .config_file()
    });

    if path.exists() {
        bail!("Config file already exists: {}", path.display());
    }

    Config::write_default(&path)?;
    println!("Created config file: {}", path.display());
    Ok(())
}

Config Migration Pattern

Handle breaking changes in config format:

#[derive(Debug, Deserialize)]
struct ConfigV2 {
    version: u32,
    #[serde(flatten)]
    data: ConfigData,
}

impl ConfigV2 {
    pub fn load(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path)?;
        let mut config: ConfigV2 = toml::from_str(&content)?;

        // Migrate from older versions
        match config.version {
            1 => {
                eprintln!("Migrating config from v1 to v2...");
                config = migrate_v1_to_v2(config)?;
                // Optionally save migrated config
                config.save(path)?;
            }
            2 => {}, // Current version
            v => bail!("Unsupported config version: {}", v),
        }

        Ok(config)
    }
}

Configuration Examples Command

Provide examples in help text:

#[derive(Subcommand)]
enum Commands {
    /// Show configuration examples
    ConfigExamples,
}

fn show_config_examples() {
    println!("Configuration Examples:\n");

    println!("1. Basic configuration (config.toml):");
    println!("{}", r#"
[general]
port = 8080
host = "localhost"
"#);

    println!("\n2. Environment variables:");
    println!("   MYAPP_PORT=9000");
    println!("   MYAPP_DATABASE_URL=postgresql://localhost/db");

    println!("\n3. CLI override:");
    println!("   myapp --port 9000 --host 0.0.0.0");

    println!("\n4. Precedence (highest to lowest):");
    println!("   CLI args > Env vars > Config file > Defaults");
}

Best Practices

  1. Provide sensible defaults - App should work out-of-box
  2. Document precedence - Make override behavior clear
  3. Validate early - Catch config errors at startup
  4. Use XDG directories - Follow platform conventions
  5. Support env vars - Essential for containers/CI
  6. Generate defaults - Help users get started
  7. Version config format - Enable migrations
  8. Keep secrets out - Use env vars for sensitive data
  9. Clear error messages - Help users fix config issues
  10. Document all options - With examples and defaults

References

Weekly Installs
9
First Seen
Jan 24, 2026
Installed on
gemini-cli7
claude-code7
antigravity6
windsurf6
codex6
opencode5