monkeys

poo

Write a .env that carries keys and no secrets.

monkeys poo [@profile] [--path DIR] [--open]
monkeys poo @profile --WITH_SECRETS [--EXPAND_DANGEROUSLY] [--path DIR] [--open]

Writes a .env carrying the same list .monkeys does: every secret's key on a line of its own, and every public value as it already stands. A key with nothing after it sets nothing, so the file satisfies whatever wanted one while the secrets keep arriving through run.

monkeys poo
wrote /tmp/monkeys-jn4u9q/.env for @test: 2 keys, 1 value
# monkeys' poo @test. read https://monk3ys.dev/poo
DATABASE_URL
STRIPE_SECRET_KEY
PORT=3000

It lands in /tmp unless you say otherwise, so nothing appears in a project that did not ask for it. The header names the profile when the project declares more than one, since that is the profile run has to be given to match.

Why a key carries no value

A placeholder would be worse than nothing. The three things that read a .env treat a bare key and a KEY=x differently, and only the bare key is safe in all three:

what reads itDATABASE_URLDATABASE_URL=x
dotenv, so Vite, SvelteKit, Nextignored, the environment's value standsignored, the environment's value stands
Docker Compose env_file:taken from the host environmentx goes into the container
set -a; . .envthe environment's value survivesoverwritten with x

The bottom two rows are the reason. A placeholder reaches a container or a shell as a real value, so the program starts and fails later, at the first call that uses the key, where the cause is no longer obvious.

IMPORTANT

If you maintain a framework, please make its loader read .monkeys, taking each key's value from the operating system's vault. Have it replace .env, or sit between .env and the process environment.

--path

--path names the directory to write into, and the file inside it is always .env:

monkeys poo --path .
a .env into ~/code/shop/.env, where a commit can take it. go ahead? [y/N] y
wrote ~/code/shop/.env for @test: 2 keys, 1 value

Anywhere inside a git checkout it asks first, because that is where a commit can find the file. Answer anything but y and nothing is written. --open shows whatever it wrote in the file manager, wherever that was.

A .env that poo did not write is left alone:

monkeys: ~/code/shop/.env was not written by poo: monkeys eat it first, or move it aside

--WITH_SECRETS

A tool that wants the values, not only the keys, takes them as vault lookups:

monkeys poo @production --WITH_SECRETS
wrote /tmp/monkeys-QA6tuw/.env for @production: 3 lookups, 1 value
# monkeys' poo @production, vault lookups. read https://monk3ys.dev/poo
DATABASE_URL="$(security find-generic-password -s monkeys -a shop.production/DATABASE_URL -w)"
STRIPE_SECRET_KEY="$(security find-generic-password -s monkeys -a shop.production/STRIPE_SECRET_KEY -w)"
SENTRY_DSN="$(security find-generic-password -s monkeys -a shop.production/SENTRY_DSN -w)"
PORT=8080

That is the line export writes, so nothing of the secret is on disk and the file is as safe to keep as the keys were. The profile has to be named: the lookup carries one profile's stored name, and the wrong one resolves to the wrong secret rather than to nothing.

What resolves it is a shell:

set -a; . ./.env; set +a
DATABASE_URL=postgres://user:pa$$w0rd@host/shop

--EXPAND_DANGEROUSLY

WARNING

This is the one command that writes secrets, in the open, to a file. Nothing else in monkeys does. Reach for it only when the tool cannot run a lookup, and delete what it wrote when you are done.

A reader that parses the file without running anything gets the text of the lookup instead of the secret. Vite, SvelteKit, Next and Compose all read a .env this way:

vite got: "$(security find-generic-password -s monkeys -a shop.production/DATABASE_URL -w)"

Only then is there reason to put the secrets themselves in a file, which is what this flag does. It runs the lookups and writes what they return:

monkeys poo @production --WITH_SECRETS --EXPAND_DANGEROUSLY
wrote /tmp/monkeys-01mjsu/.env for @production: 3 secrets, 1 value
the secrets are in the open in that file; delete it when the deploy is done
# monkeys' poo @production, secrets in the open. delete this file. read https://monk3ys.dev/poo
DATABASE_URL="postgres://user:pa\$\$w0rd@host/shop"
STRIPE_SECRET_KEY="sk-demo-live"
SENTRY_DSN="https://[email protected]/1"
PORT=8080

It is a fallback for the place that takes no vault and no command, such as a deploy dashboard that wants the variables pasted in. That is a person's errand, so the flag wants a terminal and refuses a pipe:

monkeys: --EXPAND_DANGEROUSLY leaves secrets in a file for a person to paste, so it needs a terminal

--path still works, and inside a git checkout the question is no longer one letter:

plain secrets into ~/code/shop/.env, where a commit can take them. not recommended. [UNDERSTOOD/N] UNDERSTOOD

A $ inside a secret is written \$, which both a shell and dotenv read back as one character, so a password like pa$$w0rd arrives whole. A secret holding a quote or a backslash is refused instead, since no dotenv line carries those back unchanged.

@profile

A profile names which list to write, the way it does everywhere else:

monkeys poo @production

One file is written, never one per profile. Which profile a program runs under is run's to decide, and a second place to decide it is a second place to get it wrong.

On this page