# How Configuration Files Are Managed in the Brush Project: A Typed, Layered Approach

> Discover how the Brush project manages configuration files using a typed, layered approach. Learn how clap and serde structs aggregate settings from files and CLI arguments for flexible control.

- Repository: [Arthur Brussee/brush](https://github.com/ArthurBrussee/brush)
- Tags: how-to-guide
- Published: 2026-05-14

---

**The Brush project manages configuration files through a hierarchy of `clap` and `serde`-enabled structs that aggregate settings from an optional [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) file and command-line arguments, with CLI flags always taking precedence.**

Managing training parameters for 3D Gaussian splatting requires a flexible system that works across local directories and virtual file systems. The ArthurBrussee/brush repository solves this by implementing a type-safe configuration management system that unifies file-based settings with runtime CLI overrides. This approach allows users to store baseline configurations in dataset folders while retaining granular control via command-line flags.

## Typed Configuration Structs with Clap and Serde

Brush organizes its settings into specialized configuration structs that are simultaneously parsable as CLI arguments and serializable to disk. Each struct uses `#[derive(Args, Serialize, Deserialize)]` to enable this dual representation.

### Process-Wide Options

The `ProcessConfig` struct in [`crates/brush-process/src/config.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs) defines global settings like random seeds, evaluation intervals, and export paths:

```rust
#[derive(Clone, Args, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ProcessConfig {
    #[arg(long, default_value = "42")] pub seed: u64,
    #[arg(long, default_value = "0")]   pub start_iter: u32,
    #[arg(long, default_value = "1000")] pub eval_every: u32,
    #[arg(long, default_value = "false")] pub eval_save_to_disk: bool,
    #[arg(long, default_value = "5000")] pub export_every: u32,
    #[arg(long, default_value = "./{dataset}_exports/")] pub export_path: String,
    #[arg(long, default_value = "export_{iter}.ply")] pub export_name: String,
}

```

*Source:* [[`crates/brush-process/src/config.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs)](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs#L4-L38)

### Training and Dataset Configurations

Training-specific parameters live in [`crates/brush-train/src/config.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-train/src/config.rs) within the `TrainConfig` struct, while dataset loading options reside in [`crates/brush-dataset/src/config.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-dataset/src/config.rs) as `LoadDataseConfig`. Both follow the same pattern of deriving `Parser`, `Serialize`, and `Deserialize`.

### The Unified TrainStreamConfig

Individual configuration structs are flattened into a single `TrainStreamConfig` struct, which serves as the canonical representation of all runtime settings:

```rust
#[derive(Parser, Clone, Serialize, Deserialize)]
pub struct TrainStreamConfig {
    #[clap(flatten)] #[serde(flatten)] pub train_config: brush_train::config::TrainConfig,
    #[clap(flatten)] #[serde(flatten)] pub model_config: brush_dataset::config::ModelConfig,
    #[clap(flatten)] #[serde(flatten)] pub load_config:  brush_dataset::config::LoadDataseConfig,
    #[clap(flatten)] #[serde(flatten)] pub process_config: ProcessConfig,
    #[clap(flatten)] #[serde(flatten)] pub rerun_config: brush_rerun::RerunConfig,
}

```

*Source:* [[`crates/brush-process/src/config.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs)](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/config.rs#L40-L58)

## Loading Configuration from args.txt

Brush reads baseline settings from an [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) file located in the dataset root or virtual file system (VFS). The `load_config_from_vfs` function in [`crates/brush-process/src/args_file.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs) handles this by treating the file content as CLI arguments:

```rust
pub async fn load_config_from_vfs(vfs: &BrushVfs) -> Option<TrainStreamConfig> {
    // … check presence of args.txt …
    let mut reader = vfs.reader_at_path(Path::new("args.txt")).await.ok()?;
    let mut content = String::new();
    reader.read_to_string(&mut content).await.ok()?;
    let file_args = split_args_str(&content);
    let mut all_args = vec!["brush".to_owned()];
    all_args.extend(file_args);
    TrainStreamConfig::try_parse_from(&all_args).ok()
}

```

*Source:* [[`crates/brush-process/src/args_file.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs)](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L13-L48)

The file uses the same flag syntax as the CLI, such as `--total-train-iters 5000` or `--export-path "./custom/"`.

## Merging File-Based and CLI Arguments

Configuration precedence follows a simple rule: **CLI arguments always override file-based settings**. The `merge_configs` function implements this by concatenating the argument vectors and re-parsing them through `clap`:

```rust
pub fn merge_configs(initial: &TrainStreamConfig,
                    cli: &TrainStreamConfig) -> TrainStreamConfig {
    let initial_args = config_to_args(initial);
    let cli_args     = config_to_args(cli);
    let mut all_args = vec!["brush".to_owned()];
    for a in initial_args.iter().chain(cli_args.iter()) {
        all_args.extend(a.split_whitespace().map(|s| s.to_owned()));
    }
    TrainStreamConfig::try_parse_from(&all_args).unwrap_or_else(|_| cli.clone())
}

```

*Source:* [[`crates/brush-process/src/args_file.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs)](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L112-L138)

This approach ensures that users can maintain a baseline [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) in their dataset folder while running quick experiments with ad-hoc CLI overrides.

## Serializing Configurations Back to Disk

Brush supports round-tripping configurations back to disk via the `config_to_args` function. To keep files concise, it only serializes values that differ from the defaults:

```rust
pub fn config_to_args(config: &TrainStreamConfig) -> Vec<String> {
    let config_json  = serde_json::to_value(config).unwrap();
    let default_json = serde_json::to_value(TrainStreamConfig::default()).unwrap();

    // Walk the two JSON objects, output `--key value` if the value differs.
    // Handles bools, strings, numbers and multi‑value arrays.
    // …
}

```

*Source:* [[`crates/brush-process/src/args_file.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs)](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs#L51-L88)

This allows the application to persist the current effective configuration back to [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) after a training run, creating a reproducible record of the exact parameters used.

## End-to-End Configuration Flow

The entry point in [`crates/brush-process/src/lib.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/lib.rs) orchestrates the loading sequence:

```rust
let vfs = BrushVfs::from_path(&dataset_dir).await?;
let file_cfg = load_config_from_vfs(&vfs).await.unwrap_or_default();
let cli_cfg  = TrainStreamConfig::parse();        // clap parses the CLI
let cfg      = merge_configs(&file_cfg, &cli_cfg);

```

*Source:* [`brush-process/src/lib.rs`](https://github.com/ArthurBrussee/brush/blob/main/brush-process/src/lib.rs)

This flow demonstrates how Brush abstracts configuration files through its virtual file system (`BrushVfs` in [`crates/brush-vfs/src/lib.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-vfs/src/lib.rs)), enabling the same code to operate on local directories, zip archives, or WASM file systems.

## Summary

- **Configuration files in Brush use typed structs** that derive both `clap::Args` and `serde` traits, enabling simultaneous CLI parsing and disk serialization.
- **The `TrainStreamConfig` struct aggregates** all sub-configurations (training, dataset, process, and Rerun) into a single canonical representation.
- **Settings load from an optional [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt)** file using the same syntax as CLI arguments, read through the `BrushVfs` abstraction.
- **CLI arguments override file settings** via the `merge_configs` function, which re-parses combined argument lists through `clap`.
- **Round-tripping is supported** through `config_to_args`, which serializes only non-default values to maintain concise configuration files.

## Frequently Asked Questions

### Where does Brush store its configuration files?

Brush stores runtime settings in an [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) file located in the dataset root directory or virtual file system root. This file contains CLI-style flags such as `--total-train-iters 5000` and is read at startup by the `load_config_from_vfs` function in [`crates/brush-process/src/args_file.rs`](https://github.com/ArthurBrussee/brush/blob/main/crates/brush-process/src/args_file.rs).

### How does Brush handle configuration precedence between files and CLI?

Brush merges configurations by converting both the file-based settings and CLI arguments back into argument vectors, concatenating them, and re-parsing through `clap`. Because CLI arguments appear later in the sequence, they automatically override any duplicate keys from the [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) file.

### Can Brush serialize the current configuration back to disk?

Yes. The `config_to_args` function converts a `TrainStreamConfig` instance back into a vector of CLI arguments by comparing it against the default configuration using `serde_json`. It only emits flags for values that differ from defaults, keeping the resulting [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) file minimal and human-readable.

### What makes Brush's configuration system type-safe?

Each configuration field is defined as a strongly-typed struct member with explicit types like `u64`, `u32`, or `String`, combined with `clap` derive macros for validation. This ensures that invalid values in [`args.txt`](https://github.com/ArthurBrussee/brush/blob/main/args.txt) are caught at parse time, and the compiler enforces that all required parameters are present before training begins.