# Passkeys

Register, list, check and remove the passkeys that approve held writes and undos, and what to do when you lose one.

A passkey is how you approve things. When a write is held, or you undo a run, you sign with a passkey: Touch ID, a security key, or a password manager that stores passkeys. Postgres checks the signature before anything changes. The engine, the CLI and the pages only carry it. An agent can open a link but cannot sign, and an agent's role can neither register nor remove a passkey.

For what a signature covers, see [Approvals](/concepts/approvals/) and [Undo](/concepts/undo/).

## Register one

`basalt init` registers your first passkey. After the guard is installed it prints a link:

1. Open the link in a browser on the same machine.
2. Press the button and confirm with your passkey.
3. Copy the code the page shows (it starts with `basalt-passkey.1.`) and paste it at `Paste the code the page shows:`.

You see `basalt: passkey registered for localhost`. The public key is stored in your Postgres. The private key never leaves your device.

To add another passkey, for a second device or a teammate, run init again for the same agent:

```sh
basalt init --agent NAME --add-passkey --passkey-label "work laptop"
```

Re-running init keeps the agent's password. `--skip-passkey` on a first install defers the registration.

## Which devices work

A passkey belongs to the page where you registered it. The engine serves its pages at `http://localhost:8787` by default, so the passkey is for `localhost`. A passkey made on that page signs only on that page, so keep the same port. For a bot on its own computer the pages come from the BasaltVault portal, and the passkey belongs to it.

- **macOS.** Touch ID, or any option listed for Linux.
- **Linux desktop.** There is no built-in passkey store, so use one of these:
  - Your phone, through the browser's QR code flow, only for pages on a real domain such as the BasaltVault portal. Both devices need Bluetooth.
  - A USB or NFC security key. It needs a PIN, which you set with `ykman fido access change-pin` or in the browser's security key settings.
  - A password manager extension that stores passkeys.

A phone through the QR flow is not offered for pages on `localhost` (Chrome on macOS shows no phone option), so approving from a phone needs the pages on a real domain: the hosted portal.

If the engine runs on a headless server, open an SSH tunnel with the same port on both ends, then open the link on your own computer:

```sh
ssh -N -L 8787:127.0.0.1:8787 you@server
```

Register through the same tunnel. Keep it open while `basalt init` waits for the code.

## List your passkeys

```sh
basalt passkey list --agent NAME
```

It prints one line per active passkey: its id, label, where it was registered, when it was registered, and when it last signed something (`never` if it has not). `--json` prints the same as JSON. It never prints key material.

## Check which ones a device still has

Nothing tells BasaltDB when you delete a passkey from a device or a password manager. `basalt passkey check` finds out.

```sh
basalt passkey check --agent NAME --open
```

It opens a page that asks for each passkey in turn. Confirm each one, then press **Done** and paste the code the page shows. The command prints each passkey as `found on this device` or `not found on this device`. For each one not found it asks `Remove <label> (<id>)? [y/N]`. Nothing is removed unless you answer `y`. A check signature authorizes nothing: it cannot approve a write or an undo.

## Remove one

Remove a passkey you lost, or one on a device you no longer use. This needs the admin connection string, at a hidden prompt, because it changes your database.

```sh
basalt passkey remove PASSKEY_ID
```

`PASSKEY_ID` is the full id from `basalt passkey list`, or a unique start of it with at least 8 characters. The command shows the label, where it was registered and when it last signed, and asks `Remove passkey ...? [y/N]`. A removed passkey is refused from then on: it can no longer approve a write or sign an undo. The removal is recorded in the ledger.

Removing a passkey does not delete it from the device. The command prints a link ending `/passkeys/forget`. Open it on each device that holds the passkey, so its password manager drops it. `--open` opens the link for you. Not every browser supports this. If yours does not, delete the passkey by hand in your password manager.

### The last passkey

With no passkey, nothing held can be approved and no undo can be signed. So `basalt passkey remove` refuses to remove the last one. `--force` removes it, and you then type its label at the prompt, even with `--yes`. Add another passkey first, or run this afterwards:

```sh
basalt init --agent NAME --add-passkey
```

## If you lose a device

1. If you have another passkey, run `basalt passkey list --agent NAME` and `basalt passkey remove PASSKEY_ID` for the lost one.
2. If it was your only passkey, run `basalt init --agent NAME --add-passkey` first to register a new one, then remove the old one.
3. Held writes wait until you can sign. They expire after 24 hours.

## When BasaltDB tells you

`basalt log` warns when no passkey is active. It also warns about a passkey that has not signed anything for 90 days, counted from registration if it never did. It names the passkey and says to use `basalt passkey remove`. Nothing is removed automatically. `basalt status --agent NAME` reports a missing passkey too. The agent sees the same as `NO_PASSKEY`. See [When something is refused](/guides/refused/#held).

## Next

- [Approvals](/concepts/approvals/): what a signature covers and when it expires.
- [Undo a run](/guides/undo/): sign an undo.
- [Remove BasaltDB](/guides/remove/): the passkey on your devices stays after you uninstall.
