mehendocs

GuidesBring a machine's credentials into Mehen

Bring a machine's credentials into Mehen#

I have .env files across my repos and services; how do I bring them all into Mehen at once?

On this page

You have enrolled your first machine, and it has .env files in a dozen repos, a .local-secrets folder and a few services with keys in their env files. mehen adopt brings them into the vault in one batch, which you approve with one passkey. For a single key, Move existing keys in is quicker.

What adopt does, and what it never does#

The steps, in order: scan, plan, snapshot, submit, approve, apply, verify. Run them in a terminal on the machine that holds the files. An agent may run scan and plan for you; the snapshot and the approval are yours.

1. Scan#

mehen adopt scan

With no flags it walks ~/code (six folders deep), ~/.config, ~/.local-secrets and your service units (~/Library/LaunchAgents on macOS, ~/.config/systemd/user on Linux), plus the usual login files (gh, kubeconfig, talosconfig, gcloud, Cloudflare, Docker and Claude Code settings). It reads .env, .env.*, .dev.vars, *.env, .envrc, settings JSON, launchd plists and systemd units. In .local-secrets it also reads every other file: as NAME=value lines when it has them, or else whole when it holds one key, a PEM block such as a GitHub App's .pem or an Apple .p8, a service account, or a token on one line. A key read whole is named by its file, so ci-deploy-token becomes the item ci-deploy-token-local-secrets. Any other file there, such as notes or a JSON export, is listed once as a file to move by hand. It skips example files (.env.example, config.example.json), node_modules, .git, vendor, dist, build, .venv, binaries, files over 1 MiB and files another user owns.

To scan only some folders, name them: mehen adopt scan --no-defaults --root ~/code/billing-api. --depth, --include and --exclude narrow it further.

Reading the scan:

2. Plan#

mehen adopt plan

The plan reads the scan, asks the vault which names and projects already exist, and writes ~/.config/mehen/adopt/<machine>/plan.json. It prints one row per folder with the home its credentials get, then any problems.

Homes. Every credential needs a home (credentials):

A repo or service gets a project only when at least one of its credentials is ticked. One with only plain config (a launchd PATH) or only possibly stale credentials is left as it is: no project, no mehen.toml, no variable.

A private credential cannot back a project variable, so anything mehen run should deliver to a repo or service needs a project or workspace home. Change a folder's home and plan again:

mehen adopt plan --home ~/.local-secrets=private --home ~/code/side-project=skip --home ~/code/shared-tools=project:shared-tools

Each --home takes <folder>=private, workspace, project:<slug> or skip. It covers every folder below it too, so --home ~/code/research=skip skips each repo under ~/code/research; a deeper --home wins. A folder with nothing the scan found at or below it is refused, since it would change nothing.

Workspaces per folder. A plan is for one workspace: the one this machine is enrolled in, or the one --workspace <slug> names when it is enrolled in several. When some folders belong to another workspace, skip them in this plan, then plan for the other workspace into a file of its own, skipping the folders that stay behind:

mehen adopt plan --home ~/code/client-app=skip
mehen adopt plan --workspace client-co --out ~/client-co-plan.json --home ~/code/billing-api=skip

Pass that file with --plan to the snapshot, submit, apply and verify, and name its batch with --adoption <adp_id> for apply and verify, which otherwise take the batch this machine submitted last.

Editing the plan file. The file is JSON with no values in it. For each item you may change include (true or false), slug, home and tags; for each repo file, a variable maps to an item, to plain (non-secret config) or to skip with a reason. Then check it without changing anything:

mehen adopt plan --check

--check catches what would otherwise fail later: a slug used twice, a field the kind does not have, a private credential mapped to a project variable, a project this machine may not create. It also lists the variables in protected environments (prod, production and any you protected), which you set yourself on the project page after approval.

3. Take the snapshot#

Before you submit, take an encrypted snapshot of every file the plan names:

mehen backup snapshot --plan ~/.config/mehen/adopt/<machine>/plan.json --to ~/Backups/mehen --recipient age1...

Back up a machine before adopting shows how to make the key, where to keep it and how to check the snapshot. Take a new one whenever you plan again or a file changes.

4. Submit#

mehen adopt submit --reason "bring this laptop's keys into the vault"

Submit refuses, with nothing sent, until a passed snapshot on this machine holds every file the plan names, none changed since. It uses the newest such snapshot; --snapshot <snp_id> names another, for example one you have since copied off the machine. Then it checks the plan again, creates the projects the plan needs, reads each value, checks it still matches the scan (a value that changed is skipped and listed as changed since scan), seals it and uploads the batch. It prints what it uploaded and the snapshot it relied on:

uploaded 58 items from sam-laptop to adp_7Hq2xK; snapshot snp_4fQ9cR2mT7 holds every file the plan names

then the review link, https://app.mehen.dev/w/<slug>/b/adp_7Hq2xK, and the next step, mehen wait adp_7Hq2xK, then mehen adopt apply. The batch is open for 24 hours. mehen adopt status adp_7Hq2xK shows where it stands, and mehen pending lists the link again.

5. Approve on the review page#

Open the link. The page lists every item, grouped by repo and folder. For each row you see:

Rows with a problem, only stale sources or a destination no template vouches for start unticked. Untick any row you do not want; it is left out. Change a row's home from its menu, or pick one under Home for every ticked row and choose Set home to move every ticked row at once; Untick stale rows leaves out every row whose sources are all stale. Names cannot be changed here: untick the row and adopt it again under the name you want. Filters show only problems, custom destinations, stale rows or one machine's rows.

Approve stores every ticked row in the home you chose, all or nothing, with one passkey. If any row fails a rule, nothing is stored and the page marks the rows to fix. Cancel batch stores nothing.

6. Apply#

Back on the machine:

mehen adopt apply --dry-run
mehen adopt apply

Apply runs only after approval. For each repo and service in the plan it:

For a service it only prints the change the unit needs (mehen run --env local -- before the command, the working directory, the names to remove from its environment). It never edits a plist or a unit: you, or the tool that owns the unit, make that change (Keep a service running with mehen run). --repo <dir> applies one repo, and --env <env> one environment.

7. Verify#

mehen adopt verify

Verify proves the vault copy works without the local files. For every name in each source file it checks that Mehen provides it for that repo and environment, or that the plan skips it on purpose. It runs mehen env check, and makes one read-only call through the vault for every item whose template has one (for example, listing models for an OpenAI key).

A gap is a name a program would lose if the file went: it is not in the plan, not declared in mehen.toml, does not resolve for that environment, or sits on a line that did not parse. Frameworks such as Bun, Next.js and Vite load .env by themselves, so a gap is exactly what would break. Fix each one (map it, declare it or skip it in the plan, then apply again) until verify says verified. Verify changes no file.

More machines#

Each of your machines adds to the same batch, so you still approve once. On every other machine, scan, plan and take that machine's own snapshot, then:

mehen adopt submit --join adp_7Hq2xK

--join is no exception to the snapshot rule. The batch must still be open and yours. The review page shows which machine each row came from.

What is listed as possibly stale#

The plan lists a source as possibly stale when the file has not changed for 12 months, sits under an archive, archived, old or attic folder or a test or example folder (test, tests, fixtures, testdata, examples), is a backup copy (keys.env.bak-20261027, .env.orig), belongs to a repo with no commit for 12 months or no origin, or a newer file holds the same name with another value. It leaves the item out when every file holding that value is possibly stale; a value also in a fresh file is current, and the item's note counts the stale copies. Adopting a dead key only adds noise. Check each one: if it is still in use, set include to true in the plan; if not, revoke it at the provider.

Undoing#

What comes later#

A later release adds mehen adopt retire: a separate step you run yourself, only for sources whose every item verified and whose repos and services have run cleanly under mehen run for a while. It will move a file to a quarantine folder, never delete it, and print the command to put it back. Until then, removing the originals is up to you, once verify passes and you are sure.