# ai-memory-cli Config::load() Path: Why Single-Pass Configuration Loading Matters

> Understand the ai-memory-cli's single config::Config::load path. Discover how this method prevents environment desynchronization bugs by merging settings efficiently.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: internals
- Published: 2026-08-30

---

**The ai-memory-cli uses a single `config::Config::load()` call at startup that merges defaults, a TOML config file, and environment variables in one pass, with no second read path to prevent environment desynchronization bugs.**

The ai-memory CLI from the `akitaonrails/ai-memory` repository implements a strict **single-read configuration invariant**. Every setting is resolved exactly once when the binary starts, producing an immutable `Config` value that remains unchanged for the program's entire lifetime. This design directly addresses a class of bugs where scattered environment reads caused inconsistent behavior across components.

## How Config::load() Resolves the Configuration Path

The `Config::load()` function in [`crates/ai-memory-cli/src/config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/config.rs) (lines 75-106) follows a deterministic, layered resolution strategy:

### Step 1: Determine the Data Directory

The loader prioritizes sources in this order:

- **CLI flag** `--data-dir` (`cli_data_dir` parameter) takes highest precedence
- **Environment variable** `AI_MEMORY_DATA_DIR` (captured via `RuntimeEnv::data_dir`) if no flag provided
- **Built-in default** from `default_data_dir()` as final fallback

### Step 2: Resolve the Config File Path

With the data directory established, the loader constructs the final path:

- Explicit `--config` flag (`config_path` parameter) overrides everything
- Otherwise defaults to `<data_dir>/config.toml`

### Step 3: Merge Configuration Layers Using Figment

The implementation uses the **Figment** library to merge three sources in strict order:

```rust
use figment::{Figment, providers::{Serialized, Toml, Env}};

let figment = Figment::from(Serialized::defaults(Self::default()))
    .merge(Toml::file(&resolved_config_path))  // only if file exists
    .merge(Env::prefixed("AI_MEMORY_").split("__"));

```

This merging order ensures that **environment variables override file settings**, which in turn override compiled defaults.

### Step 4: Inject Runtime-Only Values

After extracting the merged `Config`, the loader performs **exactly one** read of process-specific environment variables:

```rust
// From config.rs — these values are captured once and never re-read
config.auth.bearer_token = runtime_env.auth_token;      // AI_MEMORY_AUTH_TOKEN
config.server_url = runtime_env.server_url;             // AI_MEMORY_SERVER_URL
config.home_dir = Some(runtime_env.home_dir);           // canonicalized
config.data_dir = data_dir.canonicalize()?;

```

### Step 5: Validate and Return

Finally, `Config::load()` enforces numeric invariants (decay weights, LLM token limits) and returns either a fully-populated, immutable `Config` or an early error.

## Why No Second Config-Read Path Exists

The comment at the top of [`config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/config.rs) explicitly documents this architectural decision (lines 3-7):

```rust
//! All settings are read exactly once at startup, merged into a single
//! immutable [`Config`] value, and passed by reference everywhere. There is
//! no second read path (lesson from agentmemory #456 / #469 — the dimension
//! guard read `process.env` while the rest of the codebase used
//! `getMergedEnv()`, masking the bug for weeks).

```

### The Historical Bug That Motivated This Design

Issues **#456** and **#469** in the related `agentmemory` project revealed a critical failure mode: one component read `process.env` directly while others used a merged configuration view. This desynchronization masked bugs for weeks because tests passed in isolation but production environments exhibited inconsistent behavior.

### Four Architectural Guarantees

| Guarantee | Mechanism | Benefit |
|-----------|-----------|---------|
| **Consistency** | `RuntimeEnv::from_process()` captures all environment variables in one call | Every component sees identical values even if the external environment changes |
| **Predictability** | Single, well-defined merge order (defaults → file → environment) | No accidental overrides from late code re-reading configuration |
| **Performance** | One-time I/O and parsing | Fast startup for CLI subcommands |
| **Thread Safety** | Immutable `Config` struct | Lock-free sharing across threads |

## Practical Usage Example

Here's how the ai-memory-cli binary invokes the single-pass loader:

```rust
use ai_memory_cli::config::Config;
use std::path::Path;

fn main() -> anyhow::Result<()> {
    // Parsed from clap arguments
    let cli_data_dir: Option<&Path> = None;  // from --data-dir
    let config_path: Option<&Path> = None;   // from --config

    // SINGLE LOAD — this is the only configuration read in the entire program
    let cfg = Config::load(config_path, cli_data_dir)?;

    println!("Data directory: {}", cfg.data_dir.display());
    println!("Server URL: {}", cfg.server_url);
    
    // Runtime values injected during load, never re-read
    if let Some(home) = cfg.home_dir.as_deref() {
        println!("Home: {}", home);
    }
    Ok(())
}

```

## Key Source Files

- **[`crates/ai-memory-cli/src/config.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/config.rs)** — `Config::load()` implementation and `RuntimeEnv` struct (lines 45-87 and 75-106)
- **[`crates/ai-memory-cli/src/cli.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/cli.rs)** — CLI argument parsing that feeds `--data-dir` and `--config` into the loader

## Summary

- The `config::Config::load()` path in ai-memory-cli merges **defaults → TOML file → environment variables** in a single pass using Figment
- **No second read path exists** to prevent the environment desynchronization bugs that plagued earlier projects (#456, #469)
- Runtime-only values (`AI_MEMORY_AUTH_TOKEN`, `AI_MEMORY_SERVER_URL`, `HOME`) are captured **exactly once** via `RuntimeEnv::from_process()`
- The resulting immutable `Config` eliminates race conditions, ensures testability, and provides predictable behavior across all CLI subcommands

## Frequently Asked Questions

### What happens if the configuration file doesn't exist?

The loader continues without error. `Toml::file()` in Figment is non-failing for missing files—the merge simply proceeds with defaults and environment variables. This supports zero-configuration deployments where all settings come from environment variables.

### Can I reload configuration without restarting ai-memory-cli?

No. The single-read invariant is architectural, not optional. To change configuration, you must restart the process. This trade-off prioritizes consistency over dynamic reconfiguration, which aligns with the CLI's short-lived process model.

### Why use Figment instead of simpler alternatives like envy or config-rs?

Figment provides the precise layering semantics (defaults → file → environment) and prefix-based environment variable parsing (`AI_MEMORY_*` with `__` delimiter for nested keys) that `ai-memory-cli` requires. It also gracefully handles missing files without custom logic.