dbcli Documentation

A security-first database CLI for human developers and AI agents — one interface for SQL, MongoDB, Redis, and Elasticsearch.

permission, blacklist, dry-run, and agent skills are defence in depth, not a replacement for database authorization. Autonomous agents must receive only least-privilege database credentials; a process that can edit its own config can otherwise raise dbcli's declared permission or use another client.

Core Philosophy & Security

Built with a "Security-First" mindset, dbcli prevents accidental data leaks or corruption through multiple layers of protection:

Permission Guard

Four tiers (query-only to admin) of strict access control for every operation, judged by what a statement does rather than by its leading keyword — stacked statements, data-modifying CTEs, SELECT … INTO and MongoDB $out / $merge are all refused rather than read as reads. An Elasticsearch request is judged by scope: deleting one document is data-admin, deleting an index is admin.

Blacklist Manager

Automatic redaction of sensitive columns and tables from all outputs to ensure privacy. The decision covers every table a statement references, not just the leading one — a blacklisted table brought in by a JOIN, a comma, a UNION, or a subquery is blocked the same way. Redis saved queries and report diagnostics resolve key targets before execution, apply the connection's blacklist and mask rules, and remove protected key names from SCAN evidence.

Query Risk Analyzer

Static SQL analysis (plan) to identify risks before connecting to any live database.

Antigravity Protocol

A workflow separation between Architect (Planning) and Builder (Execution) to ensure strategy precedes action.

Getting Started

Install globally using your preferred package manager:

# Using Bun (required at run time)
bun install -g @carllee1983/dbcli

# Using npm
npm install -g @carllee1983/dbcli

dbcli runs on Bun 1.4.0+. npm and npx are supported as distribution channels, but the installed dbcli executable requires Bun on your PATH; only the ./agent-core subpath export is importable from a plain Node process.

Standalone dbcli --version and dbcli -V checks use a lightweight launcher and do not load database drivers. Other invocations load the full command runtime as usual.

Initialize your first connection with the interactive wizard:

dbcli init

For MongoDB, init walks through connection fields one at a time (Host, SRV, Port, User, Password, authSource, then optional replicaSet / tls) — see "MongoDB connection configuration" in the Engine Support Matrix section below for the full field list and how --uri is decomposed.

Every new or additional connection needs a nonblank connection name. Pass --conn-name <name>, or run dbcli init in an interactive terminal to be asked for one. The name prompt has no suggested default; empty or whitespace-only answers explain the requirement and prompt again. Cancelling or ending the name prompt stops before any connection test or configuration/credential write. An explicit nonblank --conn-name skips this prompt, and --conn-name default is valid. An explicitly blank --conn-name is an error in every mode, before connection tests or writes. --name still means the database name (the logical DB index for Redis), never the connection name.

Without a TTY, with --no-interactive, or when --use-env-refs plus all five --env-* options automatically disables interaction, a missing or blank --conn-name fails with guidance to supply it, before database access or configuration/credential writes. Complete credentials, --name, --env-file, and --force do not supply a connection name.

New initialization always creates named v2 configuration, including plain init and explicit --config paths; it no longer creates an unnamed v1 configuration. Existing v1 configurations remain readable and their established named v2 import is retained. The first explicitly named connection is still the default in a new registry; adding a connection preserves an existing registry's default or no-default state. An explicitly chosen duplicate name still requires interactive overwrite confirmation or --force; a noninteractive duplicate without --force, or a declined confirmation, leaves files untouched. --remove and --rename do not ask for a new connection name.

Behind the scenes, init writes a small version: 3 binding stub to ./.dbcli/config.json in your project, while the real connection settings and any credentials are stored in your home directory at ~/.config/dbcli/projects/<project-name>-<sha1-12>/. This keeps recoverable secrets out of the project workspace, so tools or AI agents that scan the repo never see them. The project .dbcli/ only holds the binding plus non-sensitive caches (schema cache, audit log, snapshots, verification artifacts).

For a connection shared by several projects, use the explicit global scope. It stores a v2 registry in ~/.config/dbcli/config.json and never creates a project binding:

dbcli --global init --conn-name shared --system postgresql \
  --host db.example.com --port 5432 --user app --password '<secret>' \
  --name appdb --skip-test --no-interactive --force
dbcli --global use --list --format json
dbcli --global status --format json

Root-level --global must appear before the command. Global and project registries are independent; commands continue to use the project registry unless --global is supplied. The global file is protected with the same integrity record and private file mode as home-stored project settings.

Masked credential entry

Interactive init collects the database password — and a pasted MongoDB connection string, which carries one — through a masked prompt, so the value never lands in terminal scrollback, a session recording, or a screen share. Hosts, ports, usernames, database names, and environment-variable names stay ordinary visible prompts.

There is no plain-text fallback. If masked input cannot be provided — no terminal attached, or the prompt implementation unavailable — init stops before writing any configuration and names an input you can use instead: --password, a credential parsed from .env or the process environment, --use-env-refs, or --uri for MongoDB. An explicit --password or --uri is kept exactly as given and is never offered back as a visible prompt default, and --no-interactive never reaches a secret prompt at all.

If the connection test then fails, the driver's own message and hints are redacted and bounded before they reach the terminal, because drivers routinely quote the credential or the whole connection URI back in an error.

This is about terminal echo, not storage: dbcli does not encrypt credentials at rest. Keeping them out of the config file is what --use-env-refs and home-directory storage are for.

Verified TLS for PostgreSQL, MySQL and MariaDB

Use both SQL TLS flags when creating a named PostgreSQL, MySQL or MariaDB profile. This example prompts for the password and tests the connection before saving:

dbcli init --conn-name verified-pg --system postgresql \
  --host db.example.com --port 5432 --user reader --name appdb \
  --permission query-only --sql-tls verify-full --sql-tls-ca ./certs/root-ca.pem
dbcli --use verified-pg blacklist list
dbcli --use verified-pg schema public.events --format json
dbcli --use verified-pg query "SELECT id FROM public.events LIMIT 10" --format json

MySQL and MariaDB use the same flags, with separate named profiles and CA paths. Select the corresponding profile, then check blacklist and schema before an authorized query:

dbcli init --conn-name verified-mysql --system mysql \
  --host mysql.example.com --port 3306 --user reader --name appdb \
  --permission query-only --sql-tls verify-full --sql-tls-ca ./certs/mysql-ca.pem
dbcli init --conn-name verified-mariadb --system mariadb \
  --host mariadb.example.com --port 3306 --user reader --name appdb \
  --permission query-only --sql-tls verify-full --sql-tls-ca ./certs/mariadb-ca.pem

Replace the host, database and table with your authorized target; confirm real columns through schema before querying. The saved connection includes this dedicated SQL object (shown as a connection fragment):

{
  "sqlTls": {
    "mode": "verify-full",
    "caPath": "/absolute/path/to/certs/root-ca.pem"
  }
}

--sql-tls verify-full and --sql-tls-ca <path> are required together. Empty values, other modes and unsupported engines fail explicitly. PostgreSQL, MySQL and MariaDB support this SQL TLS contract; all other engines reject an explicit request. MongoDB's tls and Elasticsearch's TLS settings remain separate. Existing profiles without sqlTls keep their previous behavior; no global TLS policy is added. V1 directory and v2 profile reads, normal init import from a V1 directory to v2, and the public migrateV1ToV2 helper preserve the TLS object. A legacy V1 single-file TLS profile remains readable and usable in human mode, but automatic init import explicitly refuses it before connecting or writing and leaves the original untouched. For that case, create a verified named profile in a separate configuration directory, verify it, then select it; preserve sqlTls throughout migration.

init resolves a relative CA path against its current working directory before saving. Saved caPath must be a nonempty absolute literal path to a PEM CA bundle; it accepts no environment-variable reference. Existing environment references in host/user and other supported fields remain usable. Explicit TLS uses only the selected profile’s CA and identity settings; ambient TLS settings cannot weaken verification or supply another profile’s trust. Each new connection reads the CA bundle again; an unreadable, empty or invalid bundle stops before connecting. --skip-test skips only the initialization test, not verification on subsequent use.

verify-full requires encryption, a trusted certificate chain, valid certificate dates and certificate identity matching the configured DNS name or IP address. Unix sockets are refused for explicit SQL TLS. Untrusted, expired or mismatched certificates and servers without TLS fail before the user query; there is no plaintext retry or verification downgrade. Correct the CA, target hostname or certificate validity before retrying. A cipher alone does not prove identity verification.

For TLS error recovery, keep TLS configured and repair the CA, hostname or certificate first. Then deliberately select the configuration with --config <path> (or --global) and, for v2, the named profile with --use <name> (DBCLI_CONNECTION is also accepted). The selected profile must still have sqlTls.mode: verify-full; missing, ambiguous or plaintext selections block live recovery steps instead of falling back to a default plaintext connection. Saved recovery envelopes redact the original selectors: your current selection is deliberate targeting, not proof of the original target. Recovery apply and verification use the current CLI binary, avoiding an older dbcli on PATH. For the MySQL example above:

bun run src/cli.ts --config .dbcli --use verified-mysql recover --apply --format json

Permitted reads through an already configured profile remain usable in agent mode; init and other configuration mutations remain refused under DBCLI_AGENT_MODE=1. TLS does not bypass explicit production selection, native database authorization, permission, blacklist, write-gate or audit controls.

For binary rollback, stop workflows that require verified TLS or keep a supporting version. Older binaries may strip sqlTls; retaining the profile after downgrading does not preserve the guarantee. Removing TLS settings or retrying in plaintext cannot restore compliance. Local isolated PostgreSQL, MySQL and MariaDB fixture results are not evidence of real RDS acceptance; RDS remains unverified.

Connection Management

Switch between Production, Staging, and Local environments:

Use --global to manage or run a named connection from the user-level registry, independent of the current project:

dbcli --global use --list
dbcli --global use shared
dbcli --global query "SELECT 1"

Without --global, dbcli continues to resolve the current project's .dbcli binding. This explicit scope prevents a global connection from being selected accidentally in an unrelated project.

Root-level --timeout <ms> overrides the connection timeout for one invocation (integer, 100–600000; must precede the command, like --global and --use). It overrides the connection config's timeout field; without either, adapters fall back to a built-in 5000ms default. The override applies only when the adapter is created for this invocation and is never written back to config.json — set the connection's timeout field instead for a value that persists across runs. Elasticsearch applies its timeout per request rather than once for the whole connection. This is useful when the default is too tight, such as a MongoDB connection over a VPN or to Atlas:

dbcli --timeout 20000 --use <conn> list

Connection time and statement time are two separate limits. --timeout caps both, so --timeout 3000 also aborts any SQL statement that runs longer than 3 seconds. To change only how long a statement may run, use root-level --statement-timeout <ms> (integer, 0–3600000; 0 removes the limit) or the connection's statementTimeout field. Without either, statements are not capped by dbcli — the server's own setting applies, and a six-second analytical query is no longer cut off just because the connection timeout defaults to five seconds:

dbcli --timeout 2000 --statement-timeout 120000 query "SELECT ... FROM big_table"
dbcli use --list         # List all connection names
dbcli use --list --format json # Credential-free JSON connection inventory
dbcli use --unset        # Clear the saved default without removing connections
dbcli use staging        # Switch your default connection
dbcli --use prod query "SELECT count(*) FROM users" # Global one-shot selector
dbcli query --use prod "SELECT count(*) FROM users" # Supported command-level form
DBCLI_CONNECTION=prod dbcli query "SELECT count(*) FROM users" # Environment selector

For an unambiguous, non-secret environment label, set the optional environment field on a v2 named connection, for example "environment": "production". dbcli use --list --format json returns each connection's name, environment label (or null), permission, system, server/database identity, and default marker. Environment-backed identity fields are null; URI-only MongoDB and Cloud ID-only Elasticsearch placeholders are too. It deliberately excludes users, passwords, URIs, Cloud IDs, API keys, and env variable names. A misspelled connection selector suggests nearby configured names.

The resolved v2 connection name is retained while commands run, so audit records are routed to that connection's own audit stream.

Production and automation safeguards

Connections labelled environment: "production" fail closed when you make one the persistent default. Repeat the exact name to make that intent explicit:

dbcli use production --confirm-production production

It only confirms a persistent-default change: one-shot --use production remains explicit and never alters configuration. In agent mode (DBCLI_AGENT_MODE=1), configuration, permission, and credential mutations are always rejected. A human or administrator must run the approved change workflow in a separate process with agent mode disabled; dbcli does not accept a same-process environment variable as approval. Trusted writes maintain a config integrity record and secure file modes where supported; agent reads fail closed on missing, replaced, non-regular, or tampered records. Agent mode also refuses legacy single-file .dbcli configs until a human/admin migration to V2 home storage. For a same-user hostile process, set DBCLI_CONFIG_INTEGRITY_ANCHOR_DIR to a host-protected or read-only directory so the detached digest cannot be replaced alongside workspace files.

Every audit entry includes non-secret metadata.connection_name and metadata.environment from the resolved connection, never credentials or endpoint secrets. A SQL entry also carries metadata.blacklist_checked — every identifier the blacklist compared this statement against. target names one table, derived separately, and for a JOIN it is whichever table came first: a statement refused because it reached salaries could be filed under target: "a", so asking the log "did anyone reach the protected table" by target found nothing. It is stored exactly as the blacklist saw it, so it deliberately contains more than table names — aliases, qualified column references, and keywords such as CREATE — because filtering it for display would be a second parser disagreeing with the first.

Selection precedence is explicit --use, then DBCLI_CONNECTION, then the configured default. Environment values are trimmed and empty values are ignored. One-shot selectors never change the persistent default. The command-level form is supported by query, schema, list, export, and check; use the global form before other commands. Conflicting root-level and command-level values fail clearly. Selectors require a v2 configuration: a single-connection (v1) project has no names to choose between, so --use or DBCLI_CONNECTION there is rejected rather than quietly running the only connection. If --use is placed after an unsupported command, dbcli keeps the request rejected and prints the copyable root-level form, such as dbcli --use <connection> status.

Working without a default

A v2 configuration may omit the top-level default field. If present, it must be a nonempty string naming an existing connection; null, "", and unknown names are invalid. A new registry defaults to its first explicitly named connection; adding another preserves the existing default or no-default state. Existing v1 reads and named v1-to-v2 imports remain supported.

dbcli use --unset
dbcli --use staging query "SELECT 1"
DBCLI_CONNECTION=staging dbcli query "SELECT 1"

Without a saved default, a command that needs a connection must select one with --use or DBCLI_CONNECTION. Otherwise it fails before database access and lists the available connection names, with no endpoints or credentials. It never picks the first connection. The selection precedence and conflict checks above still apply: blank CLI selectors are invalid; a blank environment selector is ignored. Unknown names and production connections retain their existing validation and explicit-selection safeguards.

dbcli use --unset removes only the persisted v2 default field and reports that it was cleared. It succeeds if the field is already absent, without changing connections. It rejects a connection name, --list, --confirm-production, or --format json in the same invocation without writing, and never converts or overwrites a v1 configuration. Clearing the default uses the same approved, integrity-protected configuration write path as switching it; it remains blocked in agent mode.

dbcli use describes the no-default state. Text inventory marks no connection as default, JSON inventory returns isDefault: false for every connection, and doctor accepts an omitted default as valid v2 configuration; its connection checks still need an explicit selector, such as dbcli --use staging doctor. dbcli init --remove <name> clears the default when removing its connection and reports that change; it does not choose a replacement. Removing another connection preserves the saved default or existing no-default state. Deleting the last connection is refused without changing the configuration. Renaming a connection preserves the no-default state, or updates the default name when renaming the current default. To set a default again, use dbcli use <name>; production confirmation requirements remain unchanged.

Legacy project configuration compatibility: if adding a connection would import a v2 registry stored directly in the project and an existing connection has an envFile other than .env.local, init refuses before writing. The existing configuration and env files remain intact. An administrator must migrate the registry and custom env files to supported home storage before retrying. Normal bound/home-storage v2 configurations and direct-project v2 configurations with no envFile or only .env.local remain supported.

Server-enforced SQL query-only mode

On PostgreSQL, MySQL, and MariaDB, every caller-controlled statement whose effective permission is query-only runs inside a fresh database-native read-only transaction on the physical connection that executes it. This covers query, export, saved-query bodies and verification, report diagnostics, the SQL shell, analyzed explain plans, and fan-out even when the selected connections store a higher permission tier. If dbcli cannot establish that boundary, it fails before sending the target statement. Classifier, blacklist, hidden-write, and multi-statement checks still run first. If the target completes but transaction cleanup fails, dbcli reports the uncertain outcome and discards the affected connection; the SQL shell reconnects for later statements without replaying the completed target.

The guarantee covers persistent, non-temporary data and schema. An engine may still permit temporary or session-local state under its native read-only rules, and the boundary cannot prevent effects outside the target database, such as a network call made by an unsafe extension. Database accounts and ACLs remain an important independent layer.

Read-only query fan-out

An explicit comma-separated --use runs one read-only query against several named connections. The root-level and command-level forms are equivalent:

dbcli --use primary,staging query "SELECT count(*) FROM users" --format json
dbcli query --use primary,staging "SELECT count(*) FROM users"

Names are trimmed and results preserve selector order. Empty or duplicate names are rejected. A selector containing one name follows the existing single-connection path. Fan-out is explicit-only: DBCLI_CONNECTION remains one literal connection name and is never split on commas. Neither form changes the persistent default connection.

Before any adapter connects, dbcli loads and validates every selected configuration. SQL fan-out permits only read-only classifications (SELECT, SHOW, DESCRIBE, and EXPLAIN); MongoDB permits filter objects and read-only aggregation pipelines but rejects top-level $out or $merge stages; Elasticsearch permits searches only. Redis fan-out is not supported. A multi-connection query also rejects --recovery, --ui, and CSV or HTML output; use --format table (default) or --format json.

Each connection runs independently with its own adapter, blacklist filtering, row-limit/truncation metadata, audit entry, timing, and disconnect. A failure on one connection does not cancel or hide the others. JSON returns an ordered results array with a labeled ok or error outcome per connection; table output renders a separate labeled section for each schema. The aggregate exit code is 0 when all succeed, 2 for mixed success and failure, and 1 when all fail or the request is rejected before execution.

Password rotation

dbcli password changes one connection's password without touching any other setting — built for environments where credentials rotate on a schedule.

dbcli password                       # Masked prompt, rotates the default connection
dbcli password prod                  # Masked prompt, rotates 'prod'
rotate-secret | dbcli password prod --stdin   # Non-interactive, nothing lands in shell history
dbcli password prod --password "$NEW" --skip-test --format json

Where the value lands is read from the config, never guessed: a connection whose password is { "$env": "NAME" } gets NAME rewritten in its envFile. A connection that declares no envFile has one recorded (.env.local) as part of the rotation — without it the reader would never load the file. A connection still holding a literal password is converted to { "$env": "DBCLI_<CONN>_PASSWORD" } once, so later rotations only touch the env file. Values are written quoted (NAME="..."), so leading and trailing whitespace survives the round trip.

v1 configs rewrite DBCLI_PASSWORD in .env.local, matching the v1 reader. A v1 config whose password comes from some other environment variable is refused with an explanation: v1 has no per-connection env file, so no file dbcli writes could make that variable resolve — set it in the environment, or migrate to v2.

The new password is verified by connecting with it before anything is written, so a bad rotation fails without leaving broken credentials behind. Pass --skip-test when the database is unreachable from where the command runs. The env file is written with 0600 permissions on POSIX systems (Windows has no equivalent mode bit — the file inherits the directory's ACL), and the value is never echoed or logged.

Options: [connection], --stdin, --password <value> (visible in shell history and the process list — prefer --stdin), --skip-test, --format <text|json>.

Blocked under DBCLI_AGENT_MODE=1 like every other credential mutation.

Command Reference

Discovery & Exploration

CommandPurpose
listEnumerate tables, collections, keys, or indices.
schemaInspect structure and refresh local metadata cache.
inspectFull context snapshot (objects, perms, suggested actions).
statusConfig and connectivity health summary.
capabilitiesStatic capability catalog — what dbcli can do. No database connection.
capabilities checkCheck required capability ids against the local config. Never connects.

dbcli blacklist list --format json emits one machine-readable document with tables, columns, and warnings; malformed MongoDB blacklist paths are returned in the structured warnings array instead of being mixed into stdout as human diagnostics.

Inspect one qualified SQL object

For PostgreSQL, use schema.table; for MySQL and MariaDB, use database.table. This inspects one accessible table or view, including system views, using the same namespace and local name for columns, enums, keys, indexes, and foreign keys. It does not change the connection, PostgreSQL search_path, default database, or schema cache. The scope of list, scanning, and schema without an object stays the same.

dbcli schema pg_catalog.pg_roles --format json
dbcli schema information_schema.TABLES --format json
dbcli schema '"Audit"."Order.Items"' --format json
dbcli schema '`reporting`.`order.items`' --format json

The outer single quotes in these shell examples preserve the native identifier quotes: double quotes for PostgreSQL and backticks for MySQL/MariaDB. Quote each component separately to preserve case or a dot inside a name; double an embedded native quote to escape it (for example, PostgreSQL "Audit"."Order""Items"). PostgreSQL unquoted qualified components fold to lowercase; MySQL/MariaDB follow the server’s identifier case rules. Empty components, more than two components, and invalid or unclosed quotes are rejected without trying another interpretation.

Unqualified names keep their existing exact-name behavior: PostgreSQL uses public, and MySQL/MariaDB use the connection’s default database. A missing qualified object never falls back to a same-named default object. Compatibility note: a literal local name containing a dot must now be explicit, for example dbcli schema 'public."audit.orders"' --format json. This name parsing applies to single-object schema, not to other commands.

Qualified JSON output keeps the local name and supplies a separate schema string (the database name on MySQL/MariaDB); text output distinguishes both components, including quoted dots and case-sensitive names. tableType identifies table or view; PostgreSQL materialized views use view. Qualified inspection never runs COUNT(*) on relation data. Available metadata estimates are estimates, not exact counts; when unknown, JSON has estimatedRowCount: null and sizeCategory: null, without a fabricated metadata.rowCount: 0. Text output does not substitute zero rows or a small size classification. Existing unqualified counting behavior is unchanged.

Blacklist protection applies to both unqualified and qualified SQL single-object inspection. Either a bare or qualified rule can match, with existing glob and case matching retained. Protected tables are refused; protected columns are hidden along with any entire primary key or index that contains them. A foreign key is omitted entirely from both table and column metadata if its source or target table/column is protected; composite constraints are never trimmed into misleading partial constraints. For this single-object schema command, expression or partial indexes are also omitted entirely when their complete column dependencies cannot be represented safely. JSON reports metadataOmitted: true and text output shows an omission notice when metadata is withheld, so omission is distinguishable from metadata that does not exist; the returned metadata is not necessarily complete. Hiding all columns does not mean the object is missing. Required metadata failures, including insufficient native permissions, fail the command rather than produce successful empty collections. If the database cannot distinguish a missing object from an invisible one, the error preserves that uncertainty.

A qualified name cannot be combined with --refresh or --reset (including both together); the command rejects these combinations before scanning. Use a separate unqualified schema --refresh or schema --reset operation for cache maintenance within its existing scope. Qualified inspection does not elevate database privileges or bypass query-only, agent-mode, or blacklist protections.

Capability discovery for Skills

dbcli capabilities answers “what can this tool do?” in a versioned, parseable shape, so an external Skill can decide before it starts working. dbcli capabilities check --require <ids> then answers “are those available here?” against the local configuration. Neither opens a database connection.

dbcli capabilities --format json
dbcli capabilities check --require schema.read,query.read --format json

connection.tls reports PostgreSQL, MySQL and MariaDB support from the engine capability matrix and maps to the existing connection command surfaces; it adds no TLS subcommand. Check with dbcli --use verified-pg capabilities check --require connection.tls --format json. available means tool support only: it does not prove that this profile configures TLS, the CA is readable, the handshake succeeds, or the operation is authorized. Catalog/check remain offline, read no CA bundle, perform no handshake and change no configuration. Other engines do not report this capability as available; older binaries without the ID return unknown. Configuration mutation still requires its own authorization.

The packaged assets/integration-kit/ contains a copyable Bun/TypeScript consumer and Task Pack fixture. It pins both schema versions, strictly parses catalog and Operation Envelope output, and shows the exit, correlation, and evidence boundary. Its CRUD, CQRS, and DBA examples remain external consumer requirements; dbcli does not implement or approve those workflows.

Agent output v1

--agent-output is a root option and must appear before the subcommand. In PLAT-005, dbcli --agent-output capabilities and dbcli --agent-output capabilities check --require <ids> are supported; either emits one compact UTF-8 JSON document plus a newline on stdout and nothing on stderr. It is ephemeral: it is not evidence and creates no timestamp.

Use the optional root --correlation-id <id> before the subcommand to associate an invocation with a Story, incident, change request, migration, or backfill. IDs are 1–160 ASCII letters, numbers, dots, underscores, colons, or hyphens; never use a credential, SQL, personal data, or a free-form label. For a supported non-static response, the ID appears as context.correlationId and in the existing audit entry as metadata.correlation_id; command summaries redact it. Static capabilities output keeps context: null. The option creates no evidence and changes no receipt.

The v1 envelope always has these ten keys: schemaVersion, ok, operation, status, context, data, warnings, evidence, recovery, and error. schemaVersion is 1; operation is capabilities.list or capabilities.check; status is succeeded or failed; and consumers must address keys by name, not field order. It is capped at 64 KiB including the newline.

Do not combine --agent-output with an explicitly supplied --format or --for-agent; either conflict, placement after the subcommand, invalid input, or an unsupported operation fails with one envelope and exit 2. Success exits 0; unmet requirements and unexpected internal failures exit 1. Existing output behavior is unchanged when --agent-output is absent.

Error and warning code/message vocabulary is locale-independent English. Error codes are INVALID_AGENT_OUTPUT_OPTIONS (invalid, misplaced, or conflicting output options), UNSUPPORTED_AGENT_OUTPUT_OPERATION (operation outside PLAT-005), INVALID_CAPABILITY_REQUIREMENTS (invalid --require), CAPABILITY_REQUIREMENTS_UNMET (a completed negative capability result), AGENT_OUTPUT_LIMIT_EXCEEDED (the 64 KiB limit), AGENT_OUTPUT_INTERNAL_ERROR (an unexpected safe failure), and INVALID_CORRELATION_ID (an invalid root correlation ID). Warning codes are DUPLICATE_CAPABILITY_REQUIREMENT, CAPABILITY_CONTEXT_UNAVAILABLE, CAPABILITY_CONTEXT_UNRESOLVABLE, and AGENT_MODE_RESTRICTION_ACTIVE.

A capability is one atomic dbcli ability — schema.read, data.delete. It is never a job or a method: dba.tune-production and crud.scaffold belong to the Role Skill or Method Skill that composes dbcli. The layering is Story + AGENTS.md + Role Skill + Method Skill + Tool Skill + dbcli, and dbcli only ever answers for the last two.

Four things the output deliberately does not mean:

Statuses are available, unavailable and unknown. The unavailable reasons are engine, agent-mode, permission, context-unavailable and context-unresolvable, reported least-fixable first so the reason names the blocker actually in the way. Under DBCLI_AGENT_MODE=1, capabilities that change configuration are refused whatever the permission level says. context-unresolvable is deliberately distinct from context-unavailable: a config whose {"$env": "..."} password names an unset variable is present and readable, and claiming there is no config would be false.

An unrecognised id fails closed and is never resolved to a similar-looking one. Exit codes: 0 all available, 1 any unavailable or unknown, 2 invalid input.

The catalog covers every public command, capabilities itself included, and every engine claim in it was read out of the implementation. Commands whose code refuses a non-SQL connection — explain, plan, assert, snapshot, verify, proxy — report unavailable with reason engine on MongoDB, Redis and Elasticsearch. Commands where only one mode needs SQL — --against-cache for impact assess and design, and draft validate for semantic — stay available and are marked limited on those engines. Nothing is marked supported on an engine the code does not settle.

required and results hold your ids in first-seen input order, so results[i] answers required[i] and a duplicate appears once. Reordering the arguments reorders the output — identical input is byte-identical output, but a reordered --require is different input. What reordering never changes is a capability's status, its reason, or the overall ok.

inspect output for agents

dbcli inspect returns two parallel arrays so an agent can orient on the very first call:

Both arrays are trimmed under --for-agent / --brief (≤ 3 hints, and a single safest suggested command).

Querying & Data Operations

# Raw Query
dbcli query "SELECT * FROM users LIMIT 10"

# Static SQL advisor (no DB connection)
dbcli lint "SELECT * FROM users" --format json

# Run Snippet
dbcli q @snippet --param id=1

# Export Data
dbcli export "SELECT * FROM logs" --format csv --output logs.csv

# Safe Write
dbcli insert users --data '{"name":"Alice"}' --dry-run

# DML --plan preflight (no DB connection; SQL, MongoDB, Redis, Elasticsearch)
dbcli insert users --data '{"name":"Alice","email":"a@b.com"}' --plan --format json
dbcli update users --where '{"_id":"abc"}' --set '{"status":"inactive"}' --plan
dbcli delete products --where '{"_id":"abc"}' --plan --format json
dbcli delete 'user:42' --where '' --plan --format json

Write confirmation gate (2.0.0)

Every write goes through a two-tier gate. Tier one is a question; tier two is a refusal you cannot flag your way past.

Tier one — ordinary writes. An INSERT, an UPDATE or DELETE that has a WHERE, a CREATE, an ALTER. At an interactive terminal dbcli prints what it understood the statement to do and asks for confirmation. --yes skips it. A non-interactive run — piped stdout, CI, an agent — never sees it, and --format json suppresses it even at a terminal.

Tier two — writes that cannot be taken back. An UPDATE or DELETE with no WHERE clause, a DROP, a TRUNCATE, a statement the SQL parser cannot read, several statements in one string, a write nested inside another statement (a data-modifying CTE such as WITH x AS (DELETE FROM t RETURNING *) INSERT INTO …), a MERGE carrying a WHEN … THEN DELETE or THEN UPDATE action, and — for dbcli update / dbcli delete — a --where that matches on no primary key and no unique index. dbcli asks you to type the target table name. No flag skips this, --yes and --force included. When nobody is watching, dbcli refuses: exit code 1, nothing sent to the database, and a message naming reason=no_where, reason=ddl_destruction, reason=unparseable, reason=multiple_statements, or reason=non_unique_where, reason=multi_table, reason=nested_write.

A write that joins another table is tier two, whatever its WHERE says. Whether such a write is limited to particular rows depends on the data, not on the statement: DELETE p FROM p JOIN o ON p.id = o.ref WHERE o.x > 0 deleted 2 of 5 rows against one dataset and all 2000 against another. A join's ON always mentions the target, so it proves nothing. This refuses the standard UPDATE … FROM, UPDATE … JOIN … SET and multi-table DELETE for unattended callers — rewrite the other table into a subquery in the WHERE, or run them where someone can confirm.

For a single-table write, the WHERE has to be about that table. A WHERE that lives only inside a subquery does not count: UPDATE p SET c = (SELECT max(x) FROM o WHERE o.id = 1) has a WHERE and rewrites every row. A correlated reference back to the target does count, so DELETE FROM t WHERE EXISTS (SELECT 1 FROM o WHERE o.tid = t.id) is an ordinary write, while the same statement with WHERE o.id = 1 inside deletes everything and is tier two. This is a lower bound rather than a guarantee — WHERE id IS NOT NULL names the target and still touches every row.

The escape route lives in the statement, not in a flag. To run a full-table write unattended, say so in the SQL — add WHERE 1=1 or a LIMIT. This is deliberate: appending WHERE 1=1 to a statement that already has a WHERE is a syntax error, so "add it everywhere just in case" breaks immediately and cannot become a habit. A flag would have the opposite property. For DROP and TRUNCATE there is no clause to add: whether they are possible at all is decided by the connection's permission level, and the typed confirmation still has to come from a person.

In dbcli shell, tier two applies and tier one does not. Every line in a shell is typed by a person, so a y/N on each write would become reflex; a full-table DELETE, a DROP or a TRUNCATE still asks for the table name. Typing anything else prints a cancellation and returns you to the prompt — the session, the connection and the history survive. Ctrl-C withdraws the question itself: nothing runs, and the next line you type is read as a statement rather than as an answer. Piped input (dbcli shell < script.sql) has nobody to answer, so a tier-two statement there is refused and the remaining lines still run. The Redis, MongoDB and Elasticsearch shells do not use the write gate; their writes are bounded by the connection's permission tier instead. For Elasticsearch that guard did not exist before 4.0.0 — the ES shell reached the cluster with no permission check at all, so a query-only connection could delete documents, drop an index or rewrite a mapping, and nothing was recorded. It now classifies every request through the same classifier dbcli query uses, refuses what the tier does not permit, and audits both executed and refused requests — one row before the request goes out and one after it returns, each naming the operation as <METHOD> <path>. Server-side scripts (script, script_fields) are refused in the shell as they already were in dbcli query: they run arbitrary code on the cluster and can read a blacklisted field back under a name of the request's choosing. A piped shell now finishes writing its audit rows before exiting, so a piped DELETE can no longer reach the cluster without leaving a record; the same fix applies to the SQL shell. The SQL shell audits every statement from 5.0.0, in the same two rows the Elasticsearch shell writes — attempt before it is sent and outcome after it returns, with a refused statement writing only outcome because it was never attempted. Before this it recorded nothing but tier-two write-gate decisions, so a SELECT typed at the prompt and an UPDATE that changed a row were equally invisible while the same statements through dbcli query were recorded. Both shells carry the phase in metadata.shell_phase. Audit writes are best-effort by default — a full disk or an unwritable .dbcli/audit lets the request through with only a warning — so set audit.strict: true in your config if an unrecorded request is worse than a refused one. It applies wherever dbcli records an operation *before* performing it: both shells' pre-flight rows and the SQL write gate's decision — which now includes a SELECT typed at the dbcli> prompt. Records written after the fact are outside it, because refusing then would report a completed operation as a failure rather than prevent one. dbcli audit health shows whether it is on. dbcli subcommands typed in the shell (query "...", \delete ...) run as separate processes with no stdin and cannot ask anything, so a tier-two statement there is refused with a message pointing you back to the dbcli> prompt.

A subcommand named after a SQL keyword needs a \ prefix. delete users --where id=1 is read as SQL — a shell is for typing SQL, and DELETE FROM users WHERE … has to work as typed — so it waits for a semicolon instead of running the subcommand. Type \delete users --where id=1 to reach the subcommand. The same applies to insert, update and explain — the four subcommands whose names are SQL keywords; the prefix works on every subcommand, so it is one rule rather than a list. While a statement is still accumulating, .quit and .clear keep working and Ctrl-C cancels — they used to be swallowed into the buffer, which made the shell look frozen.

A subcommand runs in its own process with no stdin, so its ordinary confirmation prompt reads EOF and cancels: \insert, \update and \delete need --force from inside the shell, or type the SQL at the prompt instead, where you can answer. The tier-two confirmation is not affected — it is refused in the child and points you back to the prompt, which is where a table name can actually be typed.

Every tier-two evaluation is written to the audit log — allowed, declined and refused alike — so the gate's effect is measurable rather than assumed. dbcli audit write-gate is that measurement: how often tier two was reached, under which criterion, and how it was answered. If it reports zero, the criterion is what needs revisiting, not the gate.

# Refused: no WHERE and nobody to confirm it
dbcli query "UPDATE users SET banned = 1" --format json
# → exit 1, reason=no_where, nothing was sent to the database

# Accepted: the intent is in the statement
dbcli query "UPDATE users SET banned = 1 WHERE 1=1" --format json

# An ordinary write, unattended or with the question skipped
dbcli query "UPDATE users SET banned = 1 WHERE id = 3" --yes

# Structured delete matching on a non-unique column: tier two
dbcli delete users --where "status=active"

Query input from positional text, files, or stdin

dbcli query [sql] [-f, --query-file <path>] requires exactly one query source: positional query text, a UTF-8 file named by --query-file, or stdin via --query-file -. Providing no source or combining positional text with --query-file is an error. After reading the source, dbcli removes one leading UTF-8 BOM and surrounding whitespace; an input that is then empty is rejected. --query-file - requires piped input: when stdin is an interactive terminal dbcli refuses immediately instead of waiting for input with no prompt.

Use stdin for multiline SQL without escaping it as one shell argument:

dbcli query --query-file - <<'SQL'
SELECT id, email
FROM users
WHERE status = 'active'
ORDER BY id;
SQL

MongoDB filters and aggregation pipelines use the same file/stdin sources. If pipeline.json contains [{"$match":{"message":{"$regex":"user's event"}}}], either form below avoids having to shell-escape the apostrophe:

dbcli query --collection raw_logs --query-file ./pipeline.json

dbcli query --collection raw_logs --query-file - <<'JSON'
[{"$match":{"message":{"$regex":"user's event"}}}]
JSON

Passive slow-query hints

query and q observe the execution time already returned for a completed query. At the default 1000ms threshold, a slow result receives a Performance hint in table output and a metadata.performanceAdvisory object in JSON output. Fast results remain unchanged, so normal queries do not produce repetitive advice.

# Suggest a review for a query taking 250ms or more
dbcli query "SELECT * FROM events WHERE account_id = 42" --slow-ms 250

# Disable the passive hint for one invocation
dbcli q @daily-active-users --slow-ms 0

The hint does not run EXPLAIN, inspect schema, or issue a second database request. On PostgreSQL, MySQL, MariaDB, and Redis it recommends the non-mutating next step dbcli guide slow-query --format markdown; run deeper diagnostics only after reviewing that guidance and the relevant query. MongoDB and Elasticsearch ship no diagnostic snippet for that goal, so their hint reports the timing without naming a command that would return nothing.

Field projection with --fields

--fields reduces SQL or MongoDB query results to the fields you need. Use an inclusion list to keep fields in the requested order:

dbcli query "SELECT * FROM events" --fields id,name,created_at
dbcli query '{}' --collection raw_logs --fields station_code,bet,win,created_at

Prefix every field with - to exclude it. The portable spelling uses = so the leading hyphens are unambiguously part of the option value:

dbcli query "SELECT * FROM events" --fields=-raw_response,-request_payload
dbcli query '{}' --collection raw_logs --fields=-raw_response,-request_payload

An invocation must use either inclusion or exclusion syntax; the two modes cannot be mixed. Empty lists or entries and duplicate paths are rejected. Dotted paths such as profile.name are supported, and inclusion output follows the requested order. A MongoDB inclusion excludes _id unless _id is explicitly requested. A requested field that the result does not contain is reported as null rather than raising an error, so a misspelled field name yields a column of nulls — check the spelling against dbcli schema before reading meaning into an all-null column.

SQL projection is applied to the returned rows after the query runs. MongoDB pushes the projection into find or the aggregation pipeline to reduce transferred data, then normalizes the returned rows again after blacklist masking. Redis and Elasticsearch queries do not support --fields.

The blacklist remains the final authority: --fields cannot reveal a protected field, and field projection is a result-shaping convenience, not a security boundary.

Mutation outcomes

After insert, update, or delete finishes in an interactive terminal, dbcli summarizes what happened in plain language: the affected row count and table, no-match, cancellation, dry-run, or failure outcome, plus elapsed time when work actually ran. Redirected or piped stdout remains the stable JSON result envelope for scripts. Use --format json to keep that envelope even in a terminal. The same outcome vocabulary applies across SQL, MongoDB, and Redis; dry runs report dry_run, never success.

A write that changed rows also states how to get the previous values back — which, for these commands, is a backup: dbcli keeps no automatic undo for a write that succeeded. A failure instead names --recovery, the flag that writes a recovery plan for dbcli recover to read. Outcomes that changed nothing (cancelled, dry-run, no rows matched) say nothing about reversal, because there is nothing to reverse.

A failure is prose too, and it goes to stderr with the exit code 1: a blacklist refusal, a malformed --set, or a statement the database rejected. --format accepts only text or json here; anything else is refused before dbcli connects, rather than silently meaning text.

The confirmation a non-forced mutation asks for — the generated SQL, the parameters, the destructive-delete warning, and the y/n question — is written to stderr, not stdout. A terminal shows both, so nothing looks different when you run one by hand; a script that captures stdout gets exactly one JSON document whether or not it passed --force. Neither a dry run nor a cancellation sends a write to the database, but both do connect and read the table schema, because the SQL they exist to show cannot be built without the column list; use --plan for the preflight that opens no connection at all. MongoDB and Redis writes ask the same question — they are issued straight from the command rather than through the SQL executor, and used to run unasked — so cancelled is now reachable on every engine. Their prompt shows the statement the dry run would print, with no parameter block, since those statements carry their values inline.

--recovery also changes what a failed insert / update / delete writes to stdout: on failure the recovery envelope replaces the JSON result envelope rather than following it, so a caller parsing stdout gets exactly one document either way. Without the flag, a failure prints the result envelope with status: "error" and exits 1, as before.

insert, update, and delete accept --plan to run a static risk analyzer against the planned write without connecting to the database. The planner now supports SQL (postgresql, mysql, mariadb), MongoDB, Redis, and Elasticsearch. It honors the connection's permission, blacklist, and the cached schema for the selected engine. --format text (default) prints a human verdict, --format json prints the full QueryRiskResult. Analyzer BLOCK decisions exit 0; configuration / engine / invalid-DSL errors exit 1. --plan is mutually exclusive with --dry-run.

Conservative MVP restrictions per engine:

EngineBLOCK examplesWARN examples
SQLUPDATE/DELETE without WHERE, DDL, blacklisted tableSchema cache missing, blacklisted column referenced
MongoDBEmpty filter {}, update operator outside $set/$unset, $whereFilter without _id, broad $in/$regex/$gte, missing schema
RedisWildcard * target, blacklisted key/fieldPattern target (e.g. user:*), missing field info on update
Elasticsearchupdate/delete without _id, blacklisted index/fieldInsert without _id, missing schema

BLOCK means the planner found an unsafe intent. Still run --dry-run on the real command before executing the write.

Snippet Management

Maintain complex queries as version-controlled snippets in your repository with visualization metadata.

Health & Diagnostics

doctor
Validates environment, connectivity, and configuration integrity; JSON config-missing failures include a structured remediation command and risk level. doctor --format json --remediation adds candidate-only plans for blacklist coverage, schema refresh, and large tables. SQL large-table candidates run dbcli plan; MongoDB/Elasticsearch candidates run dbcli schema as preflight, then offer a human-confirmed bounded dbcli query. Doctor never mutates config, blacklist rules, or database data.
report
Produces a comprehensive health, performance, and capacity report.
recover
Executes automated remediation plans for common failure modes.
audit
Tails the per-connection audit log (.dbcli/audit/<conn>.jsonl); use --for-agent for handoff JSON.

Runtime, machine output, and backfill artifacts

doctor reports runtime identity (launcher/source, runtime and package versions) and flags a bundled-runtime/package mismatch with explicit dbcli upgrade remediation. Machine-readable commands (--format json and other non-human formats, --for-agent, or --recovery) keep stdout payload-only: update and skill-update notices are suppressed. Human-facing notices go to stderr and are deduplicated once per CLI session.

backfill artifact converts a deliberately bounded JSON source catalog into a reviewable, dry-run-only artifact; it never connects to or writes either database. The catalog requires table, non-empty keyColumns, rows, verifyQuery, and expect, with at most 1,000 rows. The target must be PostgreSQL, MySQL, or MariaDB; the source identity may describe another engine. Named v2 connections capture non-secret source/target identities and their differences:

dbcli backfill artifact --source ./backfill.json \
  --source-use staging --target-use production

The artifact includes generated UPDATEs, per-statement plan commands, blacklist/schema preflight, a read-back verify safe-backfill command, and a rollback hint. Review it before any separate, explicitly human-confirmed SQL workflow. Use --stdout for JSON or --out <path> to select a path.

ORM definition drift

diff --against-orm compares an application schema with the existing SQL schema cache. The comparison is cache-only: it does not connect to the database, refresh the cache, or execute proposals. Run schema --format json first when freshness matters; an empty cache exits 1 and asks you to run dbcli schema.

Five input paths cover the supported ecosystem:

Input pathWhat to pass
PrismaOne schema.prisma file.
DrizzleOne PostgreSQL drizzle-kit v7 snapshot generated with drizzle-kit generate.
TypeORMDDL generated by schema:log; entity source files are not parsed.
SequelizeA schema-only dump of a scratch database after applying migrations; model source files are not parsed.
Portable schemaRaw PostgreSQL/MySQL/MariaDB DDL (including multiple files or globs), or one normalized JSON file.
dbcli skill tasks plan orm-drift-review --param orm_path=prisma/schema.prisma --format json
dbcli diff --against-orm prisma/schema.prisma --format json

drizzle-kit generate
dbcli diff --against-orm drizzle/meta/0001_snapshot.json --orm-format drizzle --format table

bunx typeorm schema:log -d <path/to/datasource> > schema.sql
dbcli diff --against-orm schema.sql --orm-format typeorm --format table

# Point the project Sequelize config at an empty scratch database first
bunx sequelize-cli db:migrate
pg_dump --schema-only <scratch-database> > schema.sql
# For MySQL instead:
mysqldump --no-data <database> > schema.sql
dbcli diff --against-orm schema.sql --orm-format sequelize --format json

dbcli diff --against-orm "migrations/*.sql" --orm-format ddl --format markdown
dbcli diff --against-orm schema.normalized.json --orm-format json --format table
dbcli diff --against-orm prisma/schema.prisma --recovery --format json

--against-orm is repeatable and accepts comma-separated paths. DDL-family inputs—raw DDL plus the typeorm and sequelize aliases—support real filesystem globs and multiple files; paths are deduplicated and put in deterministic order, then parsed as one shared ordered context so a later file's index can attach to an earlier file's table. Prisma, normalized JSON, and Drizzle accept exactly one file. Drizzle input must be a PostgreSQL drizzle-kit v7 snapshot at drizzle/meta/<NNNN>_snapshot.json. TypeORM schema:log prints the SQL that schema:sync would execute without applying it. Sequelize CLI has no universal db:migrate --dry-run, so migrations must run against a scratch database before pg_dump --schema-only or mysqldump --no-data. TypeORM entities and Sequelize models (.ts, .js, .mjs, or .cjs) are rejected with the exact generation recipe instead of being parsed. Use --orm-format prisma|ddl|json|drizzle|typeorm|sequelize to override detection; the TypeORM alias default-ignores typeorm_metadata and migrations, while the Sequelize alias default-ignores SequelizeMeta. Use --ignore <globs> for additional comma-separated, case-sensitive qualified table patterns and --format json|table|markdown for output.

CategoryMeaning
missing_in_dbError: the ORM expects a table, column, or index absent from the DB cache.
missing_in_ormWarning: a cached DB object is absent from the ORM definition.
mismatchError for incompatible type families/nullability; info for same-family type spelling, default, or primary-key differences.
unmanagedInformational ignored table; listed but not scored.

Only scored errors determine the drift exit code: they exit 1, while warnings, infos, unmanaged, or unparsed alone exit 0; operational failures still exit 1, and --recovery wraps them. PostgreSQL schema/table storage is exact and case-sensitive, so users and "Users" coexist. In parsed DDL, unquoted Users folds to users, while quoted "Users" resolves only to Users; quote state comes from the parsed identifier, never display capitalization. Duplicate exact or resolved identities fail closed. A Drizzle snapshot or normalized JSON artifact that is not valid JSON, and a normalized JSON artifact that violates the contract, fail closed naming the offending file and the fields at fault instead of degrading to unparsed. Unsupported Prisma/DDL/Drizzle constructs are retained in unparsed with a blocked: reason; this includes Drizzle enums and other unsupported snapshot constructs. Drizzle column defaults are accepted only when the snapshot value is a string, boolean, or finite number; an unsupported default blocks and omits that column. PostgreSQL PARTITION BY and MySQL/MariaDB table engine, charset, and other table options emit a blocked: entry without a managed table. Normalized JSON requires every unparsed.reason to start with blocked:. Structural index comparison deduplicates signatures; drift entries sort by table/object/category/detail in deterministic Unicode code-point order, independent of locale.

Proposals are shell-safe text and remain dry-run by default. Safe unqualified additions may emit migrate; schema-qualified or CLI-lossy index targets escalate to migration-review rather than emit corrupt commands. A table, column, or type positional beginning with - also escalates; leading-dash option values use option-safe attached syntax such as --default=-1 or --columns=--config,email. Capture the dry-run DDL and pass both exact values as separate quoted parameters:

dbcli skill tasks plan migration-review \
  --param "table=${exact_table}" \
  --param "ddl=${captured_ddl}"

Never add --execute until that review is complete.

Data Verification

Verify data-processing correctness — capture a result fingerprint, then assert invariants against it, a second query, or inline conditions. SQL engines only (PostgreSQL / MySQL / MariaDB).

CommandPurpose
snapshot <query>Capture a result fingerprint (row count + per-column null/distinct/min/max/sum + an order-independent checksum). Default file .dbcli/snapshots/snap-<timestamp>.json; also --out, --rows, --stdout. Blacklisted columns are masked at the source, so the snapshot is safe to store. Use as a baseline for assert --against.
assert <query>Verify an invariant; exit 1 on failure unless --no-fail. --expect (rows>0, value==X, col:c not null, unique, between a and b, >= n), --vs <query> --compare rows|value (reconcile two queries), --against <snapshot> --tolerance <pct> (drift vs a baseline; 0 = exact checksum).

assert --write-verification-artifact

Persist a result evidence record (v1 VerificationArtifact JSON) to .dbcli/verification/ whenever you need a durable audit trail for a read-back assertion. The verification artifact is always written to <cwd>/.dbcli/verification/ (relative to the current working directory), regardless of where the --config file is located.

Flags:

FlagRequiredDescription
--write-verification-artifactopt-inWrite a VerificationArtifact JSON after the assertion runs.
--evidence-receipt <path>opt-inAtomically write safe assert provenance after the outcome is authoritative; it contains no SQL or rows.
--verification-subject <kind:name>yes (when flag is set)Subject being verified. Allowed kinds: recovery, task-pack, assertion, migration, backfill, manual.
--verification-summary <text>noHuman-readable summary line. Defaults: pass → "Assertion verified the expected state."; fail → "Assertion did not verify the expected state."

assert --expect ... --evidence-receipt <path> can write a receipt without --write-verification-artifact. Only boolean check pass bits are projected into the existing receipt schema; raw SQL and result values are excluded. A failed assertion still writes a failed receipt when requested. --no-fail changes only the assertion's exit behavior: it does not turn the receipt into a success, and a receipt write error still exits 1.

evidence compose may reference that explicit workspace-contained receipt with --receipt <path>; provenance is not execution approval.

Output contract:

Planned vs Result evidence. dbcli skill tasks plan safe-backfill-verify produces a plan JSON with a verification block whose status is "planned" — this is the planned evidence definition describing which check will run. The final assert --write-verification-artifact step produces result evidence (status: verified or not_verified). These are two different records; "planned" does not mean verification has run.

Note: Bigint aggregates (count(*), sum()) arrive from Postgres as strings and are compared numerically anyway, so value == 0 works without a cast. Quote the expectation (value == "0") when you mean a text comparison.

# 1. plan the workflow (plan-only, planned evidence)
dbcli skill tasks plan safe-backfill-verify \
  --param table=orders \
  --param query="UPDATE orders SET status = 1 WHERE status IS NULL" \
  --param verify_query="SELECT count(*)::int FROM orders WHERE status IS NULL" \
  --param expect="value == 0"

# 2. dry-run the write manually
dbcli update orders --where "status IS NULL" --set '{"status": 1}' --dry-run

# 3. execute the write under existing write permissions
dbcli update orders --where "status IS NULL" --set '{"status": 1}'

# 4. run the final assertion and persist RESULT evidence
dbcli assert "SELECT count(*)::int FROM orders WHERE status IS NULL" \
  --expect "value == 0" \
  --write-verification-artifact \
  --verification-subject backfill:safe-backfill-verify

dbcli verify runs verification scenarios (safe-backfill, migration, rollback, constraint) and never executes writes/DDL. dbcli verification inspects and manages the local result artifacts those scenarios produce under .dbcli/verification/.

After an --after-write run, add --evidence-receipt <workspace-relative-path> to atomically write safe provenance linked to the result artifact. The receipt contains no SQL, rows, credentials, or user paths; it is provenance only, not execution approval. Preflight has no executed artifact and does not support a receipt. Receipt outcome (succeeded/failed) stays separate from scenario status (verified, not_verified, indeterminate, or blocked); task-pack planned evidence remains plan-only.

inspect, report, schema, plan, lint, explain, and impact assess also accept --evidence-receipt <workspace-relative-path>. Their receipt contains only a fixed command subject, capability, timestamp, safe context digests, and an optional correlation ID. Successful and failed command outcomes both produce a receipt when requested; a receipt write failure does not change the command result or exit code.

verify safe-backfill

Verify a safe backfill without ever executing the UPDATE. Preflight (default) runs read-only guards and prints the exact after-write command; --after-write re-runs the guards, runs the read-back assertion, and writes a verification artifact.

⚠️ verify safe-backfill never executes the backfill write. Run the approved write through your normal write command first, then run --after-write.

Preflight:

dbcli verify safe-backfill \
  --table users \
  --query "UPDATE users SET status = 1 WHERE status IS NULL" \
  --verify-query "SELECT count(*)::int AS n FROM users WHERE status IS NULL" \
  --expect "value == 0"

After-write (writes the artifact):

dbcli verify safe-backfill ... --after-write

Inspect the result: dbcli verification show <artifact-id>.

Preflight also echoes the planned update (the --query you must run yourself) so the printed output is a complete, copy-pasteable record of the operation.

Result status: verified (read-back matched --expect), not_verified (read-back contradicted --expect), blocked (a guard failed — blacklist, schema, plan, or a non-read-only verify-query), indeterminate (the assertion ran but could not yield a trustworthy verdict).

All four guards run and report their individual outcomes even if one fails. After-write runs the assertion only when all four pass; persisted failure reasons are fixed safe labels rather than driver error text. Persisted custom labels and SQL evidence also redact credentials, filesystem paths, and SQL comments.

Guard constraints (fail closed):

💡 Repeated backfills on the same table. Artifacts default their subject name to the table (backfill:<table>). When you run multiple distinct backfills against the same table, pass --subject-name <unique-label> so each operation is independently traceable in dbcli verification list.

verify migration

Preflight or after-write verification for a schema migration. This command never executes DDL — it analyzes the proposed ALTER TABLE, runs read-only guards, and (in after-write mode) records evidence after you apply the migration externally.

⚠️ verify migration never executes DDL. Apply the migration externally first, then run --after-write to record evidence.

Preflight:

dbcli verify migration \
  --table users \
  --ddl "ALTER TABLE users ADD COLUMN verified_at TIMESTAMPTZ" \
  --verify-query "SELECT count(*)::int AS n FROM users WHERE verified_at IS NOT NULL" \
  --expect "value == 0"

After the migration is applied externally:

dbcli verify migration ... --after-write
OptionRequiredDescription
--table <table>yesTable affected by the migration.
--ddl <sql>yesProposed migration DDL, analyzed but never executed. MVP accepts ALTER TABLE only.
--verify-query <sql>yesPlain SELECT for post-migration read-back verification.
--expect <expr>yesAssertion expression for the read-back result.
--after-writenoRun the post-migration assertion and write a v1 artifact.
--format <table|json>noOutput format, default table.
--subject-name <name>noArtifact subject name. Default is the table name.
--summary <text>noOptional artifact summary override.

Preflight returns ready or blocked and prints the exact after-write command; ready is not verified — it only means the guards passed. After-write maps the read-back assertion to verified / not_verified / indeterminate, and a failed guard to blocked. CREATE TABLE, DROP TABLE, CREATE INDEX, and multi-statement DDL are blocked in the MVP.

The ALTER TABLE target may be table, schema.table, or catalog.schema.table. Each segment is a simple unquoted name ([A-Za-z_][A-Za-z0-9_]*) or a quoted identifier — double-quoted ("…"), backtick-quoted (`…`), or bracket-quoted ([…]) — so "user accounts" or "tenant-1"."orders" are accepted. Targets that cannot be fully parsed under this contract (unterminated quotes, unsupported escapes, or more than three parts) are blocked before the after-write assertion with a "could not be parsed" reason, distinct from the must match --table mismatch reason.

verify rollback

Preflight or after-write verification for a rollback you apply externally — either reverting a schema migration (--kind ddl) or reverting a data change (--kind dml). This command never executes the reverting statement — it analyzes the proposed --statement, runs read-only guards, and (in after-write mode) records evidence after you apply the rollback yourself.

⚠️ verify rollback never executes the rollback statement. Apply the rollback externally first, then run --after-write to record evidence.

Schema rollback preflight (--kind ddl, a single ALTER TABLE):

dbcli verify rollback \
  --kind ddl \
  --table users \
  --statement "ALTER TABLE users DROP COLUMN verified_at" \
  --verify-query "SELECT count(*)::int AS n FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'verified_at'" \
  --expect "value == 0"

Data rollback preflight (--kind dml, a single UPDATE):

dbcli verify rollback \
  --kind dml \
  --table users \
  --statement "UPDATE users SET status = NULL WHERE status = 9" \
  --verify-query "SELECT count(*)::int AS n FROM users WHERE status = 9" \
  --expect "value == 0"

After the rollback is applied externally:

dbcli verify rollback --kind <ddl|dml> ... --after-write
OptionRequiredDescription
--kind <ddl|dml>yesReverting-statement kind: ddl (a single ALTER TABLE) or dml (a single UPDATE).
--table <table>yesTable affected by the rollback.
--statement <sql>yesProposed reverting statement, analyzed but never executed.
--verify-query <sql>yesPlain SELECT for post-rollback read-back verification.
--expect <expr>yesAssertion expression for the read-back result.
--after-writenoRun the post-rollback assertion and write a v1 artifact.
--format <table|json>noOutput format, default table.
--subject-name <name>noArtifact subject name. Default is the table name.
--summary <text>noOptional artifact summary override.

--kind selects which grammar the statement must satisfy and reuses the same guards as the sibling scenarios: ddl reuses the verify migration ALTER TABLE contract (single statement, target must match --table); dml reuses the verify safe-backfill plan contract (UPDATE only, must have a WHERE, target must match --table). Preflight returns ready or blocked; ready is not verified. After-write maps the read-back assertion to verified / not_verified / indeterminate, and a failed guard to blocked. The artifact records the rollback under the existing subject kind (migration for ddl, backfill for dml) with command: verify rollback. The MVP supports a single ALTER TABLE for ddl and a single UPDATE for dml; INSERT/DELETE reverts are not yet supported.

verify constraint

Preflight or after-write verification that a data-integrity invariant holds across your change — foreign-key consistency, NOT NULL coverage, uniqueness, or a custom violation query. This command never executes a write — it only runs read-only COUNT(*) violation queries and (in after-write mode) records evidence.

⚠️ verify constraint never executes a write or DDL statement. Run preflight before your change, then run --after-write afterward to record evidence.

Four check kinds, selected by --check:

FK preflight (verify no orphaned orders before a migration):

dbcli verify constraint \
  --table orders \
  --check fk \
  --column customer_id \
  --references customers.id

NOT NULL preflight (verify the column is fully populated):

dbcli verify constraint \
  --table users \
  --check not-null \
  --column email

After the change is applied externally:

dbcli verify constraint --table orders --check fk --column customer_id \
  --references customers.id --after-write
OptionRequiredDescription
--table <table>yesTable the invariant is checked on.
--check <kind>yesConstraint kind: fk | not-null | unique | custom.
--column <name>yes (fk/not-null/unique)Column to check. Repeatable for not-null/unique; the child FK column for fk.
--references <table.column>yes (fk only)Referenced <table>.<column> for FK parent lookup.
--violation-query <sql>yes (custom only)Read-only SELECT returning a single integer violation count.
--allow-preexistingnoNo-regression mode: verified when count ≤ --baseline.
--baseline <n>noBaseline violation count from preflight (use with --allow-preexisting).
--after-writenoRe-run the violation count and write a v1 artifact.
--format <table|json>noOutput format, default table.
--subject-name <name>noArtifact subject name. Default is the table name.
--summary <text>noOptional artifact summary override.

Preflight returns ready or blocked; ready is not verified. After-write maps the count to verified (violations ≤ threshold) or not_verified (violations > threshold). The default threshold is 0 (strict). With --allow-preexisting, the threshold is the --baseline count from preflight — verified as long as violations do not exceed the pre-existing level. A query error yields indeterminate; a failed guard yields blocked. The artifact uses subject.kind = 'table' and command: verify constraint. MVP: SQL engines only; single FK column; never executes writes.

verification — inspect & manage verification artifacts

dbcli verification works on artifacts written under <cwd>/.dbcli/verification/. It never connects to a database and never writes audit entries. list, show, and summary are read-only filesystem inspection; prune is a local lifecycle command that is dry-run by default and deletes files only with --execute --force. The storage root is the current working directory, independent of --config.

Statuses: verified, not_verified, indeterminate, blocked.
Subject kinds: recovery, task-pack, assertion, migration, backfill, manual.

A missing .dbcli/verification/ directory returns an empty result and exits 0. Malformed files are skipped during list/summary (surfaced via --include-invalid and the summary invalid count); selecting a malformed file with show exits 1.

prune is dry-run by default. --keep-latest (default 20) always protects the newest N valid artifacts across all subjects and statuses before the --status/--subject filters are applied. prune deletes files only when both --execute and --force are given, and only regular verification-*.json files inside .dbcli/verification/; execute-mode table output lists each deleted and skipped file.

dbcli verification summary --format json
dbcli verification list --status verified --subject backfill:safe-backfill-verify
dbcli verification show ver_abcd --format json
dbcli verification prune --older-than 30d --format json
dbcli verification prune --older-than 30d --keep-latest 20 --execute --force

evidence — compose offline evidence packs

dbcli evidence combines existing verification artifacts and audit entries into a canonical, workspace-contained JSON pack for review or handoff. It never opens a database connection and does not copy SQL, targets, audit metadata, verification summaries, or credentials into the pack. Claims are external statements, explicitly labeled as such; they are not dbcli verification verdicts.

dbcli evidence compose --claims ./claims.json --verification ver_abcd --audit 1a2b \
  --receipt .dbcli/evidence/verify-receipt.json \
  --output .dbcli/evidence/review.json
dbcli evidence validate --file .dbcli/evidence/review.json --format json
dbcli evidence render --file .dbcli/evidence/review.json

Artifact format versions

Packs and receipts carry their own version, which is not the dbcli package version (see ADR-0013). The current format is version: 2; dbcli 3.0.0 and earlier wrote version: 1 in two mutually incompatible layouts. validate --format json reports one of three answers in status, and only the first means the pack can be relied on.

statustrustMeaningExit
current-validcurrent-validCurrent format, digest verified, references resolvable.0
current-references-expiredcurrent-validCurrent format and digest, but a referenced source is gone.1
recognized-legacynot-current-validWritten by an older dbcli. legacyFormat and producedBy say which; integrity reports whether that format's own digest still verifies. References are not evaluated.1
unsupportednot-current-validThe version is unknown, or the version and the structure disagree.1

Legacy packs and receipts are readable and integrity-checkable but are never treated as current-valid, and there is no migration: a pack's identity is derived from its digest, so rewriting one would mint a new artifact wearing an old one's provenance. Compose a new pack from current evidence instead.

dbcli proxy — Local Observability Proxy

A local development observability proxy — point an existing application at the proxy port and dbcli relays every query to the real database while recording query text, latency, byte counts, row counts, and error events. This is NOT a production gateway. Use it during local development only.

Quick Start

# Explicit upstream/downstream
dbcli proxy mysql --listen 127.0.0.1:3307 --target 127.0.0.1:3306
dbcli proxy postgresql --listen 127.0.0.1:5433 --target 127.0.0.1:5432

# Infer engine + target from a named connection
dbcli --use local proxy --listen 127.0.0.1:3307

Change your application's DB host/port to the --listen address and leave credentials unchanged. The proxy is fully transparent — the application behaves identically.

Options

OptionDefaultDescription
--listen—Local address to bind (e.g. 127.0.0.1:3307). Required.
--target—Upstream DB address. Required unless --use is given.
--events.dbcli/proxy/events.jsonlPath to the append-only JSONL event log.
--slow-ms1000Queries whose durationMs reaches this threshold are flagged slow: true in the query_completed event (and a terminal warning is printed).
--redactnonenone keeps SQL text as-is; literals masks string and number literals.
--formattextConsole output format: text or json.

Event Log (JSONL)

Each completed query appends one JSON object to the event log:

{"version":1,"type":"query_completed","timestamp":"2026-06-04T12:00:00.000Z","engine":"mysql","sessionId":"pxy_1","queryId":"qry_pxy_1_1","client":"127.0.0.1:54321","target":"127.0.0.1:3306","sql":"SELECT * FROM users WHERE id = ?","statement":"SELECT","tables":["users"],"durationMs":4,"requestBytes":42,"responseBytes":318,"rowCount":1,"error":null,"tags":[]}

Privacy

SQL text is always stored in the event log. Result rows are never stored. Use --redact literals to mask string and number literals in SQL before logging (e.g. WHERE id = ? instead of WHERE id = 42).

Analyze the event log offline

dbcli proxy analyze — analyze the captured event log offline (no DB). --format json|text, --top, --slow-ms, --n-plus-one, --no-include-rotated. Produces summary, per-fingerprint stats, slowest queries, error groups, hot tables, and N+1 suspects.

Each actionable block carries machine-readable next steps so an AI agent can move from diagnosis to fix:

The recommended loop: run proxy analyze, then for each finding read its hints, run its suggestedCommands to gather schema/plan evidence, and apply the fix. The text format aggregates these into SUGGESTED COMMANDS and HINTS sections; the JSON format keeps them attached to each finding. Suggestions are printed as strings only — proxy analyze never executes them. When the proxy ran with --redact literals, the example SQL contains ? placeholders; fill in real values before running the commands.

Limitations (v1)

QueryLens — Proxy Query Analysis

QueryLens is the shareable Markdown report format of dbcli proxy analyze. It turns a proxy event log into a local query-analysis report; it is not a complete database-protocol analyzer.

It needs neither a database connection nor a dbcli connection configuration when analyzing: it reads the JSONL file on disk only. The QueryLens Markdown report redacts SQL literals before analysis. Capture with literal redaction as well, so the event file itself does not retain application values.

Quick Start

# 1. Capture local development traffic in a redacted JSONL log.
dbcli proxy mysql --listen 127.0.0.1:3307 --target 127.0.0.1:3306 \
  --events .dbcli/proxy/events.jsonl --redact literals

# 2. Produce the QueryLens Markdown report locally; no database is contacted.
dbcli proxy analyze \
  --events .dbcli/proxy/events.jsonl --format markdown

# The existing proxy JSON report remains available for machine consumption.
dbcli proxy analyze \
  --events .dbcli/proxy/events.jsonl --format json

Point the application at the proxy's --listen address while it runs. After stopping the proxy, run the analysis command against the resulting log from the repository root.

Analysis Options

OptionDescription
--events <path>Path to the proxy JSONL event log.
--format markdown|json|textmarkdown produces the redacted QueryLens report; JSON and text keep the standard proxy analysis output.
--top <n>Limit ranked findings to the top n entries.
--slow-ms <ms>Threshold used to classify slow queries in the report.
--n-plus-one <n>Repetition threshold used to flag N+1 candidates.
--no-include-rotatedAnalyze only the specified log, excluding rotated sibling logs.

QueryLens focuses on the proxy events it can read; do not rely on it as evidence that every database protocol or query form was captured. Treat its findings as investigation leads, then verify a query and its schema or plan before changing production code.

Advanced Tools

CommandPurpose
shellLaunch an interactive REPL with completion and SQL highlighting.
migrate <action>Run DDL workflows for creating, altering, or dropping SQL tables and indexes.
skill --installInstall the canonical English AI-agent instructions for Claude, Gemini, Antigravity, Codex, and similar tools.
skill contextSerialize cached schema, connections, and saved queries into LLM-optimized XML/JSON/Markdown structures for AI prompt injection.
semantic validate / semantic context / semantic search / semantic drift / semantic migrate / semantic draft validateValidate, print, search, check drift, stdout-migrate, or safely validate an explicit untrusted query draft against locally cached, blacklist-filtered semantic evidence. Offline and read-only.
contract validate / contract context / contract search / contract driftValidate, print, search, or check the optional reviewed dbcli.contracts.json against semantic evidence. Offline and read-only; ordinary agent context includes approved contracts only.
design init / design validate / design render / design diff / design proposeCreate an explicit local starter file, validate or render a version-controlled SQL database design, compare it with a local SQL schema cache or ORM definition, and produce a review-only change plan. Never executes DDL or silently writes a design.
impact assessWrite an offline declared-impact report for a design change against a local schema cache or ORM artifact. Never connects, executes SQL, or claims complete coverage.
skill tasksManage repeatable expert task packs for diagnostics, audits, and database workflows.
completionInstall shell completion for bash, zsh, or fish.

Shell completion

dbcli completion <bash|zsh|fish> prints a completion script; dbcli completion --install installs it. Installed completions cover nested subcommands — for example dbcli queries list --<TAB>, dbcli migrate add-column --<TAB>, and dbcli verify safe-backfill --<TAB>. They keep the leaf command scope after option values or positional arguments, such as dbcli queries list --format json --<TAB>.

Inside dbcli shell, command completion follows the current command surface, so newly added commands (q, queries, inspect, verify, proxy, snapshot, …) complete and dispatch automatically.

dbcli completion --install is marker-managed: it writes a single managed block to your shell rc file and re-running it replaces that block rather than duplicating it.

Builtin task pack analyze-table-perf

A read-only (plan-only) pack that takes a required table parameter and walks blacklist list → schema <table> --format json → guide index-usage --format json. dbcli inspect suggests it automatically for the hottest table in recent activity. Other read-only packs ship too — audit-permissions, safe-backfill, schema-drift-review, orm-drift-review, design-review, and connection-health. design-review validates/renders the artifact, refreshes the cache, and emits review-only proposals; it never applies them. Every pack requirement is a capability ID checked against the local engine, permission, and agent-mode context before a plan is emitted; unknown IDs and unavailable requirements fail closed. Browse all packs with dbcli skill tasks list.

Under DBCLI_AGENT_MODE=1, built-in packs can produce plans when their engine and permission requirements are met. Packs that inspect the blacklist require the readonly blacklist.read capability (blacklist list); connection-health needs no blacklist capability. blacklist.manage remains configuration-mutating and unavailable in agent mode, as do connection.init, connection.select, and connection.rotate-credential. Planning does not execute any step or grant permission to run it.

Business semantic context

Put a reviewable dbcli.semantic.json at the project root to describe business models, visible fields, aliases, relationships, and metrics backed by saved queries. Validate it without connecting to a database:

dbcli semantic validate --format json
dbcli semantic context --format json
dbcli semantic search purchases --kind model --format json
dbcli semantic drift --format json
dbcli semantic migrate --to 2 --format json
dbcli skill context --format json

How it is discovered. Installed dbcli skills tell agents to check skill context when a request uses business aliases, metrics, recurring terms, or relationship/join intent. If a validated semantic section is present, it is the governed vocabulary; otherwise the agent falls back to the blacklist-filtered schema and tells you this optional feature could make future requests consistent. dbcli never creates or changes dbcli.semantic.json unless you explicitly ask it to.

Version 1 remains compatible; v2 adds relationships between declared visible model fields. The validator rejects tables or columns absent from the cached visible schema, including blacklisted objects, and metrics whose query is not an available @saved-query. semantic search <terms...> is deterministic and offline; use --kind and --limit 1-100 (default 20). It returns only governed metadata and removes blacklist names from free-text results. semantic drift is non-zero for stale, invalid, or unavailable schema cache; migrate --to 2 prints JSON but never writes the source file.

Semantic contracts. Put an optional, reviewable dbcli.contracts.json beside the semantic file to add an owner and descriptive evidence expectation to a governed business term. It must use version 1, canonical contract names, canonical model: / field: / relationship: / metric: subjects, a draft, approved, or deprecated status, and an evidence policy of none, receipt-required, or verification-required. It cannot contain SQL, credentials, protected identifiers, or executable rules.

dbcli contract validate --format json
dbcli contract context --format json
dbcli contract search customer --format json
dbcli contract drift --format json

These commands never connect or execute queries. context, search, and skill context expose only valid approved contracts; draft and deprecated terms remain local review artifacts. A missing contract file leaves ordinary semantic context unchanged, while an explicitly requested missing or invalid file fails closed. contract drift distinguishes valid, stale, invalid, and unavailable local evidence: a subject that is not one of the four canonical forms is invalid, while a well-formed subject that no longer exists is stale. Diagnostics name the offending property or subject position, never a rejected key, value, or local path taken from the artifact or the local configuration.

Agent query drafts. First give the external agent reviewed dbcli semantic context --format json output; keep its provider account, credentials, prompt, and other agent context outside dbcli. It returns an untrusted QueryDraft using only those models and fields:

{
  "version": 1,
  "questionHash": "<sha256-of-the-original-question>",
  "candidate": { "kind": "sql", "sql": "<reviewed-read-only-sql>" },
  "semanticReferences": ["model:<model>", "field:<model>.<field>"]
}

Validate only that explicit file or stdin payload with dbcli semantic draft validate --input ./draft.json --format json or external-agent | dbcli semantic draft validate --input - --format json. The report contains only status, hashes, canonical references, and safe violation codes—never candidate SQL. Exit 0 is valid, 1 is rejected, and 2 means local semantic evidence is unavailable. A valid result is not execution permission: review the original draft, then separately invoke dbcli explain "<reviewed-read-only-sql>" or dbcli query "<reviewed-read-only-sql>" if intended. Validation neither saves the input nor invokes either command; dbcli does not receive provider credentials or make provider requests.

Database design assistant

Keep a reviewable dbcli.design.json beside a new project. It describes target PostgreSQL/MySQL/MariaDB models, fields, keys, relationships, indexes, access patterns, and decisions; it contains no SQL, credentials, rows, or provider configuration.

# Writes only to this explicit missing path; edit the starter before validation.
dbcli design init --output ./dbcli.design.json --dialect postgresql

# Both commands are offline and read-only.
dbcli design validate --format json
dbcli design render --format mermaid
dbcli design diff --against-cache --format markdown
dbcli design diff --against-orm ./prisma/schema.prisma --format markdown
dbcli design propose --against-orm ./prisma/schema.prisma --format markdown

validate fails closed for malformed artifacts, missing primary keys, invalid relationship endpoints/cardinality, incompatible relation types, and unsafe indexes; access-pattern index gaps are advisory. render emits JSON, Markdown, or Mermaid only after validation has no errors. diff --against-cache reads the existing local cache (run schema first), never opens a connection, and reports differing columns, indexes, and foreign keys. diff --against-orm is fully local: compare an explicit Prisma, DDL, Drizzle snapshot, or normalized JSON artifact without configuration or a database connection; DDL files may use globs. Both modes are read-only and choose exactly one comparison target. propose adds per-change blacklist/schema preflight, a dry-run command only where existing migration support is lossless, a rollback reminder, and an after-write read-only verification plan; every other change escalates to migration-review. It never applies a write. init is the only writer, requires --output, and refuses to overwrite. These commands do not call an LLM or query data. An external coding agent may draft the file, but a human should review it before relying on it.

New project workflow. Run design init, edit the explicit artifact, then run design validate and design render. If application models already exist, use the offline design diff --against-orm <path> to align the artifact and ORM before any database exists.

Existing database evolution. Run blacklist list, refresh the cache with schema --format json, then use design diff --against-cache and design propose --against-cache. Review the plan and separately perform any approved migration; afterwards refresh the schema and rerun the same diff. Neither design command writes the database.

Impact assessment

dbcli impact assess --design ./dbcli.design.json --against-cache --events ./.dbcli/proxy/events.jsonl --output ./impact.json --format json --fail-on warn
dbcli impact assess --design ./dbcli.design.json --against-orm ./prisma/schema.prisma --output ./impact.md --format markdown --fail-on never

Choose exactly one baseline. The command reads only local design/cache/ORM, semantic-contract, saved-query-name, verification metadata, and optional reviewed dbcli.data-access.json operation metadata. Its source paths must already exist inside the workspace; dbcli never reads or parses those source files. An optional explicit --events file is streamed through a redaction-first projection that retains only recent safe table metadata; it never starts a proxy, reads rotated logs, or renders SQL, literals, errors, sessions, or paths. Missing, malformed, stale, unreadable, or redaction-failed workload evidence remains an advisory coverage gap and cannot by itself make --fail-on warn fail. It never connects, refreshes the cache, executes SQL, reads query bodies, or publishes protected identifiers. Missing, invalid, or redacted evidence is a visible partial coverage gap; v1 never reports complete coverage. --fail-on changes only the exit code (error, warn, or never) after the report has been written.

safe-backfill-verify task plan and the verification block

Running dbcli skill tasks plan safe-backfill-verify --format json returns a plan JSON that includes a verification block with status: "planned". This block describes the read-back assertion that will be run — it is the planned evidence definition, not a result. A status of "planned" does not mean verification has run or passed; it means the task plan knows which check to perform when the task executes.

📊 Interactive HTML Dashboards

Generate standalone React-powered reports by appending the --ui flag.

dbcli query "SELECT day, count(*) FROM sessions" --ui

Rich visualizations (charts, KPIs) can be configured via the visual: block in snippet frontmatter. Supported chart types are line, bar, area, and pie; any other type is rejected at parse time.

When dbcli's lookahead proves that rows were truncated, the dashboard shows a warning before every KPI, chart, and table and names the applied limit. Blacklist redaction or omission notices appear there as well. This applies to query HTML/UI, saved-query HTML/UI, and HTML exports whenever the execution path produces the corresponding metadata.

Execution traceability (saved queries)

A dashboard generated from a saved query (dbcli q @name --ui or --format html) carries a standalone Execution Traceability section, shown after the truncation and blacklist notices and before the KPIs, charts, and table. It travels inside the HTML file, so a recipient sees it without dbcli, a database, or your workspace.

The applied-limit line always agrees with the truncation warning above it; a dashboard whose provenance and warning disagree is refused before the file is written.

Traceability is a closed contract. It never carries raw query bodies, parameter values or enums, credentials, endpoints, source paths, verification queries and expectations, target index or collection names, or rows beyond the ones displayed. The same allowlist governs the whole embedded payload, not just the visible section: only the displayed rows, the applied-limit and security notices, the provenance object, and the display name, description, and chart/KPI definitions that reference displayed fields are serialized. Invalid, oversized, or unknown metadata is rejected before any HTML is written, so a failed dashboard never leaves a partial file behind.

Direct-query dashboards (dbcli query --ui, dbcli export --format html) are unchanged and carry no traceability section.

Engine Support Matrix

Feature SQL SQLite MongoDB Redis ES
Querying ✅ ✅ ✅ ✅ ✅
Schema Caching ✅ ❌ ✅ ❌ ✅
Saved Snippets ✅ ✅ ✅ ✅ ✅
Data Writes ✅ ✅ ✅ ✅ ❌
DDL (Migrate) ✅ ❌ ❌ ❌ ❌
Interactive UI ✅ ❌ ✅ ✅ ✅
Query Size Guard ✅ ⚠️ ✅ ⚠️ ✅
Blacklist Enforcement ✅ ✅ ✅ ⚠️ ⚠️
Interactive Shell ✅ ❌ ✅ ⚠️ ⚠️

SQLite connection configuration

A SQLite connection names a database file, not a host. The only field that matters is file; there is no host, port, user, or password, because access control comes from the operating system's file permissions rather than a network authentication step. A minimal config looks like this:

{
  "version": 2,
  "default": "local",
  "connections": {
    "local": { "system": "sqlite", "file": "./data/app.sqlite", "permission": "query-only" }
  }
}

file accepts an {"$env": "APP_DB_PATH"} reference like every other connection field, and the same timeout / statementTimeout fields available to the other engines apply here too.

In-memory databases are refused. :memory:, file::memory:, and any path carrying mode=memory fail when the configuration is parsed, rather than at connect time. The reason is that a connection is the identity the blacklist, the audit log, and the schema cache are all scoped to, and an in-memory database is a different database on every invocation — see docs/adr/ADR-0038-an-in-memory-database-is-not-a-connection.md.

dbcli never creates a database file. A file path that does not exist fails at connect, and a path that exists but is not a SQLite database fails too, naming that as the reason rather than returning corrupt-looking data.

query-only is enforced by the engine, not by dbcli. Where PostgreSQL and MySQL run the statement inside a read-only transaction, SQLite has no such transaction, so dbcli instead opens the database file itself read-only (SQLITE_OPEN_READONLY) — a handle with no write channel at all. This has one cost worth stating plainly: a database left with an unreplayed write-ahead log cannot be opened read-only, because replaying the log is itself a write. dbcli reports that case by naming the WAL rather than surfacing a bare SQLITE_CANTOPEN. See docs/adr/ADR-0037-query-only-on-sqlite-closes-the-door.md.

ATTACH and DETACH are refused at every permission level, admin included, because they reach a database file outside the one the connection names — a connection-boundary question rather than a permission tier. Configure a second connection instead.

Creating a SQLite connection

dbcli init --system sqlite first asks for a connection name unless --conn-name is supplied, then asks for the database file path and permission, with no questions about a host, port, user, password or database name, because a SQLite connection has none of them. Non-interactively, provide both --conn-name and the path as --file:

dbcli init --system sqlite --file ./data/app.sqlite --conn-name local --permission query-only --no-interactive

The path is checked before anything is written. A path that does not exist, a path that is not a SQLite database, and any in-memory form are all refused with config.json left untouched — dbcli does not create the database for you.

dbcli use and dbcli status display the file path where they display a host for the other engines, so a SQLite connection never renders as an empty :0/ endpoint.

dbcli doctor checks the file rather than a network endpoint: that it exists, that it is readable, and — only where the permission allows writes — that it is writable. A read-only file under a query-only connection is correctly configured and is reported as such, not as a failure.

Existing v1 configurations cannot be upgraded to name SQLite. The v1 format predates the engine, so dbcli refuses the migration and points at dbcli init --system sqlite instead.

Today, init, use, status, list, schema, query, q, queries, export, doctor, insert, update, and delete work against a SQLite connection. Writes go through the same permission ladder and the same --dry-run as the other SQL engines, and the SQL they generate uses SQLite's standard double-quoted identifiers and ? placeholders. REPLACE INTO requires data-admin rather than read-write, because it removes a conflicting row before writing the replacement; PRAGMA, VACUUM and REINDEX require admin. migrate, diff, shell, inspect, and report are not supported for SQLite yet.

MongoDB connection configuration

dbcli init --system mongodb defaults to a field-by-field wizard, matching the SQL engines: Host, whether it is an SRV domain, Port (skipped for SRV), User, and — only when a User is set — Password plus authSource, followed by an optional advanced step for replicaSet / tls. Pasting a connection string copied from Atlas / Compass is the explicit second choice (labelled "Paste a connection string (copied from Atlas / Compass)"). Passing --uri skips the connection-mode prompt; noninteractive usage also requires --conn-name.

Unix socket connection strings (for example mongodb://%2Ftmp%2Fmongodb-27017.sock/app) are kept as the original uri, including credentials and options. They are never converted to TCP host/port fields. This applies to both --uri and interactive paste; the existing plaintext-credential warning still applies.

A pasted connection string is decomposed too. The interactive paste follows exactly the same rules as --uri (same field mapping, same fall-back to uri for multi-host, secret-option, repeated-key and non-SCRAM strings, with the same plaintext warning). After a successful decomposition the terminal lists every field that will be written — the password is shown as <redacted> and options are listed key by key — and init goes straight on to the permission prompt and the connection test, with no extra confirmation. If the string has a database in its path (or you passed --name), Database name is not asked; otherwise it is required and has no default value (the same applies to --uri in an interactive terminal, which used to default to testdb). A string that cannot be parsed (for example an invalid port or an empty authSource=) prints an error that does not repeat the string and asks again; an empty answer falls back to the field-by-field path. With --use-env-refs, a pasted string only triggers a warning: its credentials are written as they are and nothing becomes an environment-variable reference.

--uri is decomposed on write. A connection string passed with --uri (for example one copied from Atlas) is split into the field config above instead of being stored as one opaque uri: authSource, replicaSet and tls (or its alias ssl) become their dedicated fields, mongodb+srv becomes srv: true with no port, the database in the path becomes database (--name overrides it), and every other query parameter (retryWrites, w, appName, …) goes into options. A string without authSource writes none, with one exception: a non-SRV string that has a user and a path database writes authSource: <path database>, because the driver authenticates against the path database in that case while a field config without authSource would default to admin. User and password are percent-decoded. A string with no database in the path needs --name.

Some strings cannot be expressed as fields and are stored as-is in uri (the written connection then holds only system, uri and database), with a warning that the credentials will sit in plaintext in the config file: strings with more than one host; strings carrying a secret option (authMechanismProperties, tlsCertificateKeyFilePassword or proxyPassword, case-insensitive), which would otherwise land in plaintext options; strings that repeat a query key (case-insensitive, e.g. two readPreferenceTags), since a field or options entry holds one value; and strings whose authMechanism is not SCRAM-SHA-1 or SCRAM-SHA-256 (e.g. X509, GSSAPI, PLAIN, AWS). The warning names the repeated key or the mechanism. An explicit SCRAM authMechanism is decomposed normally and stays in options.

--uri cannot be combined with --host, --port, --user, --password, --auth-source, --replica-set, --srv, --tls or --no-tls (the error names the conflicting flag), and cannot be combined with --use-env-refs: either drop --use-env-refs so dbcli decomposes the string into the config, or use the --env-* flags to name one variable per field. --name may be combined with --uri.

Field-based connection options (ConnectionConfig, src/types/index.ts):

--auth-source <db>, --replica-set <name>, --srv and --tls / --no-tls are available as init flags, so CI can configure a replica set, SRV (Atlas) or TLS without a connection string. --srv writes srv: true and no port; with neither --tls nor --no-tls, no tls key is written and the driver default applies. In the interactive wizard, a flag you pass skips its own prompt (the SRV question, and the replicaSet / tls questions of the advanced step). The flags also apply with --use-env-refs, since they are not secrets.

If a config carries both uri and per-field values (host / user / options), uri wins and the per-field values are silently ignored, exactly as before — but dbcli doctor now reports this as a warning, so an edited field that "did nothing" is diagnosable instead of mysterious. doctor also warns when srv: true is combined with a non-default port, since SRV records carry their own ports.

A field-mode user with no password now fails closed with an error instead of silently downgrading to an unauthenticated connection. user, database, and password values are consistently percent-encoded when the URI is assembled, so characters such as @ or / in a username or database name no longer shift where the driver parses the authority.

Connection failures are classified into actionable hints: authentication failures point at user / password / authSource (Atlas and most self-hosted setups use admin); DNS/SRV failures point at the srv setting and local network DNS restrictions; TLS/certificate failures point at the tls field and CA trust configuration.

MongoDB write planner (operator tiers)

MongoDB nested blacklist

The dbcli config blacklist.columns[<collection>] accepts dotted paths, and every segment is a glob (*, ?, character classes like [abc]). pass* matches password; wildcards don't cross dots, so pass* does not match user.password. A wildcard in a middle segment is legal too — profile.*.email matches profile.<any segment>.email. A final segment that is exactly * keeps its special meaning: profile.tokens.* covers profile.tokens itself and everything beneath it. A rule that fails to compile (an empty path segment, an empty string, a non-string) now aborts the operation with an error naming the entry and the reason, instead of being dropped silently; read masking, request-side checks, and insert / update (which now covers every update operator) compare against the same compiled rules.

blacklist.tables is a glob for every engine. Only Redis and Elasticsearch expanded wildcards before, so tables: ["secrets*"] blocked nothing on MongoDB or SQL. secrets* now blocks the collection secrets_2026, and a SQL table of that name too — a breaking change; an object literally named report* has to be written report\* to match literally.

A request that names a protected field is refused, not masked. Masking matched the keys a document came back under, and in MongoDB the request chooses those keys: [{"$project":{"leak":"$password"}}] returned the value under leak, and [{"$group":{"_id":"$password"}}] returned it under _id, which masking exempts so that document references survive. Both ran at query-only before 4.0.0. So password appearing anywhere in a pipeline, filter or update — as a field path, an object key, or a plain string — is refused with BlacklistRejection, and masking stays for the ordinary document shape. This over-refuses in one direction on purpose: a filter whose value happens to equal a protected field name ({"status": "password"}) is refused too.

MongoDB schema sampling

dbcli schema <collection> [--sample-size 100] [--sample-method random|natural]. random (default) uses $sample; falls back to natural order on driver error. Columns include nested dot-paths with presence (0..1) and redacted: true for blacklist hits.

MongoDB saved queries

Mongo snippets end in .mongodb.sql and declare engine: mongodb with operation: find or operation: aggregate. Run with dbcli q @<key>; {{param}} placeholders are JSON-encoded so injected operator-shaped strings cannot escape into operator position.

Redis: size guard, blacklist, and shell (v1.21.0)

Size guard — unbounded reads are bounded automatically: SCAN/HSCAN/SSCAN/ZSCAN get COUNT 1000 injected (or a larger value capped); LRANGE/ZRANGE/ZREVRANGE clamp stop to a span of 1000; ZRANGEBYSCORE gets LIMIT 0 1000; HGETALL/HKEYS/HVALS/SMEMBERS/KEYS truncate at 1000. Results carry warnings[] (REDIS_SIZE_REWRITE / REDIS_SIZE_TRUNCATE). dbcli query prints each one on stderr, and a trimmed reply also reports truncated / limit_applied the same way every other engine does, so a capped reply is never mistaken for a complete one. Use --no-limit (CLI) or .no-limit on (shell) to bypass.

Blacklist — rules are enforced as Redis-native key globs (*, ?, [abc], [a-z]). dbcli blacklist table add 'secrets:*' then reads and writes whose keys match are rejected (BlacklistRejection, audited with matched_pattern); KEYS and SCAN MATCH overlapping a rule are rejected; a bare SCAN runs with protected key names removed from the reply; list filters blacklisted keys. Two of those were not true before 4.0.0: insert, update and delete compared the key name literally, so a rule written secrets:* — the spelling this page teaches — protected dbcli query and let dbcli delete secrets:api_key through; and SCAN's MATCH was never checked, so it enumerated protected key names at query-only while KEYS needed admin. A command dbcli has no key-arity entry for is now refused while a blacklist is configured, rather than passed through unchecked; with no blacklist configured nothing changes.

Masking (v1.22) — where the key-glob blacklist rejects, masking redacts: matched reads return [REDACTED] so an agent can run the command without seeing the sensitive value. Add an optional redis.mask block to your dbcli config, e.g. { "redis": { "mask": [{ "keyPattern": "user:*", "fields": ["password", "token"] }, { "keyPattern": "secret:*" }] } }. fields present → only those hash fields are redacted (HGETALL/HGET/HMGET); fields absent → the whole value is redacted (GET/GETRANGE and every hash field). Masking covers GET/GETRANGE/HGETALL/HGET/HMGET/HVALS. Rejection wins over masking — a key matching both a blacklist and a mask rule is rejected outright and never reaches masking.

Shell — dbcli shell on a Redis connection opens a single-line REPL with history, tab completion (commands + key prefixes), and a .no-limit on/off toggle. Type commands directly, no trailing semicolon (e.g. GET mykey).

Elasticsearch: interactive shell (v1.22.0)

dbcli shell on an Elasticsearch connection opens a dedicated Kibana Dev Tools-style REPL. Enter a request line <METHOD> /<path>, then an optional multi-line JSON body, and submit the whole block with a blank line. Responses render as pretty-printed JSON.

es> GET /_cat/indices
        (blank line submits)

es> POST /users/_search
... {
...   "query": { "match_all": {} }
... }
        (blank line submits)

Read-focused. Index-level blacklist rejects protected indices up front, and any _search request whose body lacks an explicit size is auto-capped at 1000 hits. A blank line submits the current block; Ctrl+C cancels the in-progress block; Ctrl+D or typing exit / quit leaves the shell.

Elasticsearch: export (v1.22.0)

dbcli export on an Elasticsearch connection writes documents to JSON, JSONL, or CSV. It accepts two forms:

# 1. Export the hits of a search DSL — requires --index
dbcli export '{"query":{"match":{"status":"open"}}}' --index orders --format json

# 2. Export a whole index via match_all + scroll — pass the index name as the query
dbcli export orders --format jsonl --output orders.jsonl

🤖 AI Agent Integration

Dbcli is designed to be the "Database Driver" for AI agents:

Bounded cross-engine context (version 2)

Use dbcli skill context --context-version 2 --format json to hand database metadata to an external agent. Version 2 is the stable agent contract: it is offline and never opens a connection, constructs an adapter, scans Redis keys, reads documents, or reads project source. The agent may inspect project code itself under its own workspace safety rules to supply business meaning that context does not contain.

dbcli skill context --context-version 2 --format json

Give the agent this output and its own safely discovered code context; do not give dbcli source paths or ask it to interpret natural language. Treat permission and descriptive capabilities as limits, not authority to execute a command. Use only returned resources and approved semantic/contracts metadata. If metadata is absent or gaps is returned, do not infer names, types, relationships, keys, or meanings: inspect permitted project code or ask for the missing evidence.

Version 2 emits only safe fields: configured engine and permission; blacklist policy; capability command, status, and sideEffectTier; resource/field IDs, names, and types; visible SQL nullable/primary-key and visible foreign-key links; flattened Elasticsearch field paths/types; declared Redis families/fields; snippet metadata without bodies/defaults; declared data-access metadata without source paths; approved semantic/contracts metadata; truncation counts; and gaps. It never emits credentials, values/results, defaults/counts, raw Elasticsearch mappings/settings, Redis keys/values, query bodies/defaults, or project source paths/contents. Blacklisted identifiers appear only in blacklist.

EngineVersion 2 resourcesBoundary
PostgreSQL, MySQL, MariaDBCached visible tables, safe columns, and visible foreign-key linksNo defaults, counts, indexes, comments, or filtered endpoints.
ElasticsearchCached indices with flattened field paths and typesNo raw mappings, _meta, settings, scripts, analyzers, documents, or counts.
RedisRepository-declared dbcli.redis-context.json key families and fieldsNo discovery, scans, concrete keys, live types, or values.

Redis declarations are intentionally small: context file ≤512 KiB; ≤500 families; lowercase kebab-case family names; patterns ≤200 characters with unique valid {placeholder} values and no glob tokens, whitespace, controls, or backslashes; and types limited to string, hash, list, set, zset, or stream. Only hash and stream may declare fields (≤100); descriptions are ≤1,000 characters and aliases are ≤20 of ≤100 characters. A malformed, concrete, unsafe, blacklisted, or Redis-field-mask-overlapping declaration fails rather than exposing a partial model.

Missing optional evidence reports SQL_SCHEMA_UNAVAILABLE, ELASTICSEARCH_MAPPING_UNAVAILABLE, REDIS_KEY_FAMILIES_UNAVAILABLE, SEMANTIC_CONTEXT_UNAVAILABLE, SAVED_QUERIES_UNAVAILABLE, DATA_ACCESS_UNAVAILABLE, or ALL_RESOURCES_FILTERED; truncation also reports CONTEXT_TRUNCATED. Present but invalid evidence fails with the corresponding INVALID_SCHEMA_CACHE, INVALID_SEMANTIC_CONTEXT, INVALID_SAVED_QUERY, INVALID_DATA_ACCESS_MANIFEST, INVALID_REDIS_CONTEXT, or INVALID_RESOURCE_REFERENCE. MongoDB and unknown engines reject explicit v2 with UNSUPPORTED_CONTEXT_ENGINE.

Omitting --context-version keeps byte-compatible version 1 JSON, XML, and Markdown output. version remains configuration metadata; v2 adds integer contextVersion: 2. Consumers must ignore unknown optional v2 fields; incompatible required-field or ID-encoding changes require a new context version.

Intent confirmation for business requests

The installed skill supports three per-request conversational preferences; they are not dbcli flags or saved configuration. An agent must not first ask the meta-question “do you want questions?”

PreferenceAgent behavior
auto (default)Resolve terms from governed semantic context and schema evidence. Ask one compact batch only when unresolved ambiguity would materially change the result; otherwise state assumptions and proceed.
confirmState the proposed interpretation and wait for approval before the task's data query.
guidedAsk short, focused questions to establish the request, and retain the answers through the task.

For example, “show yesterday's sales” is ambiguous when its result shape (total or detail), metric definition, timezone, status/refund treatment, grouping, or connection is unknown. The agent should summarize its candidate interpretation and ask only the result-changing questions. If the user explicitly asks it to decide without more questions, it proceeds in auto mode and discloses its material assumptions. This preference never bypasses blacklist, schema, permission, dry-run, production-selection, or write-confirmation gates.

When dbcli is a better fit than an MCP database server or a direct client

There is no universal winner. A direct database client is the shortest path when a trusted human is working in a disposable local environment. An MCP database server is useful when an agent host needs a conversational, tool-shaped integration for low-risk exploration. dbcli is the better boundary when the same database task must be safe and repeatable for an agent, a human, CI, or an incident runbook.

ChooseBest fitTrade-off
A direct database clientA trusted operator is making a one-off change with a least-privilege credentialThe safety, review, and evidence conventions must be rebuilt in every client and script.
An MCP database serverAn agent host needs interactive, low-risk discovery through its tool interfaceThe host and server define the authorization and audit surface; it is usually not the same surface used by CI or a terminal runbook.
dbcliA database operation must have one command contract across people, agents, automation, and recoveryIt is deliberately a CLI surface, so an agent needs shell-command authorization rather than a native MCP tool.

Quick visual decision guide

Choose the boundary that must survive the task.

01

Direct client

A throwaway local experiment, synthetic data, and a trusted developer watching every statement.

02

MCP database server

Interactive, low-risk exploration whose authorization and audit live in the agent host and tool server.

03

dbcli

Realistic data, shared fixtures, migrations, CI, incident work, or any result another person must rerun.

Local start

Synthetic scratch work

Move to dbcli when

the result needs evidence, review, or a next environment

Even for local vibe coding

Local development changes the blast radius, not the basic failure modes. A direct client is perfectly reasonable for a throwaway database with synthetic data when you are watching every statement. It is faster because the agent can connect, inspect, and run ad-hoc SQL immediately.

The trade-off is that the client session becomes the entire safety contract. The agent can still guess a column name, query a local production dump, run an unbounded statement against fixtures worth keeping, or leave a change that nobody can replay when the task moves to CI. A database client may offer its own protections, but they are not automatically the same policy or workflow that the rest of the project uses.

dbcli adds a small, deliberate pause before the agent acts: inspect the protected surface, read the real schema, and preview a write. The commands are also the handoff artifact. A developer can paste the same sequence into a terminal, a test script, or a PR description instead of reconstructing what happened in an agent-owned client session.

dbcli blacklist list
dbcli schema <table> --format json
dbcli update <table> --where "<predicate>" --set '<json>' --dry-run

That does not mean every local SELECT deserves ceremony. Use the direct client for a disposable experiment whose result has no operational life. Use dbcli as soon as the agent touches realistic data, modifies shared fixtures or migrations, needs a repeatable answer, or is likely to carry the work into CI, staging, or production. The value is not that the CLI makes local development magically safe; it makes the transition from a quick experiment to an accountable workflow explicit.

dbcli is stronger in the last case for four practical reasons:

  1. The operator can review and authorize the invocation. A command is a concrete artifact: a human can inspect the selected connection, operation, and flags before it runs. On hosts that authorize CLI arguments, such as Claude Code, this permits a useful gradient—safe discovery commands can be pre-authorized while queries or writes still require review. MCP permissions are commonly granted at the tool-name boundary; whether finer argument-level policy exists depends on the host and server.
  2. The same guardrails run outside the agent. Permission tiers, blacklist checks, result limits, schema discovery, dry-run preflights, and machine-readable output live on the CLI path. A CI job and an incident responder can use the same commands and receive the same enforcement instead of relying on a particular editor or agent session.
  3. The workflow leaves operational evidence. Use --recovery on supported commands to emit a structured failure envelope and link it to the audit record. Saved queries and task packs are files that a team can version, review, and reuse rather than instructions that exist only in a chat transcript.
  4. The tool still works when the agent is absent. A terminal, Makefile, CI job, or runbook can execute the command with meaningful exit codes. That makes dbcli a durable operational interface, not only an AI integration.

For example, the safe baseline for an agent-assisted data change is still a sequence a person or CI can reproduce:

dbcli blacklist list
dbcli schema <table> --format json
dbcli update <table> --where "<predicate>" --set '<json>' --dry-run

Use an MCP server when its conversational interface is the main value and its authorization, logging, and execution model satisfy the environment's requirements. Use a direct client when its simplicity is appropriate for the risk. Choose dbcli when the important product is the governed, reviewable execution path—not merely the ability to send a query.

Security boundary: dbcli is defence in depth, not a substitute for database authorization. Give the agent only a least-privilege database credential. A process that can change its own dbcli configuration can raise its declared permission or choose another client; blacklist and dry-run controls cannot make that credential safe by themselves.

🛠️ Developer Workflows

Beyond ad-hoc queries, dbcli supports the common development tasks where a database is involved. The agent skill ships a compact router for these; the same scenarios apply when you drive dbcli yourself:

All inherit the standard safety rules: prefer --format json, run blacklist list before touching sensitive data, confirm names with schema, dry-run writes, and never print credentials or blacklisted values.

🎯 Usage Scenarios

The Developer Workflows above are the minimum safe paths. This section maps concrete situations to an exact command path, grouped by how you arrive at them: a named task (prefer a published pack), a cross-cutting operational need, or an engine-specific job. Everything here inherits the safety baseline (blacklist list → schema → dry-run writes).

A. Task-pack scenarios

When a request matches a named workflow, discover and plan with a pack instead of inventing steps from memory. All packs are read-only plan-only and still inherit the blacklist → schema → dry-run rules.

slow-endpoint-investigation takes a required query and a required table, and plans in evidence order: blacklist list → proxy analyze --format json → schema <table> --format json → explain "<SQL>" → guide missing-index-for "<SQL>" --format json. The schema step comes before explain and index guidance on purpose — a plan or index candidate read against a table whose live shape you have not confirmed is a guess. Planning emits the commands and nothing else: it opens no connection, reads no proxy events or schema, runs no SQL, and invokes none of the steps. A proxy finding is what was observed locally, not the proven cause of endpoint latency, and an index candidate is review material — the workflow creates no index, applies no migration, and runs no DDL. Omitting query or table fails with a bounded error and emits no partial plan.

dbcli skill tasks list --format json                       # discover packs
dbcli skill tasks plan <pack> --param k=v --format json    # generate an ordered, risk-labelled plan
Situation (what the user says)PathPack
"This SQL is slow" (you have the statement)skill tasks plan diagnose-slow-query --param query="<SQL>" → lint "<SQL>" → guide missing-index-fordiagnose-slow-query
"Table X is hot / heavy" (you have the table)skill tasks plan analyze-table-perf --param table=<table>analyze-table-perf
"This API endpoint is slow"skill tasks plan slow-endpoint-investigation --param query="<SQL>" --param table=<table> (blacklist → proxy analyze → schema → explain → missing-index)slow-endpoint-investigation
Whole-environment perf scanreport --section perf → guide slow-queryreport + guide
"Audit access before granting writes"skill tasks plan audit-permissions (optional --param table=<table>)audit-permissions
"Does the live schema match the committed cache?"skill tasks plan schema-drift-review --param table=<table>schema-drift-review
"Does this ORM definition match the cached DB schema?"skill tasks plan orm-drift-review --param orm_path=prisma/schema.prismaorm-drift-review
"Is the connection healthy?"skill tasks plan connection-healthconnection-health
"Review this DB-touching PR"skill tasks plan pr-database-review; run DDL/index ideas through migration-review firstpr-database-review / migration-review
"Backfill column X safely"skill tasks plan safe-backfill-verify --param table=<t> --param query="<UPDATE>" --param verify_query="<SELECT count(*)>"safe-backfill / safe-backfill-verify

Packs resolve local > shared > builtin: assets/tasks/ (builtin), .dbcli-shared/tasks/ (team), .dbcli/tasks/ (local override). A plan never overrides blacklist, schema, dry-run, or confirmation requirements — execute its steps one at a time.

B. Cross-cutting scenarios

C. Engine-specific scenarios

SHOW GRANTS authentication redaction (MySQL/MariaDB)

SHOW GRANTS results are always redacted in the shared MySQL/MariaDB adapter before reaching any consumer, in both normal and native-read-only execution. This covers query, interactive shell, and SQL-query export, including JSON, CSV, table, and HTML output where supported. Protection applies at every permission level and in agent mode, works with an empty blacklist, and has no bypass option.

Conditional executable comments are interpreted using the server version already read when connecting; no extra version query is issued. If that version is unavailable and the actual statement cannot be determined safely, dbcli returns a fixed safe error without returning raw rows. Statements known not to be SHOW GRANTS keep their existing behavior.

For safely recognized grant text, authentication payloads become [REDACTED]; account names, roles, privilege scopes, and privileges remain readable. Recognized forms include IDENTIFIED BY, IDENTIFIED BY PASSWORD, and IDENTIFIED VIA / WITH with USING / AS, PASSWORD(...), or multiple methods joined by OR. All authentication parameters are masked, including empty strings and service names. Safely recognized privilege-only and role rows retain their original text.

Unknown, incomplete, ambiguous, or unexpected cell values become [OMITTED: grant text could not be safely redacted]. Row counts, column names, and result structure are preserved; other safely recognized cells remain visible. An omitted cell does not provide complete grant information. Redacted results are for inspection and are not replayable grant SQL.

This protection covers only SHOW GRANTS. It does not scan SHOW CREATE USER, direct catalog queries such as mysql.user, arbitrary SQL results, or secrets already present in SQL input, existing files, or history. Existing command and policy restrictions still apply; saved queries continue to reject SHOW GRANTS.

RDS CALL refusal and operator evidence handoff

Query-only continues to refuse CALL, including CALL mysql.rds_show_configuration; there is no RDS procedure allowlist. The actual CLI outcome is a local policy refusal. The DBA observation about the RDS configuration remains indeterminate (insufficient evidence), which is an evidence conclusion, not a new CLI or receipt status or schema. This refusal does not prove that the procedure is absent, that the native account lacks permission, that binlog retention is zero, or that backups are unhealthy. Missing evidence establishes neither success nor an unhealthy configuration.

The existing Q024/Q025 observations record local MySQL/MariaDB query-only refusals with both the default limit and --no-limit. They do not validate RDS procedure availability, native privileges, read-only transaction compatibility, or result handling. The shared permission boundary remains unchanged. Even if an existing refusal mentions admin, do not treat elevation as the remedy. An operator may obtain evidence only under existing authorization; dbcli does not automatically retry with admin, invoke a native client or AWS API, or modify RDS resources.

Keep the observations distinct: AWS documents binlog retention hours as binary log retention in hours, observed through mysql.rds_show_configuration. BackupRetentionPeriod is the automatic DB snapshot retention period in days. The snapshot field is not evidence of the binlog setting; do not substitute a platform API field without a source establishing equivalence for the required observation.

Copy this template into the human handoff and keep unavailable evidence explicit:

See Engine Support for the full capability matrix.

🔁 Agent Recovery Workflow

This section covers the three most common scenarios and the shared flow only. The full error-code matrix, multi-turn --next semantics, risk-gate details, and the Audit ↔ Envelope pivot live in reference.md Recovery Cookbook.

When any of query / q / insert / update / delete / export / schema / inspect / lint / diff --against-orm is invoked with --recovery and fails, a RecoveryEnvelope JSON is printed to stdout and atomically written to .dbcli/last-recovery.json. The agent then inspects it with dbcli recover or executes it with dbcli recover --apply (which by default only runs readonly + dry-run steps).

Native database authorization failures

FailureAdapter codeRecovery envelope
Database-native authorization denialDB_PERMISSION_DENIEDerror.code: "PERMISSION_DENIED", error.details.connectionCode: "DB_PERMISSION_DENIED"
Login authentication failureAUTH_FAILEDerror.code: "CONN_AUTH_FAILED"
dbcli local policy denialN/A (local PermissionError)error.code: "PERMISSION_DENIED" without the native discriminator

The native classification covers MySQL/MariaDB ER_DBACCESS_DENIED_ERROR (1044), ER_TABLEACCESS_DENIED_ERROR (1142), ER_COLUMNACCESS_DENIED_ERROR (1143), and ER_SPECIFIC_ACCESS_DENIED_ERROR (1227), including errno-only errors; PostgreSQL uses 42501 (insufficient_privilege). SQLSTATE 42000 or text containing “access denied” alone does not identify this classification. Login failures such as MySQL/MariaDB 1045 or 1698 and PostgreSQL 28000 or 28P01 keep their authentication classification.

Native authorization can fail while establishing a connection or executing an operation. This code does not prove login succeeded, and a successful connection does not prove permission to query an object. It is not a transport failure and does not trigger automatic reconnection or connection-repair branches.

Confirm the intended database/object and the privileges required for the operation; ask a DBA to resolve the denial when needed, then rerun the original command. The error provides static manual guidance: it does not suggest changing credentials, running init, or widening dbcli policy. Local canWrite/canDestruct flags cannot establish that native authorization is repaired.

Recovery stays at schema v1: recovery: [], with verify omitted (not JSON null). There are no executable repair or diagnostic commands for this path, so recover --apply performs no remediation. Existing v1 envelopes remain readable without migration; existing local PERMISSION_DENIED recovery is unchanged. Consumers should check both error layers shown above rather than treating all PERMISSION_DENIED envelopes as local policy failures.

Three typical scenarios

CONN_REFUSED — Connection refused

dbcli query "SELECT 1" --recovery
# envelope: recovery=[doctor, inspect] verify=doctor
dbcli recover --apply
# All readonly steps run, verifyStatus=passed → connection restored

BLACKLIST_TABLE — Blacklist block

dbcli query "SELECT * FROM audit_logs" --recovery
# envelope: blacklist list (readonly) + blacklist table remove (write)
dbcli recover --apply
# Step 2 mutates the local blacklist, so the gate skips it → exit 3
dbcli recover --apply --allow-write=readonly-cmd
# Confirm the unmask is intentional; opens local-write tier (still NOT the DB)

SCHEMA_CACHE_MISSING — Cache missing

dbcli inspect --require-schema-cache --recovery
# envelope: schema --refresh --force
dbcli recover --apply
# For v2 multi-connection setups the envelope includes --use <name>
# Per-connection cache lives at .dbcli/schemas/<connection>/

Multi-turn mode — for agents with their own runner

Use --next when --apply is too coarse (the plan contains an interactive step, or the agent wants step-by-step inspection):

dbcli recover --next --after-step 1 --result '{"status":"ok","exitCode":0}'
# For large outputs, pass a file (StepResultSummary JSON; stdout/stderr capped at the last 4 KB)
dbcli recover --next --after-step 2 --result @/tmp/r2.json
# When the plan completes, dbcli returns kind: "done".
# Note: --next does NOT run verify automatically — re-issue the original failing command to confirm recovery.

Branching for connection errors

For connection-class errors (CONN_REFUSED, CONN_AUTH_FAILED, CONN_TIMEOUT, CONN_HOST_NOT_FOUND, CONN_UNKNOWN), the envelope ships an additional branches map and a branchFork descriptor. After running step 1 (dbcli doctor --format json) the agent passes its output via --result; dbcli recover --next reads the doctor JSON, picks one of four labeled branches (doctor-clean, doctor-config-missing, doctor-auth-error, doctor-network-error), and returns the matching branch's first step. The response sets branchId and branchDescription so the agent can echo --branch <id> on subsequent --next calls.

FlagBehavior
--branch <id>Walk a specific branch by id. Required on all --next calls after the fork.

If the doctor JSON cannot be parsed or no keyword matches, --next falls back to the linear recovery plan — branching never causes --next to fail. --apply continues to walk recovery linearly and ignores branches.

One connection-class error does not branch: a statement canceled by the server for exceeding the statement timeout (PostgreSQL SQLSTATE 57014, MySQL 3024, MariaDB 1969). It reports as CONN_TIMEOUT with details.connectionCode set to STATEMENT_TIMEOUT, and that field is what separates it from a real connection timeout — the connection is healthy, so the plan works on the query (dbcli lint, dbcli explain, then a re-run with an explicit --statement-timeout <ms>) instead of running doctor. Because step 1 is not doctor, no branches or branchFork is emitted, and no verify step either — nothing verifies this error except re-running the statement, which only the caller has. All three steps carry placeholders, so dbcli recover --apply reports skipped-only and runs nothing.

Audit ↔ Envelope pivot

recover --apply exit-code cheat sheet

ExitMeaning
0All steps succeeded (and verify, if present, passed)
1A step failed
2Envelope missing, unreadable, or malformed
3Every step was skipped by the gate — widen --allow-write or fill placeholders, then retry

Persisting a verification artifact (opt-in)

Pass --write-verification-artifact to recover --apply to persist a bounded VerificationArtifact JSON under .dbcli/verification/ after the run:

dbcli recover --apply --write-verification-artifact

Troubleshooting & Error Reference

Error categories

dbcli distinguishes between connection errors (server down, auth failed) and SQL errors (syntax, missing table, missing column). SQL errors now print:

Already-categorized errors keep their original code, message, and hints across nested adapter calls, so messages are not prefixed twice. MySQL 8 schema introspection is compatible with the default ONLY_FULL_GROUP_BY mode.

When a connection's config fails schema validation, dbcli reports one line per invalid field path (resolved against that connection's declared system) instead of the raw nested union-error tree, so a broken .dbcli can be fixed without guessing which engine branch applies.

Bounded CLI error output

Connection-bearing discovery and read failures exit with code 1 and are presented once. In normal mode, stderr contains a readable message plus a stable error code and actionable hints when available; it does not contain a JavaScript stack, bundled source excerpt, or source-code frame. Add -v (or -vv) before the command to include the stack for diagnostics — the flag must precede the subcommand (dbcli -v list, not dbcli list -v). The write commands (insert, update, delete) and q keep their own localized wording, and honour the same stack switch. That wording is chosen by error code: only a genuine transport failure (ECONNREFUSED, ETIMEDOUT, AUTH_FAILED, ENOTFOUND, EHOSTUNREACH, CONNECTION_LOST, TOO_MANY_CONNECTIONS, TLS_ERROR, SERVER_NOT_READY, CONNECTION_REJECTED) is reported as a connection failure — a statement-level error such as TABLE_NOT_FOUND or STATEMENT_TIMEOUT reaches the server fine, so it is reported as itself, with its hints, rather than as "failed to connect". When a supported command uses --recovery, the existing JSON recovery envelope remains the only failure output on stdout and the duplicate human stderr message is suppressed.

Complete redirected stdout

When stdout is piped or redirected, dbcli completes the entire write before exiting. Large JSON, CSV, and HTML results therefore remain intact through commands such as dbcli query --format json | jq ... and dbcli export ... | cat > result.json; a successful exit never represents a partially written stdout buffer.

Query-only mode auto-LIMIT

dbcli auto-appends LIMIT 1000 to SELECT queries in query-only mode. This does not apply to:

Use --no-limit on SELECT to disable when querying information_schema.

For a dbcli-owned limit (the query-only default or an explicit --limit N), dbcli fetches one lookahead row so output can distinguish an exact N-row result from a larger result. Truncated table output ends with Rows: N (truncated; limit N). JSON always includes metadata.truncated and metadata.limit_applied when dbcli applied the limit; truncated is true only when the lookahead proved that another row existed. CSV appends a # truncated; limit N comment line. These fields are omitted for --no-limit and for a limit already written in the SQL, MongoDB pipeline, or Elasticsearch request body.

Saved snippets run through dbcli q report the same way. The snippet size guard wraps the body in its own 1000-row cap, and that cap is now stated in the footer and in metadata instead of leaving a round row count to be guessed at.

dbcli export does not report truncation — it refuses it. An export whose rows were capped by the query-only auto-limit exits 1 without writing a file:

Export would silently drop rows — 1000-row auto-limit reached.
  Re-run with --no-limit to export everything,
  or --limit 1000 to accept the cap explicitly.

An exported file has nowhere to record that rows are missing — jsonl is one document per line and MongoDB's --format json is a bare array — and a stderr warning does not survive redirection. Naming --no-limit or --limit N makes the choice explicit, and only then does the export proceed.

Bounded table cells

dbcli query table output limits each serialized cell to 120 Unicode code points by default. Use --truncate N to set a different positive-integer limit, or --no-truncate to show complete table cells. The two flags are mutually exclusive.

Cells keep their existing serialization rules before the limit is measured: null and undefined values become empty text, objects are serialized as JSON, and primitives use their string representation. If a serialized value exceeds the limit, dbcli retains the first N code points and appends a marker such as …(+3412 chars). The marker is outside the retained N-character budget, and its count is the number of omitted Unicode code points.

This behavior belongs only to table formatting and never mutates the query rows. JSON and CSV output therefore remain lossless. An explicit --truncate used with JSON, CSV, or HTML output (including --ui) is rejected instead of being silently ignored.

Schema cache bootstrap

The first dbcli schema --refresh after init writes the cache without --force. Subsequent refreshes that detect changes against an existing cache still require --force to overwrite.

Documentation Maintenance & Coverage

The Markdown (index.md) and polished HTML (index.html) files are two presentations of the same user guide. Maintain them as one documentation contract.

Parity Rules

  • Update Markdown and HTML in the same change for every command, flag, warning, example, or support-matrix update.
  • Keep shared topics in the same order with matching doc-key markers.
  • Match semantics even when the HTML uses cards, icons, or shorter labels.
  • Mirror English changes into the Traditional Chinese Markdown and HTML files.
  • Run bun run docs:check before merging documentation changes.

Required Coverage

Every user-facing feature should document setup, connection behavior, discovery commands, read/write operations, snippets, diagnostics, advanced tools, engine limits, AI workflow safety, and HTML dashboard behavior.

# Verify user-doc parity
bun run docs:check

# Run CLI tests for behavior changes
bun test
AreaRequired documentation
Installation & setupInstall commands, initialization, environment variables, and safe secret handling.
ConnectionsMulti-connection layout, listing, switching, one-shot --use, and environment examples.
Discoverylist, schema, inspect, status, output formats, and when AI agents should inspect before querying.
Reads & writesquery, q, export, insert, update, delete, --dry-run, and write guards.
Snippetsqueries list/search/suggest/new, resolution order, parameters, and visualization frontmatter.
Diagnostics & recoverydoctor, check, diff, report, guide, recover, and --recovery.
Advanced toolingshell, migrate, skill --install, skill tasks, completion, and supported permission levels.
EnginesPostgreSQL/MySQL/MariaDB, MongoDB, Redis, Elasticsearch support differences and known limitations.
AI usageBlacklist check, schema confirmation, dry-run/risk planning, then execution.
HTML dashboards--ui, export behavior, chart/KPI configuration, and browser/report expectations.

Query plan inspection — dbcli explain

Surface query plans across MySQL/MariaDB and PostgreSQL with a unified row schema and severity-coded annotations.

Basic usage

dbcli explain "SELECT * FROM betting_logs WHERE settled_at >= '2026-03-01'"
dbcli explain @analytics/live-summary               # saved query
dbcli explain --analyze "SELECT ..."                 # MariaDB ANALYZE SELECT / PG EXPLAIN ANALYZE
dbcli explain --format json "..."                    # JSON dump
dbcli explain --bulk @queries.sql                    # batch from file
dbcli explain --bulk @analytics/*                    # glob over saved queries

Annotations

RuleSeverityTriggered when
full-scanredMySQL type=ALL or key=NULL; PG Seq Scan
temp-tableyellowMySQL Using temporary
filesortyellowMySQL Using filesort; PG Sort Method: external merge
cost-estimate-skewgray--analyze actual rows / planner rows > 10×
nested-loop-largeyellowPG Nested Loop with planner rows > 10,000

Notes

Static SQL advisor — dbcli lint

lint analyzes PostgreSQL, MySQL, or MariaDB SQL without opening a database connection, executing the query, refreshing schema, or applying a rewrite. Schema-aware rules read only the layered .dbcli/schemas/ cache.

Inputs and options

dbcli lint "SELECT * FROM users WHERE email LIKE '%@example.com'"  # inline SQL
dbcli lint @analytics/live-summary                               # saved query
dbcli lint @queries.sql                                          # SQL file
dbcli lint --bulk '@queries/**/*.sql'                            # filesystem glob
dbcli lint --bulk '@analytics/*,@queries.sql' --format markdown  # mixed bulk inputs
dbcli --use staging lint @analytics/live-summary --format json   # named cache

All schema caches live beneath .dbcli/schemas/. A v2 configuration always uses .dbcli/schemas/<resolved-connection>/, including the configured default. The root .dbcli/schemas/ directory is only the v1/legacy unnamed cache. The global selector must precede the command: dbcli --use <conn> lint …; it selects another named v2 slot. lint never falls back to config.schema and never connects to refresh missing metadata.

OptionDefaultBehavior
--format text|json|markdowntextHuman text, machine JSON, or Markdown reports.
--min-severity info|warn|errorinfoHide findings below the selected severity.
--no-schemaoffDo not read schema-cache paths; skip schema-only checks while retaining static NOT IN NULL checks.
--bulk <input>noneResolve a comma-separated mix of @file, @glob, and @saved-query.
--recoveryoffOn failure, emit and save a linked recovery envelope.

Rules

RuleSeverityReports
select-starwarnTop-level SELECT *; an unambiguous single-table cache may supply a column-list draft.
unanchored-likewarnLIKE / ILIKE patterns beginning with %.
missing-limit-offsetinfoDeep pagination with OFFSET >= 1000; consider keyset pagination.
non-sargable-wherewarnFunctions or arithmetic applied to the column side of a predicate.
or-to-unioninfoTop-level OR across different columns; alternatives must preserve identity and multiplicity.
subquery-to-joininfoIN (SELECT …) that may benefit from a semantics-preserving EXISTS or proven-unique JOIN.
distinct-groupby-abusewarnRedundant DISTINCT when projected simple columns exactly cover GROUP BY.
implicit-castwarnA schema-verified column/literal type mismatch that can disable index use.
not-in-nullablewarnA right-hand NOT IN value that is NULL or may be nullable: explicit NULL, outer-join null extension, a nullable subquery projection, or a known nullable CASE/cast/aggregate expression.

not-in-nullable is the SQL “NULL poisons NOT IN” hazard on the right-hand side. It checks projections, JOIN ON, WHERE, and HAVING recursively, with each nested SELECT/CTE/derived statement using its own scope. A join's synthetic NULL extension is applied only after that join's own ON predicate; declared nullability and completed earlier joins still apply there. A nullable left-hand column is not this rule. For a subquery, filter its projected value with IS NOT NULL, or consider NOT EXISTS when correlation and semantics are appropriate. dbcli does not automatically perform that rewrite unless correlation, types, qualified-column resolution, and rewrite targeting are all unambiguous. A direct or AND-conjoined IS NOT NULL filter on the exact projected expression suppresses the finding; aggregate projections honor the same proof in HAVING. OR and ambiguous matches remain conservative findings.

Parse failures block all nine rules with blocked: parse failed. --no-schema skips implicit-cast and marks only the unavailable schema-dependent portion of not-in-nullable as blocked: --no-schema; explicit NULL and structurally known RHS hazards still run. A missing layered cache reports blocked: schema cache unavailable (run dbcli schema) for unavailable schema checks. Confidence-labelled drafts and shell-safe verification commands are report-only suggestions and are never run. dbcli explain --analyze is emitted only for structurally proven read-only SQL without explicit function calls, table-function calls, or session-variable assignments; other statements fall back to plain dbcli explain. Folded identifier collisions and CTE, derived, schema-qualified, or database-qualified bindings never borrow unqualified cache facts.

Missing-index advisor — dbcli guide missing-index-for

Analyse a single SELECT and suggest composite indexes, grounded in a real EXPLAIN plan and your existing indexes. Read-only.

Basic usage

dbcli guide missing-index-for "SELECT ... FROM betting_logs b JOIN hoster_machines hm ON ..."
dbcli guide missing-index-for @analytics/live-summary
dbcli guide missing-index-for "..." --format json        # yaml (default) | json | markdown
dbcli guide missing-index-for "..." --min-confidence medium

Each candidate carries a confidence (high / medium / low) and a reason; the tool never asserts "you must create this". Functional/expression columns (e.g. DATE(settled_at)) and unparseable SQL are reported under warnings.

Limits