Hugo Configuration Precedence: How CLI Flags, Environment Variables, and Config Files Merge
Hugo merges configuration from four hierarchical sources where environment variables override CLI flags, which override config files, which override hard-coded defaults.
Understanding how the gohugoio/hugo static site generator resolves conflicting configuration values is essential for managing deployments across development, staging, and production environments. The configuration loader in config/allconfig/load.go orchestrates a specific seven-step sequence that determines which value survives when the same key is defined in multiple places.
The Configuration Loading Pipeline
Hugo builds its final configuration provider through a deterministic merging process defined in the loadConfig function. The loader initializes an empty provider and progressively layers values, with later steps overwriting earlier ones.
Step 1: Default Values
The process begins with applyDefaultConfig, which injects hard-coded defaults for keys like themesDir, configDir, contentDir, and layoutDir. These values represent the baseline configuration and reside at the lowest precedence level.
// In config/allconfig/load.go
applyDefaultConfig(cfg)
Step 2: Configuration Files
Hugo discovers and loads hugo.toml, hugo.yaml, hugo.json, or any files under the config/ directory using config.FromFileToMap. When multiple files are specified via --config, they merge left-to-right, allowing subsequent files to overwrite keys from preceding ones.
hugo --config base.toml,production.toml
In this example, keys in production.toml override matching keys in base.toml.
Step 3: First Pass Environment Variables
The loader calls applyOsEnvOverrides to process HUGO_* environment variables. This first pass ensures that env vars can replace file-based values before flags are processed. Variables like HUGO_BASEURL are transformed into nested configuration keys (e.g., baseURL).
export HUGO_BASEURL="https://staging.example.com/"
Step 4: CLI Flag Overrides
Command-line flags are captured via flagsToCfg in commands/helpers.go. This helper inspects which flags were explicitly changed (f.Changed) and copies them into a temporary provider via applyFlagsOverrides. At this stage, flags overwrite values from files and the first env pass.
hugo server --bind=0.0.0.0 --port=3000
Step 5: Second Pass Environment Variables
Crucially, applyOsEnvOverrides runs a second time after flags are applied. This guarantees that environment variables always take precedence, even over explicit CLI flags. If you set both --baseURL and HUGO_BASEURL, the environment variable wins.
Step 6: Finalization
After configuration merging completes, Hugo finalizes module configurations and resolves directory mounts. Modules respect the same precedence hierarchy established during the loading sequence.
Configuration Precedence Hierarchy
When resolving any configuration key, Hugo uses the following precedence order (highest to lowest):
- Environment variables (
HUGO_*) – Applied in a final pass to ensure they override all other sources - CLI flags (e.g.,
--theme,--baseURL) – Captured viaflagsToCfgand applied viaapplyFlagsOverrides - Configuration files (
hugo.toml,hugo.yaml,config/directory) – Merged left-to-right when multiple files are specified - Default values – Hard-coded defaults set by
applyDefaultConfig
Practical Override Examples
Overriding BaseURL Through Three Layers
Create a default configuration in hugo.toml:
baseURL = "https://example.com/"
title = "My Site"
Override it temporarily with an environment variable for staging:
export HUGO_BASEURL="https://staging.example.com/"
hugo build # Uses staging URL
Override both with a CLI flag for a specific preview build:
hugo build --baseURL="https://preview.example.com/"
# Preview URL wins despite env var and config file
Working with Nested Parameters
Environment variables support nested keys using underscores. The loader transforms HUGO_PARAMS_FOO_BAR into params.foo.bar:
export HUGO_PARAMS_GITHUB_USER="gohugoio"
This merges into:
[params]
[params.github]
user = "gohugoio"
Multiple Configuration Files
When combining configurations, later files overwrite earlier ones:
hugo --config config.toml,config.production.toml
In this scenario, config.production.toml overrides any conflicting keys from config.toml.
Key Source Files
Understanding these implementation files helps when debugging configuration issues or contributing to Hugo:
config/allconfig/load.go– Contains the core loading orchestration includingapplyDefaultConfig,loadConfig, and the dualapplyOsEnvOverridespassescommands/helpers.go– ImplementsflagsToCfg, which translates CLI flags into configuration values viasetValueFromFlagconfig/configProvider.go– Defines theProviderinterface used throughout the codebase to access merged configuration valuescommands/config.go– Provides thehugo configcommand that prints the effective configuration after all merging completes
Summary
- Hugo merges configuration through a seven-step pipeline where later steps overwrite earlier ones
- Environment variables receive the highest precedence due to a second application pass after CLI flags
- CLI flags override configuration files but surrender to environment variables
- Multiple config files specified via
--configmerge left-to-right, with rightmost files winning - The
applyOsEnvOverridesfunction inload.gohandles variable transformation (e.g.,HUGO_PARAMS_Xtoparams.x) and is executed twice to ensure env vars always win
Frequently Asked Questions
Do environment variables override CLI flags in Hugo?
Yes. Although CLI flags are applied after the first environment pass, Hugo runs applyOsEnvOverrides a second time after processing flags. This ensures that a variable like HUGO_BASEURL always overwrites a --baseURL flag, matching the documented behavior that environment variables take precedence.
How does Hugo handle multiple configuration files?
When you specify --config a.toml,b.toml,c.toml, Hugo merges them sequentially from left to right. The rightmost file (c.toml) has the final say on conflicting keys. This merge happens during the loadConfig step before environment variables and CLI flags are applied.
Can I set nested configuration values via environment variables?
Yes. Hugo converts environment variable names with underscores into nested keys. For example, HUGO_PARAMS_FOO_BAR=baz becomes params.foo.bar = "baz" in the final configuration. The applyOsEnvOverrides function splits the variable name on underscores and builds the nested structure automatically.
What configuration source has the lowest precedence?
Hard-coded default values set by applyDefaultConfig in config/allconfig/load.go form the base layer. These defaults define standard directory names like contentDir = "content" and are only used when no other source (config file, env var, or CLI flag) provides a value for that key.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →