# CLI

Every basalt command and flag, with what it does, which credential it needs and its exit codes.

`basalt` is one binary. A person runs most of its commands. The agent's MCP client runs `basalt mcp`. The commands that change your database ask for your admin connection string. The rest read with the agent's credential or, when the engine runs as a service, ask the engine.

| Command                                                                                               | What it does                                                           |
| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`basalt init`](#basalt-init)                                                                         | Sets up the guard, an agent, its passkey and the engine.               |
| [`basalt init --remote`](#basalt-init-remote)                                                         | On the engine's machine: makes a setup code, with no admin credential. |
| [`basalt init --setup-code`](#basalt-init-setup-code)                                                 | On your own computer: runs a setup code.                               |
| [`basalt log`](#basalt-log)                                                                           | Prints the agent's change records.                                     |
| [`basalt approve`](#basalt-approve)                                                                   | Approves a held write.                                                 |
| [`basalt revert`](#basalt-revert)                                                                     | Previews and applies an undo.                                          |
| [`basalt passkey`](#basalt-passkey)                                                                   | Lists, checks and removes passkeys.                                    |
| [`basalt classify`](#basalt-classify)                                                                 | Suggests and records a class for each readable column.                 |
| [`basalt status`](#basalt-status)                                                                     | Prints what is ready and what is not.                                  |
| [`basalt upgrade`](#basalt-upgrade)                                                                   | Applies the guard migrations your database lacks.                      |
| [`basalt uninstall`](#basalt-uninstall)                                                               | Removes the guard and the roles.                                       |
| [`basalt mcp`](#basalt-mcp), [`engine`](#basalt-engine), [`service`](#sudo-basalt-service), `version` | Run the agent's MCP server, the engine and the service.                |

## Everything that applies to all commands

- Machine output with `--json` uses the same field names as the [MCP tools](/reference/mcp-tools/): camelCase keys, SCREAMING_SNAKE_CASE values.
- Paged output prints the next cursor on stderr, so stdout stays clean.
- Exit codes: `0` ok, `1` error, `2` usage.
- Flag values that you type are lowercase and kebab-case, with an explicit default, such as `--agent-rls=ask`. What is stored or sent is the enum they map to.
- Commands that read as the agent (`log`, `approve`, `revert`, `passkey`) pick the credential in this order: `--credential-file`, then `--agent NAME` looked up in the engine config, then `BASALT_DATABASE_URL`. A flag always wins over the environment.
- When the engine runs as a background service, `log`, `approve`, `revert`, `status` and `upgrade` with `--agent NAME` go through the engine's person socket and never read the config or the credential file. If the engine is stopped they print the command to start it and exit `1`. See [Configuration](/reference/config/).
- `log`, `approve`, `revert`, `passkey list` and `passkey check` also take `--agent NAME`, `--config PATH` and `--credential-file PATH`. `--config` is the engine config to look the agent up in. It defaults to the system path in [Configuration](/reference/config/#where-the-files-live).
- The admin connection string comes from `BASALT_ADMIN_DATABASE_URL` or a hidden prompt. It is never written anywhere.

## `basalt init`

```sh
basalt init --agent NAME [--write schema.table,...] [--read schema.table,...]
```

Shows the setup SQL with the agent's password redacted, asks to confirm unless `--yes`, then:

1. Installs or upgrades the guard (`basalt_meta`) and attaches it to your tables.
2. Creates the agent's role. An existing role keeps its password unless `--rotate-password`. Init sends Postgres only the password's SCRAM-SHA-256 verifier, never the password. An existing role is taken over only if it is a plain agent role.
3. Registers your passkey: it prints and opens a registration link and reads the pasted `basalt-passkey.1.` code. Skip with `--skip-passkey`.
4. Sets up the engine: asks `Install the engine as a background service?` at a terminal.
5. With the `claude` CLI on your `PATH`, offers to run `claude mcp add basalt-NAME -- basalt mcp --agent NAME`.

Before it shows the SQL, init looks for tables where row-level security hides every row from the agent role. At a terminal it asks once per table whether the agent may see all rows. After installing, it prints how many readable columns are not classified yet, and tells you to run `basalt classify`.

| Flag                  | Default                                     | What it does                                                                                                                                                                                                                                                                                                 |
| --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--agent NAME`        | required                                    | The agent's name: lowercase letters, digits and `_`. Its role is `<prefix>_agent_<name>`.                                                                                                                                                                                                                    |
| `--write`             | none                                        | Comma-separated `schema.table` names the agent may read and write.                                                                                                                                                                                                                                           |
| `--read`              | none                                        | Comma-separated `schema.table` names the agent may only read.                                                                                                                                                                                                                                                |
| `--prefix`            | `basalt`                                    | Role name prefix.                                                                                                                                                                                                                                                                                            |
| `--statement-timeout` | `30s`                                       | The query time limit of the agent's role.                                                                                                                                                                                                                                                                    |
| `--config`            | the system default                          | The engine config to add the agent to. Another path means you run the engine yourself.                                                                                                                                                                                                                       |
| `--socket-dir`        | the system default                          | The engine's socket directory, when the config is new.                                                                                                                                                                                                                                                       |
| `--os-user`           | you                                         | The OS user the agent runs as, the only one allowed on its socket.                                                                                                                                                                                                                                           |
| `--engine-user`       | the service user                            | The OS user the engine service runs as.                                                                                                                                                                                                                                                                      |
| `--credential-file`   | `<config dir>/agents/NAME.url`              | Where to write the agent's connection string.                                                                                                                                                                                                                                                                |
| `--person-uid N`      | you                                         | An OS user that may use the person socket. Repeat for a second person. Refused for uid `0` and the engine's own user.                                                                                                                                                                                        |
| `--rotate-password`   | off                                         | Sets a new password for an existing agent role. Restart the engine after.                                                                                                                                                                                                                                    |
| `--agent-rls`         | `ask`                                       | `ask`, `all-rows` or `none`. What to do about tables where row-level security hides every row. `all-rows` adds the agent policy without asking.                                                                                                                                                              |
| `--operated-by`       | `person`                                    | `person` or `agent`. Who runs machine steps on the engine's machine.                                                                                                                                                                                                                                         |
| `--passkey-origin`    | `http://localhost:8787`                     | The pages the passkey belongs to: the engine's local pages, or the hosted portal `https://portal.basaltvault.app` (RP ID `basaltvault.app`; the default with `--operated-by=agent`). Init saves it as `pagesUrl`. On an engine your bot operates the pages are the portal, `https://portal.basaltvault.app`. |
| `--vault`             | `on` with `--operated-by=agent`, else `off` | `on` or `off`. Whether the engine reaches the portal's pages through the Vault. `on` with a local pages origin is a usage error.                                                                                                                                                                             |
| `--passkey-label`     | `<database> · <agent> · <date>`             | The passkey's name in the picker.                                                                                                                                                                                                                                                                            |
| `--add-passkey`       | off                                         | Registers another passkey even if one is registered.                                                                                                                                                                                                                                                         |
| `--skip-passkey`      | off                                         | Does not register a passkey now.                                                                                                                                                                                                                                                                             |
| `--no-service`        | off                                         | Does not install the engine service. Prints the foreground command.                                                                                                                                                                                                                                          |
| `--yes`               | off                                         | Does not ask before running the SQL or installing the service. Without a terminal this is the only way to add the agent policy, with `--agent-rls=all-rows`.                                                                                                                                                 |
| `--dry-run`           | off                                         | Prints the SQL and the engine setup, and runs nothing.                                                                                                                                                                                                                                                       |
| `--demo`              | off                                         | Creates and guards a sample `basalt_demo` schema (`leads`, `lead_notes`) and a demo agent that can write only there.                                                                                                                                                                                         |

Init saves the flags it was run with in `engine.json` as `initArgs`, so every "run init again" step uses the same ones. See [Configuration](/reference/config/#agent-keys). Re-running init keeps the agent's password.

When the engine runs as a service, init runs `sudo basalt service install` for you. It refuses a `basalt` binary that anyone but root could change. See [Install](/start/install/).

<a id="basalt-init-remote"></a>

## `basalt init --remote`

For an engine on a machine you cannot reach, run by your bot. The bot runs this on the engine's machine and gets a setup code. It needs no admin credential.

```sh
basalt init --remote --agent NAME [--write ...] [--read ...] [--prefix P] \
  [--statement-timeout D] [--operated-by person|agent] [--database-url URL] \
  [--new-code] [--config PATH] [--json]
```

| Flag             | What it does                                                                                                                           |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `--database-url` | The connection string without a password. Required on the agent's first run. A URL with a password is refused, and nothing is written. |
| `--new-code`     | Makes a new code with a new password for the agent.                                                                                    |
| `--json`         | Prints the setup code, its fingerprint, a `personMessage` and `nextSteps`.                                                             |

It makes the agent's password on this machine and writes it to the credential file. The code holds only a verifier. The code is valid for 24 hours and works once. It does not install or start the engine. Exit `0` means the setup is pending, not failed. See [A bot on its own computer](/guides/bot-own-computer/).

<a id="basalt-init-setup-code"></a>

## `basalt init --setup-code`

On your own computer. It runs the code the bot gave you, with your admin connection string.

```sh
basalt init --setup-code CODE [--yes] [--agent-rls=ask|all-rows|none] [--add-passkey] [--passkey-label LABEL] [--dry-run]
```

It decodes the code, checks that it is not expired and names the database you meant, shows a summary and a fingerprint to compare with the one your agent shows, shows the SQL and asks `Run this? [y/N]`. It runs everything in one transaction, registers a passkey on the portal if none is active, and ends with `Done. Tell NAME to check its status.` It writes no engine config and no credential file. macOS and Linux only.

It refuses with nothing changed when the code is expired (`EXPIRED`), claims a time in the future, needs a newer `basalt` (`BINARY_TOO_OLD`), names a different database than your admin connection string, was already used (`USED`), was superseded by a newer one (`SUPERSEDED`), names a different prefix, would replace the password of a role that already exists (`ROLE_EXISTS`), is an `AGENT` code for a role that does not exist (`ROLE_MISSING`), or was made for a guard older than version 16 (`GUARD_TOO_OLD`). Ask the agent for a new code.

## `basalt log`

```sh
basalt log --agent NAME [--run ID] [--limit N] [--cursor CURSOR] [--full] [--sort -id|id] [--json] [--full-check]
```

Prints the agent's change records, newest first. With `--run` it also prints the rows each change wrote, indented below it. Reads are collapsed into one line.

| Flag           | Default | What it does                                                                                 |
| -------------- | ------- | -------------------------------------------------------------------------------------------- |
| `--run ID`     | none    | One run, with each row it wrote.                                                             |
| `--limit N`    | `50`    | Page size, at most 200.                                                                      |
| `--cursor`     | none    | The next-page cursor. See below.                                                             |
| `--full`       | off     | Whole before and after rows, and every read on its own line.                                 |
| `--sort`       | `-id`   | `-id` is newest first, `id` is oldest first. Any other value is a usage error.               |
| `--json`       | off     | The raw change records. See [Change record](/reference/change-record/).                      |
| `--full-check` | off     | Checks the whole hash chain from the start. By default it checks from where it last checked. |

When there is another page, `basalt log` prints `next cursor: VALUE` on stderr. To read the next page, run the same command again with `--cursor VALUE`:

```sh
basalt log --agent NAME --limit 20
basalt log --agent NAME --limit 20 --cursor VALUE
```

Keep `--sort` the same between pages. The cursor is opaque. Do not edit it.

It warns on stderr about a broken chain, unguarded tables, no engine heartbeat for 7 days, no active passkey, a passkey unused for 90 days, row-level security that hides every row and columns that are not classified. With a guard of version 15 or later it prints a line such as `PII: 12 columns classified, 0 unclassified; dictionary of 4,310 values, last scan 2026-11-13T05:40:02Z`. See [See what it did](/guides/see-what-it-did/).

## `basalt approve`

```sh
basalt approve --agent NAME --open APPROVAL_ID
basalt approve --agent NAME --show APPROVAL_ID
basalt approve CODE
```

| Form / flag       | What it does                                                                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--show ID`       | Prints the approval link for a held write, after a headline on stderr that states its cascaded rows.                                              |
| `--open ID`       | The same, but opens the link in your browser. If none opens, it prints the link.                                                                  |
| `--reissue`       | With `--show` or `--open`: uses a newly issued short link and revokes the earlier one. The page then relays your signature, so no code is pasted. |
| `CODE`            | Decodes a `basalt-approval.1.` code from the long-link page, checks it against your stored passkey and submits it.                                |
| `--pages-url URL` | The pages base URL. Default: the engine config's `pagesUrl` for `--agent`, else `http://localhost:8787`.                                          |

The CLI only relays. Postgres checks the signature. How approving works: [Approvals](/concepts/approvals/).

## `basalt revert`

```sh
basalt revert --agent NAME (--change ID | --run ID | --task ID) [--intent TEXT] [--dry-run] [--open] [--json]
basalt revert --agent NAME --apply CODE
basalt revert --agent NAME --status REVERT_ID
basalt revert --agent NAME --reject REVERT_ID [--note TEXT]
```

It prepares the undo, prints every line on stdout (rows to restore with the columns that change back, then skipped rows with the reason), prints the link on stderr and waits for your approval. It exits `0` even when some rows were skipped. Without an approval it exits `1` and nothing is reverted.

| Flag                 | What it does                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--change ID`        | Undo one change. A revert's own change undoes that revert.                                                   |
| `--run ID`           | Undo every applied change of one run.                                                                        |
| `--task ID`          | Undo the changes with that task id.                                                                          |
| `--intent TEXT`      | Why. Default `Revert <scope> <id>`.                                                                          |
| `--dry-run`          | Prints the preview only. Nothing is prepared.                                                                |
| `--open`             | Opens the revert link in your browser instead of printing it.                                                |
| `--apply CODE`       | Applies a prepared revert later, within its hour, from a `basalt-revert.1.` code.                            |
| `--status REVERT_ID` | Prints a large plan's progress: restored, skipped, pending, batches, `applyBy` and the lines skipped so far. |
| `--reject REVERT_ID` | Rejects a pending revert, as the page's Reject does. `--note TEXT` adds a note the agent can read.           |
| `--json`             | Machine output.                                                                                              |
| `--pages-url URL`    | The pages base URL, as for `approve`.                                                                        |

When the engine serves the local pages, it prints the short link `<base>/reverts/<revertId>` and waits for the page to apply the revert, while still accepting a pasted code. A large plan (over 1000 lines) prints its size instead of every line. After you sign it, it prints `basalt: tell your agent: apply revert REVERT_ID in batches`. Rows to restore come first, in a readable order: re-inserts, then restored updates, then deletes, then skipped rows. See [Undo a run](/guides/undo/).

Row limits do not apply to an undo. It ends with `basalt: reverted: N rows restored, M left as they are`. When you pasted a code instead of the page relaying your signature, the line ends with ` (change ID)`, the id of the undo's own change; a relayed revert has no change id there, and `basalt log` shows it.

## `basalt passkey`

```sh
basalt passkey list --agent NAME [--json]
basalt passkey check --agent NAME [--pages-url URL] [--open]
basalt passkey remove CREDENTIAL_ID [--force] [--yes] [--pages-url URL] [--open]
```

| Command  | What it does                                                                                                                                                                                                                   |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list`   | The database's active passkeys: credential id, label, RP ID, when registered and when last used (`never`). `--json` prints `credentialId`, `label`, `rpId`, `origin`, `createdAt` and `lastUsedAt`. Never prints key material. |
| `check`  | Which registered passkeys this device can still produce. Prints a check link, reads the pasted `basalt-passkey-check.1.` code, and for each passkey not found asks whether to remove it. A check signature authorizes nothing. |
| `remove` | Revokes a passkey in Postgres with your admin connection string. The id is the full id or a unique prefix of at least 8 characters. The last active passkey is refused unless `--force`, and then you type its label.          |

`remove` then prints a forget link to open on each device that holds the passkey, so its password manager drops it. `--open` opens it. See [Passkeys](/guides/passkeys/).

## `basalt classify`

```sh
basalt classify [--review=changed|all] [--set schema.table.column=class[:entity-type]]... \
  [--phone-region=auto|XX] [--yes] [--dry-run] [--json] [--check]
```

Suggests a class for every column an agent can read and records the classes you confirm. It uses your admin connection string. It needs the guard at version 15 or later. No value is ever printed or stored in plain form. The engine warns about columns that are not classified; it does not refuse them.

| Flag             | Default   | What it does                                                                                                                                 |
| ---------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `--review`       | `changed` | `changed` shows new, unclassified, retyped and flagged columns, and those whose suggestion differs. `all` shows every readable column.       |
| `--set`          | none      | Repeatable. Sets a column's class without the review. The entity type is required with `identifier` and refused with any other class.        |
| `--phone-region` | `auto`    | `auto` infers the region per phone column. An ISO 3166-1 alpha-2 code such as `EE` is used for every phone column of the run.                |
| `--yes`          | off       | Accepts every suggestion shown. It never lowers a class you confirmed. Without a terminal, `classify` writes only with `--yes` or `--set`.   |
| `--dry-run`      | off       | Prints the review. Asks nothing and writes nothing.                                                                                          |
| `--json`         | off       | Prints the review as JSON and asks nothing. It writes only with `--yes` or `--set`.                                                          |
| `--check`        | off       | Writes nothing and shows every column. For each identifier column it samples values and reports how many do not come back to the same token. |

Classes are `public`, `identifier`, `free-text`, `secret` and `unclassified`. Entity types, for `identifier` only, are `email`, `phone`, `person-name`, `address` and `other`. A column that is not readable, or a value outside these spellings, is a usage error.

## `basalt status`

```sh
basalt status [--agent NAME] [--config PATH] [--json]
```

The twin of the [`status` tool](/reference/mcp-tools/#status), for you. It prints the config path, whether the engine runs (pid and OS user), the socket and state directories, the pages URL, `operatedBy` and, per agent, the guard version against the build (up to date, upgrade available, upgrade required), whether a passkey is registered and each problem with its message and command. It never prints a secret. It exits `0` whatever the problems.

Without `--agent` it reports the config's only agent. With several agents `--agent` is required (exit `2`). `--json` is the `status` tool's result plus `configPath`, `engine.pid`, `engine.osUser`, `socketDir` and `stateDir`.

## `basalt upgrade`

```sh
basalt upgrade --agent NAME [--config PATH] [--yes] [--dry-run]
```

Applies only the guard migrations this build has and your database lacks. It never rotates the agent's password, adds no policies, registers no passkey and needs no engine restart. It stops with exit `0` when the guard is current. It refuses a guard that is not installed (that is `basalt init`'s job) and a guard newer than this build. It asks for your admin connection string once, and refuses one that names a different database than the agent's credential. It prints `This upgrades Basalt's guard from version FROM to TO; no agent changes.` and the SQL. `--dry-run` stops there. Otherwise it asks `Run this? [y/N]`, unless `--yes`. See [Upgrade](/guides/upgrade/).

On an engine your bot operates, `basalt upgrade` refuses and does not ask for your admin connection string. The upgrade goes through a setup code instead.

## `basalt uninstall`

```sh
basalt uninstall [--prefix P] [--yes]
```

Drops the guard's row-level security policies, `basalt_meta` and the roles. It takes your admin connection string the same way as `init`.

| Flag       | Default  | What it does                                                                                                               |
| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `--prefix` | `basalt` | The role name prefix. Use the one you gave `basalt init`.                                                                  |
| `--yes`    | off      | Does not ask `Drop basalt_meta, the ledger, every basalt_* role and the demo schema basalt_demo, if init --demo made it?`. |

Stop the engine first with `sudo basalt service uninstall`. See [Remove BasaltDB](/guides/remove/).

## `basalt mcp`

```sh
basalt mcp --agent NAME [--config PATH] [--socket-dir DIR] [--socket PATH]
```

The agent's MCP server. The agent's MCP client starts it. It relays to the engine, holds no credential and reaches no database. When the engine is down it serves `status` and the tool list, and answers every other call `UNAVAILABLE` with the reason `ENGINE_UNREACHABLE`.

| Flag           | Default            | What it does                                                                                  |
| -------------- | ------------------ | --------------------------------------------------------------------------------------------- |
| `--agent NAME` | none               | The agent. It names the socket `<socket-dir>/NAME.sock`.                                      |
| `--socket-dir` | the system default | The engine's socket directory.                                                                |
| `--socket`     | none               | The engine socket path. Overrides `--agent` for the socket.                                   |
| `--config`     | the system default | The engine config, read when it can be so that `status` still works while the engine is down. |

## `basalt engine`

```sh
basalt engine --config PATH
```

Runs the engine in the foreground. Used when you run init with `--no-service`, and by a bot that runs its own engine.

## `sudo basalt service`

```sh
sudo basalt service install [flags]
sudo basalt service uninstall [--user USER] [--config PATH] [--binary PATH] [--dry-run]
```

Installs or removes the engine's background service: its OS user, the config, the credential file and the systemd or launchd unit. `basalt init` runs `install` for you. You run it yourself to rewrite the unit, for example after raising `pii.maxMemoryMB`, using the same flags. It prints every step first. `--dry-run` stops there. It refuses a `basalt` binary that anyone but root could change.

| Flag                         | Default                 | What it does                                                                                                      |
| ---------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--user USER`                | the service user        | The OS user the engine runs as.                                                                                   |
| `--config PATH`              | the system default      | The engine config file.                                                                                           |
| `--binary PATH`              | this binary             | The absolute path of the `basalt` binary.                                                                         |
| `--dry-run`                  | off                     | Prints the steps without running them.                                                                            |
| `--agent NAME`               | none                    | `install` only: also place this agent. This is what init runs under `sudo`.                                       |
| `--os-user USER`             | none                    | With `--agent`: the agent's OS user. Required.                                                                    |
| `--credential-stdin`         | off                     | With `--agent`: read the agent's connection string from stdin. It never goes on the command line.                 |
| `--keep-credential`          | off                     | With `--agent`: keep the agent's credential file, when its role kept its password. Not with `--credential-stdin`. |
| `--person-uid N`             | `SUDO_UID`              | With `--agent`: an OS user id the person socket admits. Repeat for a second person. Refused for uid `0`.          |
| `--init-arg FLAG`            | none                    | With `--agent`: a flag init saves for the agent, such as `--write=a.b`. Repeat. These become `initArgs`.          |
| `--pages-url URL`            | the config's `pagesUrl` | With `--agent`: the pages base URL the agent's passkey belongs to.                                                |
| `--vault`, `--vault-default` | keep the config's       | With `--agent`: `on` or `off`. `--vault-default` sets it only when the config has none yet.                       |

## `basalt version`

Prints the version.

The agent's own tools are in [MCP tools](/reference/mcp-tools/). Every error an agent can get is in [Error codes](/reference/errors/).
