Build a new Python CLI named sql-agent-cli that runs safe, read-only SQL queries against local or remote databases and emits deterministic, agent-friendly output.
The tool should follow the same broad product pattern as similar agent-first CLIs such as confluence-fetch:
- local single-file execution via
uv run ./sql_agent_cli.py ...using PEP 723 inline script metadata - packaged execution via
uvx sql-agent-cli ... - agent-first stdout/stderr behavior with stable output contracts
Primary audience:
- agentic coding tools such as Codex CLI and Claude Code
Secondary audience:
- humans running ad hoc database inspection commands directly in a terminal
License:
- MIT
- The primary happy path is
sql-agent-cli "SELECT ..."against a configured default target. - The tool supports named targets so multiple databases can be queried safely from one CLI.
- V1 supports MySQL, MariaDB, PostgreSQL, and SQLite.
- The internal design leaves room for SQL Server later without redesigning the config model.
- The tool is strictly read-only in v1.
- Stdout contains payload only. Diagnostics, warnings, progress, and errors go to stderr only.
- The default output is optimized for agents, not for humans staring at a terminal.
- The project is publishable so
uvx sql-agent-cli --helpworks. - The repo also keeps a root-level
sql_agent_cli.pywrapper foruv run.
- Write queries in v1.
- Interactive TUI behavior.
- Backward compatibility with
sql-query.py. - Cross-database joins or fan-out queries in a single invocation.
- Full database administration features.
sql-agent-cli
sql-agent-cli/
pyproject.toml
README.md
sql_agent_cli.py
spec.md
src/
sql_agent/
__init__.py
cli.py
config.py
models.py
validation.py
render.py
engines/
__init__.py
base.py
mysql.py
postgres.py
sqlite.py
- Publishable Python package via
pyproject.toml. - Console script entry point exposed as
sql-agent-cli. - Root-level
sql_agent_cli.pywrapper with PEP 723 metadata for local script execution. - The wrapper delegates into packaged implementation instead of duplicating the application logic.
Recommended implementation dependencies:
PyMySQL[rsa]for MySQL and MariaDBpsycopg[binary]for PostgreSQL- standard-library
sqlite3for SQLite sqlglotfor parser-backed SQL validation
Rationale:
- these choices are compatible with the
uvxpackaging and execution model PyMySQL[rsa]avoids common MySQL authentication friction while remaining easy to shippsycopg[binary]avoids common system-library installation issues for PostgreSQLsqlglotprovides token-aware, dialect-aware SQL parsing suitable for read-only validation
sql_agent_cli.py must include inline script metadata similar to:
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "...",
# ]
# ///Notes:
- The wrapper may add
src/tosys.pathduring local development. - The package itself still uses
pyproject.tomlmetadata foruv buildanduv publish.
- MySQL
- MariaDB
- PostgreSQL
- SQLite
The target/config model must be engine-neutral enough that SQL Server can be added later without changing the user-facing target structure.
That means:
- targets are named records with an explicit
engine - shared concepts such as host, port, database name, SSL mode, and read-only behavior live in common models where practical
- engine-specific fields may exist where required
- Payload output goes to stdout only.
- Diagnostics, warnings, progress, and errors go to stderr only.
- Resolution rules must be deterministic and documented in
--help. - The default output must be structured enough for agents to consume reliably.
- Read-only safety must be enforced before any query is executed.
- Missing target or credential context must fail fast with corrective error text.
Primary happy path:
sql-agent-cli "SELECT ..."
Named target:
sql-agent-cli --target reporting "SELECT ..."
Explicit query flag:
sql-agent-cli --target reporting --query "SELECT ..."
SQL file:
sql-agent-cli --target reporting --sql-file query.sql
Stdin:
Get-Content query.sql | sql-agent-cli --target reporting
V1 should support exactly one query source per invocation:
- positional query text
--query SQL--sql-file PATH- stdin when no positional query and no query flag/file were provided
If more than one query source is supplied, fail with a usage error.
Resolution order:
--target NAME[defaults].target- fail with a clear error if no target is available
The tool should also support one-off execution without config by allowing connection details on the CLI.
Examples:
sql-agent-cli --engine mysql --host db.example.com --database app --user paul "SELECT ..."
sql-agent-cli --engine postgres --host db.example.com --database app --user paul --password-stdin "SELECT ..."
sql-agent-cli --engine sqlite --path C:\data\app.db "SELECT ..."
Behavior:
- CLI connection flags can define a full ephemeral target for the current run.
- CLI connection flags override the selected config target field-by-field.
- If no target is specified and no default target exists, a complete ephemeral CLI target is allowed.
- If the resulting target is incomplete, fail with a clear validation error before attempting a connection.
V1 should avoid password-as-argument by default.
Recommended supported methods:
- native engine credential resolution
--password-stdin- optional
--prompt-passwordfor human use
V1 should not require --password as a normal CLI argument.
sql-agent-cli "SELECT ..."
sql-agent-cli --target NAME "SELECT ..."
sql-agent-cli config show
sql-agent-cli config set-default-target NAME
sql-agent-cli config add-target NAME [options]
sql-agent-cli config remove-target NAME
sql-agent-cli config init-native-auth --engine postgres [--target NAME]
sql-agent-cli config init-native-auth --engine mysql [--target NAME]
sql-agent-cli targets
Compatibility requirement:
- none
V1 must execute exactly one SQL statement per invocation.
Recommended behavior:
- allow a single trailing semicolon and strip it before validation
- reject multiple statements
- reject empty input
Rationale:
- agents frequently emit a harmless trailing semicolon
- stacked statements materially increase risk and complexity
Implementation requirement:
- validation should be token-aware or parser-backed rather than a simple regex-only keyword scan
Recommended implementation:
- use
sqlglotwith the selected engine dialect and require exactly one parsed statement
V1 should allow only read-oriented statements.
Required allowlist:
SELECTWITH ... SELECTSHOWDESCRIBEandDESCEXPLAIN
Engine-specific notes:
- SQLite may also allow read-only
PRAGMAstatements if they are explicitly validated as non-mutating. USEmust not be allowed. Target selection belongs to CLI/config, not SQL text.- Stored procedures, dynamic SQL, and administrative commands must not be allowed.
The validator should reject statements containing or invoking mutating or administrative behavior, including but not limited to:
INSERTUPDATEDELETEREPLACEMERGEUPSERTDROPALTERCREATETRUNCATEGRANTREVOKELOCKUNLOCKCALLEXECPREPAREDEALLOCATESETLOAD_FILEINTO OUTFILEINTO DUMPFILE- sleep/benchmark-style functions
Validation must happen before a database connection is attempted where practical.
Validation requirements:
- comments must not confuse statement classification
- quoted strings and identifiers must not be treated as executable keywords
- semicolons inside quoted literals must not be treated as statement separators
- CTE-based
WITH ... SELECTqueries must be accepted when they remain read-only - parse failures should return a clear validation error instead of falling through to execution
Where the underlying driver/database supports a true read-only connection mode, the tool should enable it.
Examples:
- SQLite connections should be opened in read-only mode when possible.
- MySQL/MariaDB should use the strongest practical read-only session settings the driver supports, but SQL validation remains the primary safety boundary.
V1 should support:
jsonmarkdowntablecsv
Default output format:
json
Rationale:
- JSON is the best default for agents because it can carry structured metadata, truncation status, and stable field names.
Recommended top-level structure:
{
"target": {
"name": "default",
"engine": "mysql",
"database": "app",
"host": "db.example.com"
},
"query": {
"input": "SELECT id, name FROM users",
"normalized": "SELECT id, name FROM users",
"statement_type": "select"
},
"result": {
"columns": ["id", "name"],
"rows": [
[1, "Alice"],
[2, "Bob"]
],
"returned_row_count": 2,
"truncated": false
}
}The exact field names may change during implementation, but the payload should include:
- resolved target metadata with secrets omitted
- normalized query text
- column names
- rows
- returned row count
- truncation indicator
Serialization requirements:
- datetimes and dates should be emitted as ISO 8601 strings
- decimals should be emitted as strings to avoid silent precision loss
- bytes/blob values should be emitted as base64 strings or explicit placeholders; the choice should be documented and stable
NULLshould become JSONnull- UUID-like values may be emitted as strings
Row count semantics:
- use
returned_row_countfor the number of rows actually present in the payload - use
truncatedto indicate whether rows were omitted due tomax_rows - do not claim a
total_row_countunless the tool actually computed it
markdownshould render a stable prompt-friendly table plus a short metadata header.tableshould optimize for terminal readability.csvshould emit raw CSV payload only, with no explanatory text on stdout.
Use a single user config file at:
~/.sql-agent-cli/config.toml
The default target should be an alias, not a duplicated block of connection data.
Recommended shape:
[defaults]
target = "dev"
format = "json"
max_rows = 200
connect_timeout_seconds = 8
query_timeout_seconds = 15
[targets.dev]
engine = "mysql"
host = "az-mysql-pub-sona-asia1-dev.mysql.database.azure.com"
port = 3306
database = "asiadev_2794"
user = "paul"
ssl_mode = "required"
[targets.reporting]
engine = "postgres"
host = "db.example.com"
port = 5432
database = "app"
user = "report_reader"
[targets.local_sqlite]
engine = "sqlite"
path = "C:/data/app.db"This is the preferred approach because:
[defaults].targetclearly selects the default target- all connection details live in exactly one place
- renaming or editing a target does not require syncing duplicated settings
V1 should prefer native engine credential mechanisms and non-interactive secret input over storing plaintext passwords in ~/.sql-agent-cli/config.toml.
Recommended policy:
- config stores target selection and non-secret defaults first
- credentials should be resolved through native engine mechanisms where practical
--password-stdinis the generic fallback for automation--prompt-passwordmay exist as an optional human convenience- plaintext passwords in
~/.sql-agent-cli/config.tomlshould not be the preferred design
Recommended target fields:
Shared fields:
enginedatabaseuserhostportssl_modemax_rowsconnect_timeout_secondsquery_timeout_seconds
SQLite-specific fields:
path
Notes:
ssl_modeshould default to secure behavior where relevant.- SQLite ignores network-only fields.
- The implementation may allow additional engine-specific fields later.
- Engine-specific credential hints may be stored in config, but raw passwords should not be the preferred v1 path.
The tool should reuse existing engine conventions where practical instead of inventing a separate credential system for every database.
Preferred support:
- libpq-style environment variables such as
PGHOST,PGPORT,PGDATABASE,PGUSER, andPGPASSWORD .pgpass- optionally
PGSERVICElater if implementation remains clean
Behavior:
- if explicit CLI/config target fields are missing, the tool may resolve compatible PostgreSQL defaults from libpq-style environment variables
.pgpasssupport is desirable because it is already standard for non-interactive PostgreSQL use- on Windows, the tool should support both
%APPDATA%\postgresql\pgpass.confand~/.pgpass - on non-Windows platforms, the tool should support
~/.pgpass
Preferred support:
- standard MySQL option files such as
~/.my.cnf/my.cnf
Behavior:
- using native option-file resolution is preferred over introducing a custom password store
- support for MySQL login-path credentials managed by
mysql_config_editorvia.mylogin.cnfis explicitly deferred from initial v1 - on Windows, the tool should support both
%APPDATA%\MySQL\.my.cnfand~/.my.cnfif implementation remains straightforward - on non-Windows platforms, the tool should support
~/.my.cnf
For engines where native resolution is unavailable or inconvenient, support:
--password-stdin- optional
--prompt-password
Rationale:
- agents need non-interactive secret input
- humans sometimes want a prompt
- native client mechanisms are often already configured on developer machines
Recommended behavior:
- default to
ssl_mode = "required"for MySQL and MariaDB targets - default to
ssl_mode = "required"for PostgreSQL targets - support a per-target config value for
ssl_mode - support a shared user-facing
ssl_modemodel ofrequired,preferred, ordisabled - map the shared
ssl_modevalues onto engine-specific driver options internally - support an explicit CLI
--ssl-mode {required,preferred,disabled}override - support a CLI
--insecureshorthand that means--ssl-mode preferred - support an explicit less-secure override when needed, such as
--ssl-mode disabled --insecureshould not modify config permanently
Rationale:
- SSL behavior is a target attribute, but
--insecureis a useful temporary override for troubleshooting
Recommended commands:
sql-agent-cli config show
sql-agent-cli config set-default-target NAME
sql-agent-cli config add-target NAME --engine mysql --host HOST --port 3306 --database DB --user USER
sql-agent-cli config add-target NAME --engine postgres --host HOST --port 5432 --database DB --user USER
sql-agent-cli config add-target NAME --engine sqlite --path PATH
sql-agent-cli config remove-target NAME
sql-agent-cli config init-native-auth --engine postgres [--target NAME]
sql-agent-cli config init-native-auth --engine mysql [--target NAME]
Behavior requirements:
config showdisplays the effective defaults and configured targets with credential sources described but secrets redactedconfig showshould indicate whether each target is complete enough to run- config-writing commands create the file if it does not exist
- config-writing commands preserve unrelated existing settings where practical
V1 should help users bootstrap native credential files without requiring them to remember the exact file format.
Recommended command:
sql-agent-cli config init-native-auth --engine postgres [--target NAME]
sql-agent-cli config init-native-auth --engine mysql [--target NAME]
Recommended behavior:
- create the standard file only if it does not already exist, unless an explicit overwrite flag is later added
- write a commented template with placeholders and brief inline instructions
- print the exact path written and the next step for the user
- never auto-fill or persist secrets unless the user explicitly supplied them for that purpose
- for PostgreSQL, set restrictive file permissions where required and warn if that cannot be done
- if the file already exists, do not modify it silently; print a clear message instead
- when
--target NAMEis supplied, prefill non-secret fields such as host, port, database, and user from the selected target where those fields are relevant
PostgreSQL helper requirements:
- seed a
.pgpasstemplate in the preferred platform-specific user location - include the expected
hostname:port:database:username:passwordrecord shape - explain that PostgreSQL expects restrictive permissions on the file
- when
--target NAMEis supplied, prefill host, port, database, and user from the target and leave password blank
MySQL helper requirements:
- seed a
~/.my.cnftemplate or platform-appropriate user option file - include a minimal
[client]example section with host, user, password, and optional port - note that the file stores plaintext secrets and should be protected with filesystem permissions
- when
--target NAMEis supplied, prefill host, port, and user from the target and leave password blank
Platform-specific path rules:
- on Windows, PostgreSQL lookup and template helpers should support both
%APPDATA%\postgresql\pgpass.confand~/.pgpass - on Windows, MySQL lookup and template helpers should support both
%APPDATA%\MySQL\.my.cnfand~/.my.cnfif practical - when both locations exist, prefer the explicit native platform path first, then the
~/fallback - when seeding a new file, prefer the native platform path unless the user later requests an explicit path override
Rationale:
- this reduces setup friction without inventing a proprietary credential format
- it keeps the tool aligned with native client conventions
- it avoids requiring users to search for file syntax documentation during setup
Recommended precedence:
- explicit CLI connection flags such as
--host,--database,--user,--path,--engine,--max-rows,--connect-timeout-seconds,--query-timeout-seconds,--insecure - selected target from
--target NAME - default target named by
[defaults].target - global defaults from
[defaults]for non-connection behavior such as output format and row limits
Credential precedence should be documented per engine, but the generic model should be:
--password-stdin--prompt-password- engine-native credential mechanisms
- fail with a clear auth error
Recommended order:
- connection identity fields explicitly provided on the CLI
- selected target fields from config
- libpq-style environment variables for any still-missing connection fields
--password-stdin--prompt-passwordPGPASSWORD.pgpasslookup for password resolution using the resolved host/port/database/user tuple- fail with a clear auth error
Recommended order:
- connection identity fields explicitly provided on the CLI
- selected target fields from config
--password-stdin--prompt-password- standard MySQL option files for any still-missing connection fields and password resolution
- fail with a clear auth error
Only one query source may be provided. If more than one is supplied, fail.
Accepted sources:
- positional query
--query--sql-file- stdin
--format[defaults].format- built-in default
json
V1 should enforce sensible default guardrails, overridable by config or CLI.
Recommended defaults:
max_rows = 200connect_timeout_seconds = 8query_timeout_seconds = 15
Behavior:
- results are truncated to
max_rows - truncation is reflected in output metadata
- timeout failures return a stable non-zero exit code and a concise stderr error
Recommended contract:
0success1runtime, connection, driver, or query execution failure2usage or validation error
Requirements:
- use a mature driver with TLS support
- support host, port, database, and user
- apply secure SSL defaults unless
--insecureis used - support native option-file based credential resolution where practical
- do not document or guarantee
MYSQL_PWDas a public credential source
Requirements:
- use a mature driver that aligns with libpq-style connection behavior where practical
- support host, port, database, and user
- support standard PostgreSQL credential conventions such as
.pgpass - support secure SSL defaults unless explicitly overridden
- map shared
ssl_modevalues onto PostgreSQL driver-specific SSL settings internally
Requirements:
- accept a filesystem path instead of host/port/database
- open the database in read-only mode where possible
- document any platform-specific path handling in
--help
The project should include a release workflow suitable for public PyPI publishing.
Release expectations:
uv build --no-sources- publish via
uv publishor a GitHub Actions workflow - prefer PyPI Trusted Publishing over long-lived API tokens
The README should include:
- quick start for the default target model
- one-off CLI-only connection examples
- config file examples for MySQL, PostgreSQL, and SQLite
- examples showing
.pgpass, MySQL option files,config init-native-auth --target NAME, and--password-stdin - a clear statement that v1 is read-only by design
These are intentionally out of scope for initial v1 unless implementation is unexpectedly cheap:
- SQL Server support
- OS keychain-backed credential storage
- write-enabled mode
- connection profile inheritance
- schema introspection commands beyond what the query interface already allows
- MySQL login-path support via
.mylogin.cnf