axm-config
Non-sensitive runtime config under ~/.axm (env>file>default)
Installation
Quick Start
The public API is exactly six symbols: get, load, set_, delete,
axm_home, and ConfigError.
Python
from axm_config import ConfigError, axm_home, delete, get, load, set_
# Resolve (and create, 0700) the per-user ~/.axm directory.
home = axm_home()
print(home) # e.g. /Users/you/.axm
# Resolve runtime config with env > file > default precedence.
set_("research.fred", "api_key", "abc123") # writes [research.fred] in ~/.axm/config.toml
key = get("research.fred", "api_key", default=None) # "abc123"
# Remove a key (no-op if absent); it then resolves to the default again.
delete("research.fred", "api_key") # set_(..., None) does the same
get("research.fred", "api_key", default="fallback") # "fallback"
# namespace/key are validated: a traversal/empty/invalid segment raises
# ConfigError and never writes outside ~/.axm.
set_("../evil", "k", "v") # raises ConfigError
# Env wins: AXM_RESEARCH__FRED_API_KEY overrides the file value.
# (a namespace dot folds to a *double* underscore so a dotted namespace
# stays distinct from an underscore one; keys must be dot-free.)
# Populate a pydantic model — each field is resolved by name.
from pydantic import BaseModel
class FredConfig(BaseModel):
api_key: str
timeout: int = 30
cfg = load("research.fred", FredConfig) # ConfigError if api_key unresolved
From the shell, the axm-config command exposes the same resolution layer:
Bash
axm-config set research.fred api_key abc123 # persist to ~/.axm
axm-config get research.fred api_key # prints the resolved value
axm-config delete research.fred api_key # remove a key (no-op if absent)
axm-config path # prints the ~/.axm home
axm-config doctor research.fred # per-key provenance, read-only
Features
- ✅
~/.axmhome —axm_home()resolves and creates the per-user config directory with mode0700(idempotent, tightens looser perms) - ✅ Layered resolution —
get()/set_()/delete()resolve a(namespace, key)withenv > file > defaultprecedence; the env name is derived deterministically asAXM_<NS>_<KEY>(upper-cased, each namespace dot → a double underscore). The mapping is provably injective and always POSIX-valid: segments are lowercase-only (soDemoanddemocan never fold to the sameAXM_DEMO_*), a namespace carries no_of its own and no-, and a key joins lowercase-alphanumeric runs with single_(no leading/trailing/doubled__) — so a__can only come from a namespace dot, the lone single_separates the folded namespace from the key, and no-ever leaks into the name. The on-disk store keeps one~/.axm/config.tomlwith a[<namespace>]table per namespace (a dotted namespace → a nested table, e.g.[storage.portfolio]) and writes it atomically (file0600, temp file cleaned up even if the atomic move fails). A read-modify-write of the whole file preserves every other namespace's section; a missing or corrupt file/section degrades gracefully to{}instead of raising. Legacy per-namespace~/.axm/<ns>.tomlfiles (the previous layout) are read-through and folded intoconfig.tomlon the next write — no silent data loss.delete()removes a key (no-op if absent);set_(ns, key, None)routes to the same delete - ✅ Path-traversal safe & unambiguous env names —
namespaceandkeyare validated at the public boundary against safe-segment patterns (a namespace is lowercase-alphanumeric segments joined by dots,^[a-z0-9]+(\.[a-z0-9]+)*$, so uppercase,_and-in a namespace are rejected; a key is lowercase-alphanumeric segments joined by single_,^[a-z0-9]+(_[a-z0-9]+)*$, so uppercase, dots/dashes, and leading/trailing or doubled_in a key are rejected). A traversal/empty/invalid segment raisesConfigError, a config file can never land outside the resolved~/.axmhome (aHOMEpointing into a git checkout is refused), and the derived env-var name is always POSIX-valid - ✅ Model binding —
load(namespace, model)populates a consumer's pydantic model, resolving each field by name; a missing required field raisesConfigError - ✅
axm-configCLI —get/set/delete/path/doctorsubcommands wrap the resolution layer for shell use; every command delegates to the same central function with no logic duplicated - ✅ Provenance doctor — the
config_doctorAXMTool reports which layer (env/file/default) would win for every visible key, read-only; available over MCP, theaxmCLI, andaxm-config doctor - ✅ Minimal deps — stdlib
pathlib/os/tomllib, plustomli-wfor atomic TOML writes andcycloptsfor the CLI - ✅ Modern Python — 3.12+ with strict typing