confit export: one unlock, one file, every secret your agent needs
confit 0.4.0 ships confit export and [env] profiles — materialize a complete multi-section dev env into a sourceable file with one 1Password unlock. Designed for agent workflows where re-auth isn't an option.
The Re-Auth Wall
Here's a situation that comes up constantly in agentic workflows. You're running a sequence of shell commands — say, migrate, then seed, then server. Each command needs a full dev env: database credentials from 1Password, service URLs from config, API keys from somewhere else. With confit run, each command gets injected from its section. Works great.
Except your agent runs each shell tool in a fresh subprocess. Env vars don't persist. So every step re-triggers provider resolution. With 1Password, that means three interactive unlocks for three commands. For a human sitting at a terminal this is annoying. For an agent driving a shell tool, it's a hard block — the agent can't respond to the 1Password prompt, and the command just hangs.
confit run wasn't designed for this. It's a single-command runner: inject env, exec, done. It doesn't help you build a persistent env file that a fresh subprocess can source without re-authenticating. That gap is what v0.4.0 closes.
confit export
confit export is a new first-class verb that materializes a complete env — across multiple sections or a named profile — into a single file you can source repeatedly. The full resolution happens once. One 1Password unlock. One subprocess per source bag. After that, the file is a flat KEY=VALUE store and nothing needs to re-authenticate to read it.
The basic form takes one or more section names:
confit export credentials app db --reveal --out .env.dev
That resolves [credentials], [app], and [db], merges them (later section wins on key conflict), and writes the result to .env.dev. The --reveal flag is required any time secrets are present — without it, confit refuses to write rather than produce a file full of *** masks.
The output file is written with 0600 permissions, atomically (temp file + rename), and confit checks git check-ignore before writing — it will refuse to write to a path that isn't gitignored unless you pass --force. The failure mode is hard: if any value can't resolve, you get a nonzero exit and no file. No partial outputs.
Formats are selectable with --format dotenv|shell|json. All formats use shell-safe single quoting. The default is dotenv.
env profiles
Typing out section lists every time is fine for one-offs, but in practice you have a stable "dev env" and a stable "prod env" and you don't want to remember what sections each one pulls from. That's what [env.<name>] profiles are for.
A profile declares a named env in config that composes sections and can override specific variables:
[vars]
stage = "prod"
[sources.openv]
load = "op environment read {op.envs.{vars.stage}}"
secret = true
[op.envs]
dev = "env_abc123"
prod = "env_xyz789"
[env.dev]
sections = ["credentials", "app", "db"]
vars.stage = "dev"
[env.prod]
sections = ["credentials", "app", "db"]
# inherits vars.stage = "prod" from [vars]The profile's vars.stage = "dev" key overrides [vars] for that profile's resolution only. Precedence is: [vars] defaults → profile vars → CONFIT_VAR_* env → --set CLI. So you can still override from the command line even when using a profile.
Invoking it:
confit export --profile dev --reveal --out web/.env.dev
The profile and the ad-hoc section list share a single SourceCache. If openv is referenced across three sections in the same profile, it loads once. This composes directly with the [sources] feature from v0.3.0 — that's exactly what sources were built for.
The Agent Workflow
The intended workflow for agents is now straightforward. At the start of an agentic session — or at the start of a task that needs a dev env — you run one export command that the human approves:
# once — human unlocks 1Password here confit export --profile dev --reveal --out .env.dev # any subsequent shell tool, no re-auth needed set -a; . .env.dev; set +a node scripts/migrate.js set -a; . .env.dev; set +a node scripts/seed.js set -a; . .env.dev; set +a node server.js
The human approves one unlock. After that, the agent reads from a flat file and every fresh subprocess has everything it needs. No more mid-task auth prompts, no hanging commands, no half-migrated databases because the second step couldn't authenticate.
The security model is deliberate here. When you use --out, secrets go only to the file — a keys-only summary (no values) prints to stderr. So secret values aren't leaking into the agent's conversation transcript. The file sits at 0600 and is gitignored. It's as safe as you can reasonably get for a materialized env file.
Fail Closed
A design principle worth naming: the whole feature fails closed. If any value can't resolve — a missing source field, a provider that returns nonzero, an unset env var — you get an error and no file is written. This is intentional. A partial env file is worse than no env file. Your agent sources the file and silently runs with half the vars it expected, and then something weird happens three steps later that's hard to trace back.
The gitignore check is the same philosophy. Confit won't let you accidentally write secrets to a file that git would happily commit. You can override it with --force if you genuinely know what you're doing, but the default is: refuse.
How It Composes
One thing I want to be clear about: export doesn't duplicate anything from run or show. It's a different operation for a different use case. run is for single commands where you want injection without a file. show is for inspection. export is for materializing a persistent, sourceable env for use across multiple commands or processes.
And since it's built on the same resolution core — same providers, same sources, same interpolation — it inherits all the existing config you've already written. You're not defining your env twice; you're getting a new way to materialize it.
v0.4.0 is available for install now:
curl -fsSL https://raw.githubusercontent.com/krondor-corp/confit/main/install.sh | bash