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 |
Sets up the guard, an agent, its passkey and the engine. |
basalt init --remote |
On the engine’s machine: makes a setup code, with no admin credential. |
basalt init --setup-code |
On your own computer: runs a setup code. |
basalt log |
Prints the agent’s change records. |
basalt approve |
Approves a held write. |
basalt revert |
Previews and applies an undo. |
basalt passkey |
Lists, checks and removes passkeys. |
basalt classify |
Suggests and records a class for each readable column. |
basalt status |
Prints what is ready and what is not. |
basalt upgrade |
Applies the guard migrations your database lacks. |
basalt uninstall |
Removes the guard and the roles. |
basalt mcp, engine, service, version |
Run the agent’s MCP server, the engine and the service. |
Everything that applies to all commands
Section titled “Everything that applies to all commands”- Machine output with
--jsonuses the same field names as the MCP tools: camelCase keys, SCREAMING_SNAKE_CASE values. - Paged output prints the next cursor on stderr, so stdout stays clean.
- Exit codes:
0ok,1error,2usage. - 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 NAMElooked up in the engine config, thenBASALT_DATABASE_URL. A flag always wins over the environment. - When the engine runs as a background service,
log,approve,revert,statusandupgradewith--agent NAMEgo 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 exit1. See Configuration. log,approve,revert,passkey listandpasskey checkalso take--agent NAME,--config PATHand--credential-file PATH.--configis the engine config to look the agent up in. It defaults to the system path in Configuration.- The admin connection string comes from
BASALT_ADMIN_DATABASE_URLor a hidden prompt. It is never written anywhere.
basalt init
Section titled “basalt init”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:
- Installs or upgrades the guard (
basalt_meta) and attaches it to your tables. - 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. - Registers your passkey: it prints and opens a registration link and reads the pasted
basalt-passkey.1.code. Skip with--skip-passkey. - Sets up the engine: asks
Install the engine as a background service?at a terminal. - With the
claudeCLI on yourPATH, offers to runclaude 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. 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.
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.
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.
basalt init –setup-code
On your own computer. It runs the code the bot gave you, with your admin connection string.
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
Section titled “basalt log”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. |
--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:
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.
basalt approve
Section titled “basalt approve”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.
basalt revert
Section titled “basalt revert”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.
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
Section titled “basalt passkey”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.
basalt classify
Section titled “basalt classify”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
Section titled “basalt status”basalt status [--agent NAME] [--config PATH] [--json]
The twin of the status tool, 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
Section titled “basalt upgrade”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.
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
Section titled “basalt uninstall”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.
basalt mcp
Section titled “basalt mcp”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
Section titled “basalt engine”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
Section titled “sudo basalt service”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
Section titled “basalt version”Prints the version.
The agent’s own tools are in MCP tools. Every error an agent can get is in Error codes.
BasaltDB · Apache-2.0