Shhh, Don't Put Secrets in the Repo

andreasflakstad1 pts0 comments

Shhh, Don't Put Secrets in the Repo | Andreas FlakstadMay 19, 2026<br>Shhh, Don't Put Secrets in the Repo<br>Let&rsquo;s talk about app config and secrets.<br>Every web app needs a port, URLs, feature flags, API keys, OAuth secrets, maybe<br>a service account JSON file, maybe a certificate and key.<br>Some values come from environment variables. Some come from local files. Some<br>are defaults. Some are fetched from a secret manager. Some need to be written as<br>files because the library using them expects a path.<br>The common answer I&rsquo;ve been exposed to is to make the app orchestrate all of<br>this. Add a config library and teach it where to look.<br>Well, that works, but now startup has its own rules. Does the env var override the<br>file? Does the local profile override the default? Does the cloud secret replace<br>the local one? To understand how the program starts, I have to understand the<br>precedence rules inside the app.<br>Local development often adds another problem. Developers are expected to keep<br>secret files around, remember to not check them in, add them to .gitignore.<br>While .gitignore prevents accidental commits, it does not prevent local tools<br>from reading the file. And that certainly matters if you play with AI agents.<br>When an agent examines the project, ignored files are of course still local<br>files it can read. If the secret is sitting in the working tree, the agent can<br>see it.<br>So is there a better way? Well, here&rsquo;s one way I came up with. Some secret<br>managers can do parts of this, but they are usually focused on fetching secrets<br>at runtime and require internet access. I wanted something small and dedicated,<br>that works offline, and solves problems in my local workflows. So, the source<br>code repo should describe what config and secrets it needs, but the repo should<br>not contain any secret values. The app should not need to know how to load secrets<br>from five different places. And reading secrets should not require internet<br>access.<br>Introducing Kimen, a local-first secrets tool. It is<br>basically a small vault plus a projection step.<br>The vault stores your secrets. The projection step turns vault keys into the<br>environment variables and files a process expects, just before that process<br>starts.<br>Let&rsquo;s start with one secret which we&rsquo;ll call prod.database_url:<br>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,<br>an env var, or a text file on disk.<br>Then later we project that secret into the environment of your program:<br>kimen run --env DATABASE_URL=prod.database_url -- ./your-app

The app sees DATABASE_URL like any other environment variable. It does not<br>call Kimen or know about the vault.<br>If we have many secrets to project a good approach is to add a profile to the<br>repo; a small config file with placeholders for secrets. The left side of the<br>following is what the app will see. The right side is what Kimen will retrieve<br>from the vault.<br>So let&rsquo;s create a file in the repo at .kimen/profiles/prod.kmap:<br>env DATABASE_URL=prod.database_url<br>env SERVICE_API_TOKEN=prod.service_api_token<br>env PORT=const:5050<br>file credentials.json=prod.gcp_credentials_json<br>envpath GOOGLE_APPLICATION_CREDENTIALS=credentials.json

file here renders a secret to a runtime file. envpath sets an environment<br>variable to the path of that rendered file. const:5050 is the syntax for<br>declaring a config constant in the profile that should not be replaced by a<br>secret value.<br>The names on the right in the profile are not secret values. They are keys in<br>the vault. This profile can be checked in safely because it only describes the runtime<br>shape.<br>Then, when I start the program, Kimen hydrates that profile for this one run:<br>kimen run --profile prod -- ./your-app

Kimen resolves prod to the prod.kmap file, reads the mappings, fetches the<br>referenced vault values, sets DATABASE_URL, SERVICE_API_TOKEN, and PORT,<br>renders credentials.json into a temporary runtime directory, and sets<br>GOOGLE_APPLICATION_CREDENTIALS to that file path.<br>The app still does not call Kimen. It just reads from the standard environment.<br>For deploys to my VPS services, I use the same profiles to render envfiles before uploading them to<br>the server. Kimen runs locally; the server only receives the rendered envfiles.<br>This can also be done in a CI pipeline.<br>For local development, I typically use a dev profile to start the REPL:<br>kimen run --profile dev -- clojure ...

Each invocation of kimen that accesses the vault requires a passphrase. If I<br>plan to run several of these within a short timeframe it can be nice to unlock<br>the vault only once:<br>kimen session start --ttl 15m

That gives trusted same-user tools a bounded window to use the vault without<br>asking for the passphrase again. Then I can lock it when I am done, or let the<br>timeframe expire:<br>kimen session lock

The important boundary is still before the app starts, not inside the app.<br>Source code: github.com/flakstad/kimen

kimen secrets secret file vault prod

Related Articles