Skip to content

Decision 108: the env file is loaded by a stdlib parser, resolved against the config, and never overrides the environment

  • Status: proposed
  • Date: 2026-09-03
  • Work item: issue-318
  • Deciders: MadaraUchiha-314 (owner, via the ticket), the-loop (design)
  • Refines: decision-032 (the CLI config is independent of any checkout), decision-038 (PyYAML is the CLI's one dependency)

Context

Every credential the-loop uses is named in cli-config.yaml and read from the process environment — the config never carries a value. That left the operator to export the values by hand before the-loop start, in every terminal, or to wrap the command. The owner asked for a .env file the config can name and the-loop loads at start.

Four things had to be chosen: how the file is parsed, where a relative path points, who wins when the environment and the file disagree, and when it is read.

Decision

#What was chosenWhy
D1A stdlib parser (the_loop.envfile), not python-dotenv. NAME=value lines, # comments, export, double quotes with five escapes, single quotes literal, unquoted trimmed with a trailing comment; no interpolation.The CLI has one runtime dependency by decision (decision-038) and a second one for eighty lines is not a trade worth making; the subset an operator needs for tokens is small, and stating the grammar ourselves means the behaviour is the-loop's to document and test rather than a library's to change.
D2A relative path resolves against the directory of the config file that names it; ~ expands.The config is found in four places — a flag, an env var, ./.the-loop/, ~/.the-loop/ — two of them outside any checkout. "The file beside my config" reads the same from all four; "relative to wherever I ran the command" does not, and state.root already shows how a cwd-relative default surprises operators.
D3The process environment wins. A name already set is left alone; the file fills gaps.A deliberately exported value must survive a config edit — anyone who can edit the config would otherwise be able to redirect a running operator's credential to a file of their choosing. It is also what every dotenv implementation defaults to, so nothing an operator brings from elsewhere is contradicted.
D4Read once, at process start, by each entry point; never on reload. cli.main, daemon_entry.main and api/serve.main each load it before their own work; children inherit and load again, idempotently.The daemons hot-reload cli-config.yaml; a reload that could change the credentials a running process holds is a reload that can be used to swap them. A restart is the price, as it is for service.host. Each entry point loading independently means a systemd unit that runs daemon_entry directly gets the same variables as one the-loop start spawned.
D5A missing or malformed file warns and loads what it can; it never stops the process.The config may be tracked in git while the file is per-machine (this repository's is): a checkout without the file must still run the-loop --version and the-loop events. Every credential-dependent feature already fails closed when its variable is absent, so a missing file degrades to exactly the 13.2.0 behaviour, and the warning says why.

Consequences

Good. One line of config replaces a shell ritual; every process the-loop runs sees the same variables however it was started; the config still never carries a value; a stale config, a missing file and a malformed line are each reported without stopping anything.

Costs, accepted. No interpolation and no multi-line values (an operator with either keeps their shell); a change to the file needs a restart; a world-readable file is warned about, not refused, because the same values in a shell are as visible and a refusal would push operators back to export.

Alternatives considered

AlternativeWhy not
Depend on python-dotenvA second runtime dependency (decision-038) for a subset we can state in a page; its interpolation would also be a surface to document and defend
Resolve relative to the working directoryWrong for ~/.the-loop/cli-config.yaml, the common case, and a repeat of the state.root surprise
The file overrides the environment (override=True)A config edit could redirect an exported credential; contradicts every dotenv default
Re-read on config reloadA reload could change a running process's credentials; the file is not the config
A --env-file flag / $THE_LOOP_ENV_FILEThe config already has both a flag and a variable to select it; a second pair for a file the config names is two more things to get wrong
Refuse to start on a missing fileBreaks --version, migrate-config and every checkout where the config is tracked and the file is per-machine

Released under the MIT License.