Let’s talk about app config and secrets.
Every web app needs a port, URLs, feature flags, API keys, OAuth secrets, maybe a service account JSON file, maybe a certificate and key.
Some values come from environment variables. Some come from local files. Some are defaults. Some are fetched from a secret manager. Some need to be written as files because the library using them expects a path.
The common answer I’ve been exposed to is to make the app orchestrate all of this. Add a config library and teach it where to look.
Well, that works, but now startup has its own rules. Does the env var override the file? Does the local profile override the default? Does the cloud secret replace the local one? To understand how the program starts, I have to understand the precedence rules inside the app.
Local development often adds another problem. Developers are expected to keep
secret files around, remember to not check them in, add them to .gitignore.
While .gitignore prevents accidental commits, it does not prevent local tools
from reading the file. And that certainly matters if you play with AI agents.
When an agent examines the project, ignored files are of course still local
files it can read. If the secret is sitting in the working tree, the agent can
see it.
So is there a better way? Well, here’s one way I came up with. Some secret managers can do parts of this, but they are usually focused on fetching secrets at runtime and require internet access. I wanted something small and dedicated, that works offline, and solves problems in my local workflows. So, the source code repo should describe what config and secrets it needs, but the repo should not contain any secret values. The app should not need to know how to load secrets from five different places. And reading secrets should not require internet access.
Introducing Kimen, a local-first secrets tool. It is basically a small vault plus a projection step.
The vault stores your secrets. The projection step turns vault keys into the environment variables and files a process expects, just before that process starts.
Let’s start with one secret which we’ll call prod.database_url:
kimen secret set prod.database_url
That call prompts for you to type out the secret safely in the terminal, without putting it in shell history, an env var, or a text file on disk.
Then later we project that secret into the environment of your program:
kimen run --env DATABASE_URL=prod.database_url -- ./your-app
The app sees DATABASE_URL like any other environment variable. It does not
call Kimen or know about the vault.
If we have many secrets to project a good approach is to add a profile to the repo; a small config file with placeholders for secrets. The left side of the following is what the app will see. The right side is what Kimen will retrieve from the vault.
So let’s create a file in the repo at .kimen/profiles/prod.kmap:
env DATABASE_URL=prod.database_url
env SERVICE_API_TOKEN=prod.service_api_token
env PORT=const:5050
file credentials.json=prod.gcp_credentials_json
envpath GOOGLE_APPLICATION_CREDENTIALS=credentials.json
file here renders a secret to a runtime file. envpath sets an environment
variable to the path of that rendered file. const:5050 is the syntax for
declaring a config constant in the profile that should not be replaced by a
secret value.
The names on the right in the profile are not secret values. They are keys in the vault. This profile can be checked in safely because it only describes the runtime shape.
Then, when I start the program, Kimen hydrates that profile for this one run:
kimen run --profile prod -- ./your-app
Kimen resolves prod to the prod.kmap file, reads the mappings, fetches the
referenced vault values, sets DATABASE_URL, SERVICE_API_TOKEN, and PORT,
renders credentials.json into a temporary runtime directory, and sets
GOOGLE_APPLICATION_CREDENTIALS to that file path.
The app still does not call Kimen. It just reads from the standard environment.
For deploys to my VPS services, I use the same profiles to render envfiles before uploading them to the server. Kimen runs locally; the server only receives the rendered envfiles. This can also be done in a CI pipeline.
For local development, I typically use a dev profile to start the REPL:
kimen run --profile dev -- clojure ...
Each invocation of kimen that accesses the vault requires a passphrase. If I
plan to run several of these within a short timeframe it can be nice to unlock
the vault only once:
kimen session start --ttl 15m
That gives trusted same-user tools a bounded window to use the vault without asking for the passphrase again. Then I can lock it when I am done, or let the timeframe expire:
kimen session lock
The important boundary is still before the app starts, not inside the app.
Source code: github.com/flakstad/kimen