# How SXO Resolves Configuration Precedence Between CLI Flags, Environment Variables, and Config Files

> Learn how SXO efficiently resolves configuration precedence. Discover its strict order for CLI flags, environment variables, and config files to ensure the first defined value wins.

- Repository: [Víctor García/sxo](https://github.com/gc-victor/sxo)
- Tags: internals
- Published: 2026-03-02

---

**SXO resolves configuration precedence by merging four sources in strict order—explicit CLI flags override config files, which override environment variables, which override built-in defaults—using the `pickDefined` helper in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js) to ensure the first defined value wins.**

SXO (Simple eXtensible Organizer) is an open-source CLI tool that requires flexible configuration management across multiple sources. Understanding how SXO resolves configuration precedence between flags, environment variables, and config files is essential for debugging deployment issues and managing complex development workflows.

## The Four-Layer Configuration Hierarchy

SXO builds its final configuration by evaluating four distinct layers. When the same option appears in multiple layers, the value from the highest-priority layer wins.

### 1. Explicit Command-Line Flags (Highest Priority)

Flags typed directly into the terminal take precedence over all other sources. SXO tracks which flags were explicitly provided versus which hold default values, ensuring that only user-supplied arguments override lower layers.

### 2. User Configuration Files

The user config file (`sxo.config.{mjs|js|cjs|json}`) provides project-level defaults. SXO searches for and loads this file in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js) via the `loadUserConfig` function (lines 68‑112), merging its exports into the configuration stack.

### 3. Environment Variables

Environment variables from `process.env` sit below config files in the hierarchy. Before loading user configs, SXO invokes `loadDotenv` (lines 57‑66) to populate `process.env` from `.env` and `.env.local` files without overwriting existing environment values.

### 4. Built-in Command Defaults (Lowest Priority)

Each SXO command (`dev`, `build`, etc.) defines its own sensible defaults. These defaults only apply if no higher layer provides a value for a given option.

## How the Merge Logic Works in src/js/config.js

The resolution engine lives in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js), where the `resolveConfig` function orchestrates the merge process.

### Loading Environment and Config Files

First, SXO prepares the ground layers:

```javascript
// Lines 57-66: Dotenv loading without clobbering existing env vars
await loadDotenv();

// Lines 68-112: User config loading with format detection (mjs/js/cjs/json)
const fileConfig = await loadUserConfig();

```

### Normalizing and Filtering Explicit Flags

Before merging, SXO normalizes each layer to ensure type consistency. The critical step is **explicit flag filtering** (lines 94‑102):

```javascript
// Only flags actually typed on the CLI are allowed to override lower layers
const flagsExplicit = {};
for (const key of Object.keys(flags)) {
  if (flags[key] !== undefined && /* flag was explicitly provided */) {
    flagsExplicit[key] = flags[key];
  }
}

```

This prevents default flag values (e.g., `--open` defaulting to `true` for the `dev` command) from unintentionally stomping on user config or environment settings.

### The Precedence Merge with pickDefined

The final merge occurs in `normalizeConfig` (lines 109‑122) using the `pickDefined` helper:

```javascript
// Precedence: flags → file → env → defaults
const resolved = {
  port: pickDefined(flagsExplicit.port, fileConfig.port, env.PORT, defaults.port),
  publicPath: pickDefined(flagsExplicit.publicPath, fileConfig.publicPath, env.PUBLIC_PATH, defaults.publicPath),
  // ... additional options
};

```

The `pickDefined` function returns the first argument that is not `undefined`, effectively implementing the priority stack.

### Final Adjustments

After merging, SXO applies post-processing (lines 124‑140) such as port clamping, path normalization, and default `open` handling to ensure the configuration is valid and secure.

## Practical Configuration Resolution Example

Consider a scenario where the same option is defined in multiple places:

```bash

# 1. Environment variables (lowest active priority)

export PORT=4000
export PUBLIC_PATH=/static/

# 2. Config file (higher priority than env)

# sxo.config.js

module.exports = {
  port: 5000,
  publicPath: "/assets/"
};

# 3. CLI flags (highest priority)

sxo dev --port 6000 --public-path /custom/

```

The resolved configuration would be:

- **port**: `6000` (from explicit CLI flag)
- **publicPath**: `/custom/` (from explicit CLI flag)
- **open**: `true` (command default for `dev`, since no override provided)

You can inspect the final resolved configuration using `sxo dev --print-config` or by examining the output of `resolveConfig` in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js).

## Summary

- SXO uses a four-layer precedence stack: **CLI flags > Config files > Environment variables > Built-in defaults**.
- The merge logic is centralized in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js), specifically within the `resolveConfig` and `normalizeConfig` functions.
- **Explicit flag filtering** prevents default CLI values from overriding user configs or environment variables.
- The `pickDefined` helper selects the first defined value from the precedence chain (flags → file → env → defaults).
- Dotenv files (`.env`, `.env.local`) are loaded early but do not overwrite existing `process.env` values.

## Frequently Asked Questions

### What happens if I specify the same option in both a config file and as a CLI flag?

The CLI flag always wins. According to the precedence logic in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js) (lines 109‑122), explicit flags are checked first via `pickDefined`, so `--port 3000` overrides `port: 4000` in [`sxo.config.js`](https://github.com/gc-victor/sxo/blob/main/sxo.config.js).

### Does SXO support .env files for local development overrides?

Yes. SXO loads `.env` and `.env.local` files via the `loadDotenv` function (lines 57‑66 in [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js)). These files populate `process.env` without overwriting existing environment variables, placing them below config files but above built-in defaults in the precedence stack.

### How does SXO handle boolean flags that have default values?

SXO uses **explicit flag filtering** to prevent boolean defaults from stomping on user configuration. In [`src/js/config.js`](https://github.com/gc-victor/sxo/blob/main/src/js/config.js) (lines 94‑102), only flags actually typed on the CLI are added to `flagsExplicit`. This ensures that a default like `--open true` for the `dev` command does not override an `open: false` setting in your config file or `OPEN=false` environment variable.

### Can I see the final resolved configuration without running the command?

Yes. You can inspect the fully resolved configuration by running your command with the `--print-config` flag (e.g., `sxo dev --print-config`). This outputs the result of the `resolveConfig` function after all layers have been merged and normalized, showing exactly which source won for each option.