# Bring a machine's credentials into Mehen

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](https://docs.mehen.dev/guides/existing-keys.md) is quicker.

## What adopt does, and what it never does

- **It never moves, rewrites or deletes an original.** Every `.env`, settings file, login file and service unit stays byte for byte as it was. You decide later when to remove them.
- **Values are sealed on the machine.** `mehen adopt submit` reads each value in its own process and seals it to the vault before it leaves. No value, part of one, its length or a hash of it is printed, written to a file or shown to an agent.
- **A human approves the whole batch.** You review every row on one page in the vault and confirm with one passkey. Nothing is stored until then.
- **You keep your own copy.** Submit is refused until a snapshot of every file the plan names exists, encrypted to a key that is on no enrolled machine ([Back up a machine before adopting](https://docs.mehen.dev/guides/backup.md)).

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

```sh
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:

- The last line counts what it found: `found 41 files, 96 secret variables (58 distinct values), 30 config, 4 unsupported; wrote ~/.config/mehen/adopt/<machine>/scan.json`. Distinct values tell you how many copies of the same key you have.
- A line such as `~/code/api/.env:5: not NAME=value` is a line that did not parse. It is never quoted, only its line number.
- `--json` prints the counts, one row per folder with its proposed home and how many secrets, config, unknown, unsupported and possibly stale names it holds, and one row per file that did not parse, with its number of lines (the first 50 files). The scan file lists every name with its file, its class (`secret`, `config` or `unknown`), the template it matched (for example `openai`) and why. Values that are equal share a duplicate label such as `d3`; the label is random on each run and says nothing about the value.

## 2. Plan

```sh
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](https://docs.mehen.dev/concepts/credentials.md)):

- a repo's `.env` goes to that repo's project. If the repo has a `mehen.toml`, that project; otherwise submit creates one bound to the repo's `origin` remote;
- a service's env file goes to a project for this machine's services, `<machine>-services`;
- `.local-secrets`, settings files and login files go to your private home.

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:

```sh
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:

```sh
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:

```sh
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:

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

[Back up a machine before adopting](https://docs.mehen.dev/guides/backup.md) 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

```sh
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:

- the name, the service it is for and where it sends the credential. A destination from a template is marked as such; one the agent proposed is shown in red with every host spelled out, for you to check;
- where it belongs, as a menu of the homes you may choose;
- each secret field as its length, its well-known prefix (such as `sk-proj-`) and a fingerprint, never the value;
- where it was found: machine, file, variable and any stale hints, and the other rows that hold the same value.

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:

```sh
mehen adopt apply --dry-run
mehen adopt apply
```

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

- writes a `mehen.toml` with every mapped name set to `"vault"`, where there is none. Where there is one, it changes nothing and prints the lines to add under `[vars]`;
- sets each variable in the vault: secrets point at their new credential, and plain config becomes a vault plain value, never a literal in the committed file;
- runs `mehen env check` for each environment;
- lists the variables in protected environments for you to set on the project page, and any `.env` that is committed to git, whose values you should rotate because history still holds them.

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](https://docs.mehen.dev/guides/services.md)). `--repo <dir>` applies one repo, and `--env <env>` one environment.

## 7. Verify

```sh
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:

```sh
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

- **Before approval:** `mehen adopt cancel adp_7Hq2xK`, or **Cancel batch** on the page. The staged values are dropped and nothing is stored. An unapproved batch also ends by itself after 24 hours.
- **After approval:** your files are as they were, so nothing local needs undoing. Delete a `mehen.toml` that apply wrote if you do not want it, and delete any credential you did not mean to keep from its page under Credentials in the web app.
- **A file lost or changed by something else:** restore it from the snapshot with `mehen backup restore` ([Back up a machine before adopting](https://docs.mehen.dev/guides/backup.md#5-restore)).

## 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.
