# Configuration file support

- **Pattern:** `ab-000659` (`cli-quality.config-distribution.config-file`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000659
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000659)

## Why it matters

Without a config file, users must pass `--api-key`, `--format`, `--output-dir`, and `--region` on every single invocation. That friction drives them toward shell aliases that are fragile, `.env` files that aren't portable, and wrapper scripts that become undocumented infrastructure. ISO 25010:2011 usability.operability scores CLIs on whether persistent preferences can be set once and forgotten. A CLI that requires 6 flags per invocation is not operationally usable in real workflows — teams will script around it rather than into it, breaking the tool's intended composability.

## Severity rationale

Medium because the absence of config file support imposes per-invocation repetition that degrades usability in team workflows without causing data loss or security failures.

## Remediation

Add config file discovery using `cosmiconfig` (Node.js) or a standard `~/.config` location (Python/Go). CLI flags must always override config file values:

```ts
import { cosmiconfig } from 'cosmiconfig'

const explorer = cosmiconfig('mytool')
const result = await explorer.search()   // finds .mytoolrc, mytool.config.js, etc.
const fileConfig = result?.config ?? {}

// CLI flags override config — spread order matters
const config = { ...fileConfig, ...cliFlags }
```

```python
import tomllib
from pathlib import Path

def load_config() -> dict:
    for loc in [Path('mytool.toml'), Path.home() / '.config' / 'mytool' / 'config.toml']:
        if loc.exists():
            with open(loc, 'rb') as f:
                return tomllib.load(f)
    return {}  # no config file is not an error
```

Document which config file locations are checked in `--help` output or a `mytool config --locations` subcommand.

## Detection

- **ID:** `config-file`
- **Severity:** `medium`
- **What to look for:** Enumerate all configuration sources (config files, environment variables, CLI flags). For each, check for config file support: cosmiconfig, rc, configparser, viper, config crate. Look for config file loading from standard locations: `.mytoolrc`, `.mytoolrc.json`, `mytool.config.js`, `~/.config/mytool/config.json`, or `pyproject.toml` `[tool.mytool]` section. Check that command-line flags override config file values. Check that a missing config file doesn't cause an error (defaults should be used).
- **Pass criteria:** The CLI supports a configuration file for persistent options. Config file values are overridable by command-line flags. Missing config file is handled gracefully (defaults used, no error). Config file locations follow platform conventions (XDG on Linux, `~/Library/Preferences` on macOS, or project-local dotfiles) — at least 1 configuration file format supported (JSON, YAML, TOML, or RC file). Report: "X configuration sources found."
- **Fail criteria:** No config file support — all options must be passed as flags every time, or config file is required (crashes if missing), or config file values cannot be overridden by flags.
- **Skip (N/A) when:** The CLI has 2 or fewer options total — config file overhead is not warranted. All checks skip when no CLI entry point is detected.
- **Detail on fail:** `"No configuration file support found — users must pass --api-key and --format on every invocation"` or `"Config file is loaded from hardcoded path '/etc/mytool.conf' — non-standard and requires root to edit"`
- **Remediation:** Config files save users from repetitive flag typing:

  ```ts
  // Node.js — cosmiconfig for standard config file discovery
  import { cosmiconfig } from 'cosmiconfig'

  const explorer = cosmiconfig('mytool')
  const configResult = await explorer.search()
  const fileConfig = configResult?.config ?? {}

  // CLI flags override config file values
  const config = {
    ...fileConfig,
    ...cliFlags,  // spread CLI flags last to override
  }
  ```

  ```python
  # Python — standard config locations
  from pathlib import Path
  import tomllib

  def load_config():
      locations = [
          Path('mytool.toml'),
          Path.home() / '.config' / 'mytool' / 'config.toml',
      ]
      for loc in locations:
          if loc.exists():
              with open(loc, 'rb') as f:
                  return tomllib.load(f)
      return {}  # defaults, no error
  ```

## External references

- iso-25010 usability.operability

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-000659
