cli-configuration

CLI Configuration Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cli-configuration" with this command: npx skills add geoffjay/claude-plugins/geoffjay-claude-plugins-cli-configuration

CLI Configuration Skill

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

Configuration Precedence

The standard precedence order (lowest to highest priority):

  • Compiled defaults - Hard-coded sensible defaults

  • System config - /etc/myapp/config.toml

  • User config - ~/.config/myapp/config.toml

  • Project config - ./myapp.toml or ./.myapp.toml

  • Environment variables - MYAPP_KEY=value

  • 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) = &#x26;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(&#x26;self) -> PathBuf {
    self.config_dir.join("config.toml")
}

pub fn ensure_dirs(&#x26;self) -> Result&#x3C;()> {
    fs::create_dir_all(&#x26;self.config_dir)?;
    fs::create_dir_all(&#x26;self.data_dir)?;
    fs::create_dir_all(&#x26;self.cache_dir)?;
    fs::create_dir_all(&#x26;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: &#x26;Path) -> Result&#x3C;()> {
    let config = Self::default_config();
    let toml = toml::to_string_pretty(&#x26;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(&#x26;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

  • Provide sensible defaults - App should work out-of-box

  • Document precedence - Make override behavior clear

  • Validate early - Catch config errors at startup

  • Use XDG directories - Follow platform conventions

  • Support env vars - Essential for containers/CI

  • Generate defaults - Help users get started

  • Version config format - Enable migrations

  • Keep secrets out - Use env vars for sensitive data

  • Clear error messages - Help users fix config issues

  • Document all options - With examples and defaults

References

  • XDG Base Directory Specification

  • The Twelve-Factor App: Config

  • directories crate

  • config crate

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cli-ux-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

cli-distribution

No summary provided by upstream source.

Repository SourceNeeds Review
General

documentation-update

No summary provided by upstream source.

Repository SourceNeeds Review