Skip to content
BasaltDBBasaltDB
Star

CLI

View as Markdown

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.
  • Machine output with --json uses 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: 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.
  • 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.
  • The admin connection string comes from BASALT_ADMIN_DATABASE_URL or a hidden prompt. It is never written anywhere.
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. 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 --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 --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 --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 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 [--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 [--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 --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 [--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 --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 --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 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.

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