Full command-line reference for lib_layered_config, including the file-overwrite and
backup behavior and the per-layer permission and secret-handling guidance. For the
overview and installation see the README.
| Command | Description |
|---|---|
lib_layered_config read |
Load configuration (human readable by default) |
lib_layered_config read-json |
Emit config + provenance JSON envelope |
lib_layered_config deploy |
Copy a source file into one or more layer directories |
lib_layered_config generate-examples |
Scaffold example trees (POSIX/Windows layouts) |
lib_layered_config env-prefix |
Compute the canonical environment prefix |
lib_layered_config info |
Print package metadata |
lib_layered_config fail |
Intentionally raise a RuntimeError (for testing) |
Load configuration and print either human-readable prose or JSON.
Usage:
lib_layered_config read --vendor Acme --app ConfigKit --slug config-kit \
[--prefer toml] [--prefer json] \
[--start-dir /path/to/project] \
[--default-file ./config.defaults.toml] \
[--env-file /path/to/.env] \
[--format human|json] \
[--indent | --no-indent] \
[--provenance | --no-provenance]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--vendor |
string | Yes | - | Vendor namespace used to compute filesystem paths |
--app |
string | Yes | - | Application name used to compute filesystem paths |
--slug |
string | Yes | - | Configuration slug for file paths and environment prefix |
--prefer |
string | No | None | Preferred file suffix (repeatable flag: --prefer toml --prefer json). Earlier values take precedence. Valid values: toml, json, yaml, yml |
--start-dir |
path | No | current dir | Starting directory for upward .env file search. Must be an existing directory |
--default-file |
path | No | None | Path to lowest-precedence defaults file. Must be an existing file |
--env-file |
path | No | None | Explicit .env file path. Skips upward directory search when set. Must be an existing file |
--format |
choice | No | human |
Output format. Valid values: human (annotated prose), json (structured JSON) |
--indent / --no-indent |
flag | No | --indent |
Pretty-print JSON output with indentation. Only applies when --format json |
--provenance / --no-provenance |
flag | No | --provenance |
Include provenance metadata in JSON output. Only applies when --format json |
Examples:
Example 1: Basic configuration inspection (human-readable)
# Load and display configuration in human-readable format
lib_layered_config read --vendor Acme --app MyApp --slug myappOutput:
service.timeout: 30
provenance: layer=app, path=/etc/xdg/myapp/config.toml
service.endpoint: https://api.example.com
provenance: layer=user, path=/home/alice/.config/myapp/config.toml
database.host: localhost
provenance: layer=env, path=None
database.port: 5432
provenance: layer=app, path=/etc/xdg/myapp/config.toml
Explanation: The default format shows each configuration value with its source layer and file path (or "None" for environment variables). Perfect for quick debugging.
Example 2: JSON output for automation scripts
# Get configuration as JSON for use in shell scripts
config_json=$(lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--format json --no-provenance --no-indent)
# Parse with jq
echo "$config_json" | jq -r '.database.host'
# Output: localhostExplanation: Use --format json --no-provenance --no-indent to get just the configuration values as compact JSON, perfect for piping to jq or other JSON processors.
Example 3: Full audit with provenance (JSON)
# Get both configuration and provenance metadata
lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--format json --provenance --indent > config-audit.json
# View the structure
cat config-audit.jsonOutput:
{
"config": {
"service": {
"timeout": 30,
"endpoint": "https://api.example.com"
},
"database": {
"host": "localhost",
"port": 5432
}
},
"provenance": {
"service.timeout": {
"layer": "app",
"path": "/etc/xdg/myapp/config.toml",
"key": "service.timeout"
},
"service.endpoint": {
"layer": "user",
"path": "/home/alice/.config/myapp/config.toml",
"key": "service.endpoint"
},
"database.host": {
"layer": "env",
"path": null,
"key": "database.host"
}
}
}Explanation: This gives you complete audit information - both the final configuration values and where each one came from.
Example 4: Using file format preferences
# Prefer TOML files, then JSON, then YAML
lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--prefer toml --prefer json --prefer yamlExplanation: When multiple configuration file formats exist in the same directory (e.g., config.toml and config.json), the --prefer flag controls which one takes precedence. Earlier values win.
Example 5: Load with defaults and specific .env location
# Load configuration with shipped defaults and project-specific .env
lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--default-file ./config/defaults.toml \
--start-dir /opt/myapp \
--format humanExplanation: Use --default-file to provide application defaults that ship with your app, and --start-dir to specify where to start searching for .env files (useful when running from a different directory).
Example 6: Explicit .env file (skip directory search)
# Load a specific .env file instead of searching upward
lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--env-file /opt/myapp/secrets/.env.productionExplanation: Use --env-file to load a specific .env file directly. This bypasses the upward directory search entirely, which is useful in CI/CD pipelines or containers where the .env file lives in a known location.
Example 7: Debugging configuration issues
# Check if environment variables are overriding your config
MYAPP___SERVICE__TIMEOUT=5 lib_layered_config read \
--vendor Acme --app MyApp --slug myapp \
--format human | grep -A1 "service.timeout"Output:
service.timeout: 5
provenance: layer=env, path=None
Explanation: Set environment variables before the command to test how they override file-based configuration. The provenance shows which layer won.
Always emit combined JSON output (config + provenance). This is a convenience alias for read --format json --provenance.
Usage:
lib_layered_config read-json --vendor Acme --app ConfigKit --slug config-kit \
[--prefer toml] [--prefer json] \
[--start-dir /path/to/project] \
[--default-file ./config.defaults.toml] \
[--env-file /path/to/.env] \
[--indent | --no-indent]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--vendor |
string | Yes | - | Vendor namespace |
--app |
string | Yes | - | Application name |
--slug |
string | Yes | - | Configuration slug |
--prefer |
string | No | None | Preferred file suffix (repeatable) |
--start-dir |
path | No | current dir | Starting directory for .env search |
--default-file |
path | No | None | Path to defaults file |
--env-file |
path | No | None | Explicit .env file (skips search) |
--indent / --no-indent |
flag | No | --indent |
Pretty-print JSON output |
Example:
lib_layered_config read-json --vendor Acme --app ConfigKit --slug config-kit --indentCopy a source configuration file into one or more layer directories.
Usage:
lib_layered_config deploy --source ./config/app.toml \
--vendor Acme --app ConfigKit --slug config-kit \
--target app [--target host] [--target user] \
[--profile production] \
[--platform linux|darwin|windows] \
[--force] [--batch] \
[--permissions | --no-permissions] [--dir-mode MODE] [--file-mode MODE]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--source |
path | Yes | - | Path to the configuration file to copy. Must be an existing file |
--vendor |
string | Yes | - | Vendor namespace |
--app |
string | Yes | - | Application name |
--slug |
string | Yes | - | Configuration slug |
--profile |
string | No | - | Configuration profile name (e.g., test, production). Adds profile/<name>/ segment to deployment paths |
--target |
choice | Yes | - | Layer targets to deploy to (repeatable flag). Valid values: app, host, user. Can specify multiple: --target app --target user |
--platform |
string | No | auto-detect | Override platform. Valid values: linux, darwin, windows, or any string starting with win |
--force |
flag | No | false |
When file exists with different content: backup existing file to .bak and overwrite |
--batch |
flag | No | false |
Non-interactive mode: keeps existing files and writes new config as .ucf for review (CI/CD pipelines). Ignored if --force is set |
--permissions / --no-permissions |
flag | No | configured enabled, else on |
Set Unix file permissions on deployed files. Uses the configured modes, else app/host = 755/644 (world-readable), user = 700/600 (private). Skipped on Windows |
--dir-mode |
string | No | - | Directory mode for every target, octal (750 or 0o750). Refused when unsafe (see File Permissions) |
--file-mode |
string | No | - | File mode for every target, octal (640 or 0o640). Refused when unsafe (see File Permissions) |
Returns: JSON object with keys for each action taken:
created: Array of paths for newly created filesskipped: Array of paths for files that were skipped (identical content)overwritten: Array of paths for files that were overwrittenbackups: Array of paths for backup files created (.bakfiles)kept: Array of paths for existing files that were keptucf_files: Array of paths for UCF files created (.ucffiles with new config)
Only non-empty arrays are included in the output.
Profile Examples:
# Deploy to production profile
lib_layered_config deploy --source ./configs/prod.toml \
--vendor Acme --app MyApp --slug myapp \
--profile production --target app
# Linux: /etc/xdg/myapp/profile/production/config.toml
# Deploy to test profile
lib_layered_config deploy --source ./configs/test.toml \
--vendor Acme --app MyApp --slug myapp \
--profile test --target app --target user
# Linux: /etc/xdg/myapp/profile/test/config.toml
# ~/.config/myapp/profile/test/config.tomlThe deploy command has safe-by-default behavior with smart conflict handling:
Before any conflict handling, the deploy command compares the source content with the existing destination file byte-by-byte. If the content is identical, the file is skipped without creating backups:
# First deployment - creates file
lib_layered_config deploy --source ./config.toml \
--vendor Acme --app MyApp --slug myapp --target user
# Output: {"created": ["/home/alice/.config/myapp/config.toml"]}
# Second deployment with SAME content - smart skip (no backup needed)
lib_layered_config deploy --source ./config.toml \
--vendor Acme --app MyApp --slug myapp --target user --force
# Output: {"skipped": ["/home/alice/.config/myapp/config.toml"]}This prevents unnecessary .bak file proliferation when repeatedly deploying unchanged configurations.
When a file exists with different content and neither --force nor --batch is set:
- Prompts user with two options:
- [K]eep existing - Save new config as
.ucf(Update Configuration File) - default - [O]verwrite - Backup original to
.bak, then write new file
- [K]eep existing - Save new config as
- Creates new files if they don't exist
- Smart skips if content is identical (no backup created)
- Backs up and overwrites if content differs - existing file saved to
.bak - 📋 Returns JSON with
overwrittenandbackupsarrays
# Force deploy with different content - creates backup
lib_layered_config deploy --source ./new-config.toml \
--vendor Acme --app MyApp --slug myapp --target user --force
# Output: {"overwritten": ["/home/alice/.config/myapp/config.toml"],
# "backups": ["/home/alice/.config/myapp/config.toml.bak"]}- Creates new files if they don't exist
- Smart skips if content is identical
- 📄 Creates
.ucffiles when content differs - keeps existing, writes new as.ucffor review - 🛡 Safe for CI/CD pipelines - predictable behavior without user interaction
# Batch mode - keeps existing file, writes new config as .ucf for review
lib_layered_config deploy --source ./new-config.toml \
--vendor Acme --app MyApp --slug myapp --target user --batch
# Output: {"kept": ["/home/alice/.config/myapp/config.toml"],
# "ucf_files": ["/home/alice/.config/myapp/config.toml.ucf"]}Note: In
--batchmode, when content differs, the new configuration is written to a.ucffile for manual review. This allows CI/CD pipelines to deploy updates without overwriting user customizations, while making new configs available for review.
If .bak or .ucf files already exist, numbered suffixes are used:
config.toml.bak→config.toml.bak.1→config.toml.bak.2config.toml.ucf→config.toml.ucf.1→config.toml.ucf.2
┌─────────────────────────────────────┐
│ lib_layered_config deploy │
│ --source config.toml │
│ --target user │
└──────────────────┬──────────────────┘
▼
┌─────────────────┐
│ Does destination│
│ file exist? │
└───────┬─────────┘
YES │ NO
┌────────┴────────┐
▼ ▼
┌───────────────┐ ┌─────────────┐
│ Content same? │ │ Create file │
└───────┬───────┘ │ (created) │
YES │ NO └─────────────┘
┌─────┴─────┐
▼ ▼
┌─────────┐ ┌───────────┐
│ Skip │ │ --force ? │
│(skipped)│ └─────┬─────┘
└─────────┘ YES │ NO
┌──────┴───────┐
▼ ▼
┌─────────┐ ┌───────────┐
│ Backup │ │ --batch ? │
│ .bak │ └─────┬─────┘
│Overwrite│ YES │ NO
│(overwr.)│ ┌─────┴─────┐
└─────────┘ ▼ ▼
┌─────────┐ ┌──────────┐
│Keep + │ │ Prompt: │
│Write UCF│ │ K/O ? │
│ (kept) │ │(default K)│
└─────────┘ └──────────┘
# First time deploying - no files exist yet
sudo lib_layered_config deploy \
--source ./dist/config.toml \
--vendor Acme --app MyApp --slug myapp \
--target app
# Result: File created
# Output: {"created": ["/etc/xdg/myapp/config.toml"]}# Deploy same config again - content identical
lib_layered_config deploy \
--source ./dist/config.toml \
--vendor Acme --app MyApp --slug myapp \
--target app --force
# Result: Skipped (no backup created - content identical)
# Output: {"skipped": ["/etc/xdg/myapp/config.toml"]}# Deploy new version with --force - creates automatic backup
lib_layered_config deploy \
--source ./v2-config.toml \
--vendor Acme --app MyApp --slug myapp \
--target user --force
# Result: Old file backed up, new file written
# Output: {"overwritten": ["/home/alice/.config/myapp/config.toml"],
# "backups": ["/home/alice/.config/myapp/config.toml.bak"]}
# User's old config is preserved in .bak file!# Deploy in CI - keeps existing, writes new config as .ucf for review
lib_layered_config deploy \
--source ./new-config.toml \
--vendor Acme --app MyApp --slug myapp \
--target app --batch
# Result: Creates new files, keeps existing and writes UCF for review
# Output: {"kept": ["/etc/xdg/myapp/config.toml"],
# "ucf_files": ["/etc/xdg/myapp/config.toml.ucf"]}# Deploy to both app and user
# App: file exists with same content, User: no file exists
lib_layered_config deploy \
--source ./config.toml \
--vendor Acme --app MyApp --slug myapp \
--target app --target user --force
# 📋 Result: App skipped (same content), user created
# Output: {"created": ["/home/alice/.config/myapp/config.toml"],
# "skipped": ["/etc/xdg/myapp/config.toml"]}# As root/admin, deploy user config for a specific user
# Uses sudo -u to run as that user, ensuring correct $HOME and permissions
sudo -u alice lib_layered_config deploy \
--source ./user-defaults.toml \
--vendor Acme --app MyApp --slug myapp \
--target user
# Result: Config deployed to alice's home directory with correct ownership
# Output: {"created": ["/home/alice/.config/myapp/config.toml"]}
# File is owned by alice:alice with 600 permissions
# Deploy to multiple users in a loop
for user in alice bob charlie; do
sudo -u "$user" lib_layered_config deploy \
--source ./user-defaults.toml \
--vendor Acme --app MyApp --slug myapp \
--target user --batch
doneWhy
sudo -uinstead of justsudo? Running as root would deploy to/root/.config/, not the target user's home. Usingsudo -u <user>ensures:
- Correct
$HOMEdirectory resolution- Files owned by the target user (not root)
- Proper user-layer permissions (700/600)
-
Use
--batchfor CI/CD pipelines:# Predictable behavior - keeps existing, writes new as .ucf for review lib_layered_config deploy --source ./config.toml \ --vendor Acme --app MyApp --slug myapp --target app --batch -
Use
--forcewhen you want automatic backups:# Force creates .bak backup before overwriting lib_layered_config deploy --source ./new-config.toml \ --vendor Acme --app MyApp --slug myapp --target user --force # Old config preserved in config.toml.bak
-
Check the JSON output keys to understand what happened:
result=$(lib_layered_config deploy --source config.toml \ --vendor Acme --app MyApp --slug myapp --target user --batch) # Check what action was taken if echo "$result" | jq -e '.created' > /dev/null 2>&1; then echo "New file created" elif echo "$result" | jq -e '.kept' > /dev/null 2>&1; then echo "File kept, new config at .ucf for review" elif echo "$result" | jq -e '.skipped' > /dev/null 2>&1; then echo "File skipped (identical content)" fi
-
Document in installation scripts:
#!/bin/bash # Installation script echo "Deploying system-wide defaults..." result=$(sudo lib_layered_config deploy \ --source ./defaults.toml \ --vendor Acme --app MyApp --slug myapp \ --target app --batch) if echo "$result" | jq -e '.created' > /dev/null 2>&1; then echo "Configuration deployed" elif echo "$result" | jq -e '.kept' > /dev/null 2>&1; then echo "ℹ Configuration already exists, new config at .ucf for review" else echo "ℹ Configuration unchanged (identical content)" fi
-
Don't ignore the JSON output keys:
# BAD: Assuming array format result=$(lib_layered_config deploy --source config.toml --target user --batch) if [ "$result" = "[]" ]; then # Wrong! Output is now a JSON object echo "Nothing deployed" fi # GOOD: Parse JSON properly result=$(lib_layered_config deploy --source config.toml --target user --batch) if echo "$result" | jq -e '.created' > /dev/null 2>&1; then echo "Files created" fi
-
Don't forget to check backup files after
--force:# After force deploy, check for backups result=$(lib_layered_config deploy --source ./new-config.toml \ --vendor Acme --app MyApp --slug myapp --target user --force) # If overwritten, backups array contains the .bak file paths echo "$result" | jq -r '.backups[]?' 2>/dev/null
The deploy command automatically sets appropriate Unix file permissions based on the target layer. This ensures configuration files have the right access controls without manual chmod commands.
For each target layer and each side (directory, file), the first of these wins:
--dir-mode/--file-mode(Python:dir_mode/file_mode);- the configured
[lib_layered_config.default_permissions]setting for that layer; - the built-in layer mode:
| Layer | Directory Mode | File Mode | Rationale |
|---|---|---|---|
app |
755 (rwxr-xr-x) |
644 (rw-r--r--) |
System-wide config readable by all processes |
host |
755 (rwxr-xr-x) |
644 (rw-r--r--) |
Machine-specific config readable by all processes |
user |
700 (rwx------) |
600 (rw-------) |
Private user config not accessible by others |
--permissions / --no-permissions turns permission setting on or off; without either, a mode option turns it
on, and otherwise the configured enabled decides (on when unset).
A deployed file can hold credentials, so a mode may widen read access but nothing else. Refused, with every offending bit named, before anything is written:
- anything outside 0..0o7777, and any text that is not a plain octal literal (
-1,7_5_0,0x1ed,10000); - the setuid, setgid and sticky bits;
- group write and world write (a group can be every local account,
staffon macOS); - for a directory, a mode without owner rwx; for a file, any execute bit or a mode without owner rw.
The deployed application's configuration can set the modes per layer:
[lib_layered_config.default_permissions]
app_directory = "0o750"
app_file = "0o640"
user_file = "0o600"
enabled = trueWrite each mode as a quoted octal string. A bare integer is refused rather than guessed at: TOML reads
user_file = 400 as the decimal 400 (0o620), and the TOML literal 0o640 arrives as the integer 416. In the
environment use the 0o prefix (MYAPP___LIB_LAYERED_CONFIG__DEFAULT_PERMISSIONS__USER_FILE=0o640), since a
bare 640 there is a number (0640 stays text and also works); the same goes for an application's --set
option, where ...user_file=640 arrives as a number and ...user_file=0o640 as text. deploy reads these
settings only when a configured value can change the result, from the deployed source (the defaults layer), the
app, host and user files it does not itself write, and the environment, taking app_* and host_* from all of
those except the user files (see below). It never reads .env (whose search
starts at the working directory), and the file being replaced never decides its replacement's mode, so a broken
destination does not block deploy --force. If those settings cannot be read, or the section is invalid,
deploy stops with one line per problem, naming the key and its source (and the line, for a file that cannot
be parsed) but never its content. Both --dir-mode and
--file-mode deploy anyway; --no-permissions does too, but leaves every mode to the umask, which can make a
user file that holds secrets readable by other accounts. --no-permissions together with a mode option is a
usage error.
Modes are applied to each file deploy writes. A file whose content already matches is skipped and keeps its
mode, so changing a configured mode and redeploying the same release changes nothing on disk; chmod the
file, or remove it and deploy again.
A deployed copy's own section is never consulted by the deploy that writes or keeps it. Two effects follow:
with --batch a file that differs is KEPT (the new content goes to a .ucf beside it) and stays the live
configuration, but the directory and the .ucf get their modes from the other layers, not from the kept copy;
and the result depends on the targets, since --target app --target user treats the app config.toml as a
destination and does not read its user_* settings, while --target user alone does. Put local settings in a
file the package does not ship, such as config.d/99-local.toml in the layer directory, which deploy never
writes, so it is read whenever the configured modes are.
app_* and host_* are taken only from the deployed source, the app and host files and the environment,
never from a user-layer file (the user config.toml or a file in the user config.d/); a user-layer file
decides only user_* and enabled. A root deploy --target app that runs with the invoking account's HOME
or XDG_CONFIG_HOME (sudo -E, or a sudoers rule that keeps HOME) therefore cannot widen the modes of the
system files it writes through that account's files. A user-layer app_* or host_* value is ignored, not
refused: it neither decides a mode nor hides one set in a lower layer (an app file's app_file = "0o640" applies
even when the user file says "0o644"), a malformed one does not stop the deploy, and each ignored key is logged
as a deploy_setting_ignored warning naming the key and the file, never the value.
The environment can still set app_* and host_*. A root deploy that keeps an environment the invoking
account controls (sudo -E) therefore reads that account's variables, and a mode there can widen the READ
access of the system files the deploy writes. It can never add write access or an execute bit, since the
refusal rules above hold for every source. Run a system-wide deploy with root's own environment (sudo -i), or
pass both --dir-mode and --file-mode, which no configured value overrides.
lib_layered_config deploy --source ./config.toml \
--vendor Acme --app MyApp --slug myapp --target user --dir-mode 750 --file-mode 640from lib_layered_config import deploy_config
# Configured or built-in modes per layer
deploy_config(source="./config.toml", vendor="Acme", app="MyApp", slug="myapp", targets=["user"])
# Explicit modes win over the configured ones
deploy_config(
source="./config.toml", vendor="Acme", app="MyApp", slug="myapp", targets=["app"], dir_mode=0o750, file_mode=0o640
)
# Runtime overrides of the section (for example from a --set option) go on top of deploy's own read
deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
slug="myapp",
targets=["user"],
permission_overrides={"user_file": "0o640"},
)permission_overrides takes the section's own setting names (app_directory, app_file, host_directory,
host_file, user_directory, user_file, enabled); a refused value is reported as (source: override).
{"enabled": False} turns permission setting off without reading the configuration, like set_permissions=False
(unless a mode or set_permissions=True is also given, which set modes whatever enabled says).
permissions= takes a complete DeployPermissions instead and cannot be combined with it. Do not build that
object from your application's read_config(...) to carry overrides: that read includes .env (searched upward
from the working directory), the deployed files and a user file's app_*/host_* modes, which the deploy's own
read leaves out on purpose. deploy_permissions_from_config reads the merged Config it is given as it is, so
it does not apply the user-file rule above.
| Platform | Behavior |
|---|---|
| Linux | Full permission support via chmod() |
| macOS | Full permission support via chmod() |
| Windows | Permissions skipped (Windows uses ACLs, not Unix modes) |
Note: On Windows, file access control should be managed through Windows ACLs (Access Control Lists) using native tools like
icaclsor PowerShell'sSet-Acl. The library does not attempt to set Windows ACLs.
| File Type | Location | Owner:Group | Mode | Notes |
|---|---|---|---|---|
| App defaults | /etc/xdg/<slug>/config.toml |
root:root |
644 |
World-readable system defaults |
| Host overrides | /etc/xdg/<slug>/hosts/*.toml |
root:root |
644 |
Machine-specific settings |
| User config | ~/.config/<slug>/config.toml |
<user>:<user> |
600 |
Private user preferences |
Project .env |
/path/to/project/.env |
<user>:<user> |
600 |
Contains secrets - highly restricted |
User .env |
~/.config/<slug>/.env |
<user>:<user> |
600 |
Contains secrets - highly restricted |
.env files often contain secrets (API keys, database passwords, tokens) and should be highly restricted:
# Secure your .env files
chmod 600 .env
chmod 600 ~/.config/myapp/.env
# Verify permissions
ls -la .env
# -rw------- 1 alice alice 256 Jan 31 12:00 .envWarning: Never commit
.envfiles to version control. Add.envto your.gitignore.
macOS respects Unix permissions but also has additional security layers:
# Set restrictive permissions on macOS
chmod 600 ~/Library/Application\ Support/Acme/MyApp/config.toml
# Check extended attributes (if any)
xattr -l ~/Library/Application\ Support/Acme/MyApp/config.tomlFor applications distributed via App Store or notarized packages, additional entitlements may be required to access certain directories.
Windows uses Access Control Lists (ACLs) instead of Unix-style permissions:
# View current ACL
Get-Acl "$env:APPDATA\Acme\MyApp\config.toml" | Format-List
# Set restrictive ACL (current user only)
$acl = Get-Acl "$env:APPDATA\Acme\MyApp\config.toml"
$acl.SetAccessRuleProtection($true, $false)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
$env:USERNAME, "FullControl", "Allow")
$acl.SetAccessRule($rule)
Set-Acl "$env:APPDATA\Acme\MyApp\config.toml" $aclFor system-wide configuration in %ProgramData%, use Windows built-in permissions that allow read access for all users but write access only for administrators.
Best Practice: Don't store secrets in configuration files at all. Instead:
-
Use environment variables for secrets (highest precedence layer)
export MYAPP___DATABASE__PASSWORD="secret123" # Config will have database.password = "secret123" from env layer
-
Use
.envfiles with600permissions for local development# .env file (chmod 600) MYAPP___DATABASE__PASSWORD=secret123 MYAPP___API__TOKEN=tok_abc123 -
Use a secrets manager for production
- HashiCorp Vault - Enterprise-grade secret management
- AWS Secrets Manager - Native AWS integration
- Azure Key Vault - Native Azure integration
- Google Secret Manager - Native GCP integration
- 1Password CLI - Developer-friendly with
opCLI
# Example: Inject secrets from Vault at runtime export MYAPP___DATABASE__PASSWORD=$(vault kv get -field=password secret/myapp/db)
-
Use redaction when displaying or logging configuration
from lib_layered_config import read_config config = read_config(vendor="Acme", app="MyApp", slug="myapp") # Redacted output for logs (masks passwords, tokens, etc.) safe_json = config.to_json(redact=True) print(safe_json) # {"database": {"password": "***REDACTED***", "host": "localhost"}} # A key whose NAME looks secret but whose value is not: declare it once config = read_config(vendor="Acme", app="MyApp", slug="myapp", not_sensitive=["performance.max_token_bytes"]) print(config.to_json(redact=True)) # max_token_bytes shown, passwords still masked
A declaration covers its own path only: keys below it are still checked, and a masked section stays masked unless the section itself is declared. Only the Python API can declare; a config file or environment variable cannot unmask anything.
Environment variables provide the highest precedence layer and are ideal for secrets because:
- Not stored in files (no accidental commits)
- Process-scoped (visible only to the process and its children)
- Easy to rotate (just restart with new values)
- Compatible with container orchestration (Kubernetes secrets, Docker secrets)
# Linux/macOS: Set secrets at runtime
MYAPP___DATABASE__PASSWORD=secret123 \
MYAPP___API__TOKEN=tok_abc123 \
python -m myapp
# Or export for the session
export MYAPP___DATABASE__PASSWORD=secret123
python -m myappThe Python deploy_config() function returns list[DeployResult] with rich information:
from lib_layered_config import deploy_config
from lib_layered_config.examples.deploy import DeployAction
# Deploy with batch mode (CI/CD safe)
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
batch=True, # Keep existing, write new as .ucf for review
)
for result in results:
print(f"{result.action.value}: {result.destination}")
if result.ucf_path:
print(f" UCF file: {result.ucf_path}")
# Force deploy with automatic backups
results = deploy_config(
source="./config.toml",
vendor="Acme",
app="MyApp",
targets=["user"],
slug="myapp",
force=True, # Creates .bak backup before overwriting
)
for result in results:
if result.action == DeployAction.OVERWRITTEN:
print(f"Overwrote: {result.destination}")
print(f"Backup at: {result.backup_path}")
elif result.action == DeployAction.SKIPPED:
print(f"Skipped (identical content): {result.destination}")
elif result.action == DeployAction.CREATED:
print(f"Created: {result.destination}")Examples:
Example 1: Deploy system-wide defaults during installation
# Deploy app defaults to the system directory (requires sudo on Linux/macOS)
sudo lib_layered_config deploy \
--source ./dist/config.toml \
--vendor Acme --app MyApp --slug myapp \
--target appOutput:
{"created": ["/etc/xdg/myapp/config.toml"]}Explanation: This copies your configuration file to the system-wide location (/etc/xdg/myapp/config.toml on Linux, /Library/Application Support/Acme/MyApp/config.toml on macOS, etc.). If the source has a companion .d directory (e.g., ./dist/config.d/), those files are also deployed to /etc/xdg/myapp/config.d/. This is typically done during package installation.
Example 2: Deploy user-specific configuration
# Deploy user config (no sudo needed)
lib_layered_config deploy \
--source ./my-preferences.toml \
--vendor Acme --app MyApp --slug myapp \
--target userOutput:
{"created": ["/home/alice/.config/myapp/config.toml"]}Explanation: Deploys configuration to the current user's config directory. Great for user onboarding or preference templates.
Example 3: Deploy to multiple layers with --force
# Deploy base configuration to both system and user levels
lib_layered_config deploy \
--source ./config/base.toml \
--vendor Acme --app MyApp --slug myapp \
--target app --target user \
--forceOutput (if content differs from existing files):
{
"overwritten": ["/etc/xdg/myapp/config.toml", "/home/alice/.config/myapp/config.toml"],
"backups": ["/etc/xdg/myapp/config.toml.bak", "/home/alice/.config/myapp/config.toml.bak"]
}Output (if content is identical - smart skip):
{"skipped": ["/etc/xdg/myapp/config.toml", "/home/alice/.config/myapp/config.toml"]}Explanation: Using multiple --target flags deploys the same file to multiple locations. The --force flag creates .bak backups before overwriting. If content is identical, files are skipped without creating backups.
Example 4: Cross-platform deployment
# Deploy for Windows even when running on Linux (for CI/testing)
lib_layered_config deploy \
--source ./config.toml \
--vendor Acme --app MyApp --slug myapp \
--target user \
--platform windowsOutput:
{"created": ["C:\\Users\\alice\\AppData\\Roaming\\Acme\\MyApp\\config.toml"]}Explanation: Use --platform to override platform detection. Useful for testing deployment paths on different platforms without actually being on that platform.
Example 5: Deploy host-specific configuration
# Deploy configuration specific to this server
hostname=$(hostname)
lib_layered_config deploy \
--source ./hosts/${hostname}.toml \
--vendor Acme --app MyApp --slug myapp \
--target hostOutput:
{"created": ["/etc/xdg/myapp/hosts/server-01.toml"]}Explanation: Host-specific configurations are stored in the hosts/ subdirectory with the hostname as the filename. They override app defaults but only on machines with matching hostnames.
Example 6: CI/CD deployment with --batch
# Deploy in CI pipeline - keeps existing, writes new as .ucf for review
lib_layered_config deploy \
--source ./config.toml \
--vendor Acme --app MyApp --slug myapp \
--target user \
--batch
# If file exists with identical content - smart skipped
# Output: {"skipped": ["/home/alice/.config/myapp/config.toml"]}
# If file exists with different content - kept and UCF created
# Output: {"kept": ["/home/alice/.config/myapp/config.toml"],
# "ucf_files": ["/home/alice/.config/myapp/config.toml.ucf"]}
# If file doesn't exist - created
# Output: {"created": ["/home/alice/.config/myapp/config.toml"]}Explanation: Use --batch for non-interactive deployments in CI/CD pipelines. When content differs, the existing file is kept and the new config is written to a .ucf file for manual review, making automation predictable while preserving user customizations.
Generate example configuration trees for documentation or onboarding.
Usage:
lib_layered_config generate-examples --destination ./examples \
--vendor Acme --app ConfigKit --slug config-kit \
[--platform posix|windows] \
[--force | --no-force]Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
--destination |
path | Yes | - | Directory that will receive the example tree. Will be created if it doesn't exist |
--slug |
string | Yes | - | Configuration slug used in generated files |
--vendor |
string | Yes | - | Vendor namespace interpolated into examples |
--app |
string | Yes | - | Application name interpolated into examples |
--platform |
choice | No | auto-detect | Override platform layout. Valid values: posix (Linux/macOS layout), windows (Windows layout) |
--force / --no-force |
flag | No | --no-force |
Overwrite existing example files |
Returns: JSON array of file paths created.
Examples:
Example 1: Generate examples for your project documentation
# Create example configuration files in your docs directory
lib_layered_config generate-examples \
--destination ./docs/configuration-examples \
--vendor Acme --app MyApp --slug myappOutput:
[
"/path/to/docs/configuration-examples/xdg/myapp/config.toml",
"/path/to/docs/configuration-examples/xdg/myapp/hosts/your-hostname.toml",
"/path/to/docs/configuration-examples/xdg/myapp/config.d/10-override.toml",
"/path/to/docs/configuration-examples/home/myapp/config.toml",
"/path/to/docs/configuration-examples/.env.example"
]File contents preview:
# docs/configuration-examples/xdg/myapp/config.toml
# Application-wide defaults for myapp
[service]
endpoint = "https://api.example.com"
timeout = 10Explanation: Creates a complete set of example configuration files showing users how to configure your application. Include these in your documentation or repository.
Example 2: Generate both POSIX and Windows examples
# Generate Linux/macOS examples
lib_layered_config generate-examples \
--destination ./docs/examples/unix \
--vendor Acme --app MyApp --slug myapp \
--platform posix
# Generate Windows examples
lib_layered_config generate-examples \
--destination ./docs/examples/windows \
--vendor Acme --app MyApp --slug myapp \
--platform windowsExplanation: Generate platform-specific examples for comprehensive documentation. Windows examples use paths like ProgramData\Acme\MyApp\config.toml, while POSIX examples use /etc/xdg/myapp/config.toml.
Example 3: Update examples after configuration changes
# Regenerate examples with --force to update them
lib_layered_config generate-examples \
--destination ./examples \
--vendor Acme --app MyApp --slug myapp \
--forceExplanation: When you update your configuration schema, use --force to regenerate all example files. This ensures your documentation stays in sync with your application.
Example 4: Generated file structure (POSIX)
lib_layered_config generate-examples \
--destination ./examples \
--vendor Acme --app MyApp --slug myapp \
--platform posix
# View the generated structure
tree ./examplesOutput:
./examples/
├── etc/
│ └── myapp/
│ ├── config.toml # System-wide defaults
│ └── hosts/
│ └── your-hostname.toml # Host-specific overrides
├── xdg/
│ └── myapp/
│ ├── config.toml # User preferences
│ └── config.d/
│ └── 10-override.toml # Split configuration
└── .env.example # Environment variable template
Explanation: The generated structure mirrors the actual configuration layout your application will use, making it easy for users to understand where to place their config files.
Example 5: Use examples as onboarding templates
# Generate examples in a temp directory
lib_layered_config generate-examples \
--destination /tmp/myapp-examples \
--vendor Acme --app MyApp --slug myapp
# User can copy these to actual locations
echo "To get started, copy these examples:"
echo " sudo cp /tmp/myapp-examples/etc/myapp/config.toml /etc/myapp/"
echo " cp /tmp/myapp-examples/xdg/myapp/config.toml ~/.config/myapp/"
echo " cp /tmp/myapp-examples/.env.example .env"Explanation: Generate examples in a temporary location, then provide instructions for users to copy them to the actual configuration directories.
Example 6: CI/CD - Validate configuration structure
#!/bin/bash
# In your CI pipeline, generate examples and validate them
# Generate examples
lib_layered_config generate-examples \
--destination ./ci-examples \
--vendor Acme --app MyApp --slug myapp
# Check that all expected files were created
expected_files=(
"etc/myapp/config.toml"
"xdg/myapp/config.toml"
".env.example"
)
for file in "${expected_files[@]}"; do
if [ ! -f "./ci-examples/$file" ]; then
echo "ERROR: Missing example file: $file"
exit 1
fi
done
echo "✓ All configuration examples are valid"Explanation: Use in CI/CD to ensure your configuration structure is correct and all example files can be generated successfully.
Compute the canonical environment variable prefix for a configuration slug.
Usage:
lib_layered_config env-prefix <slug>Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
slug |
string | Yes (positional) | - | Configuration slug to convert to environment prefix |
Returns: Uppercase environment prefix with dashes converted to underscores.
Examples:
Example 1: Check what environment prefix your app uses
lib_layered_config env-prefix myappOutput:
MYAPP___
Explanation: This shows the environment variable prefix for your application (including the ___ separator). Use this prefix with double underscores for nested keys: MYAPP___DATABASE__HOST, MYAPP___SERVICE__TIMEOUT.
Example 2: Generate documentation for users
#!/bin/bash
# Script to document environment variables
app_slug="myapp"
prefix=$(lib_layered_config env-prefix "$app_slug")
cat << EOF
Environment Variables for $app_slug
====================================
All environment variables must be prefixed with: ${prefix}
Examples:
${prefix}DATABASE__HOST=localhost
${prefix}DATABASE__PORT=5432
${prefix}SERVICE__TIMEOUT=30
${prefix}SERVICE__RETRY__MAX_ATTEMPTS=3
Note: Use double underscores (__) to denote nesting in configuration keys.
EOFOutput:
Environment Variables for myapp
====================================
All environment variables must be prefixed with: MYAPP___
Examples:
MYAPP___DATABASE__HOST=localhost
MYAPP___DATABASE__PORT=5432
MYAPP___SERVICE__TIMEOUT=30
MYAPP___SERVICE__RETRY__MAX_ATTEMPTS=3
Note: Use double underscores (__) to denote nesting in configuration keys.
Explanation: Use this in documentation generation scripts to automatically show users the correct environment variable format.
Example 3: Validate environment variables in a script
#!/bin/bash
# Validate that users have set required environment variables
app_slug="config-kit"
prefix=$(lib_layered_config env-prefix "$app_slug")
required_vars=(
"${prefix}DATABASE__HOST"
"${prefix}DATABASE__PASSWORD"
"${prefix}API__SECRET_KEY"
)
missing=()
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
missing+=("$var")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo "Error: Missing required environment variables:"
printf ' %s\n' "${missing[@]}"
exit 1
fi
echo "✓ All required environment variables are set"Explanation: Programmatically check that required environment variables are set with the correct prefix before starting your application.
Example 4: Set test environment variables
# In a test script, set environment variables with the correct prefix
prefix=$(lib_layered_config env-prefix myapp)
export ${prefix}DATABASE__HOST="test-db.local"
export ${prefix}DATABASE__PORT="5432"
export ${prefix}SERVICE__TIMEOUT="5"
# Run tests
python -m pytest tests/Explanation: Dynamically generate environment variable names for testing, ensuring they match your application's expected prefix.
Print package metadata including version, author, and license.
Usage:
lib_layered_config infoParameters: None
Example:
lib_layered_config infoIntentionally raise a RuntimeError for testing error handling and CLI behavior.
Usage:
lib_layered_config failParameters: None
Raises: RuntimeError with message "i should fail".
Example:
lib_layered_config fail
# Output: RuntimeError: i should fail
# Exit code: 1