QueryGate is a read-only SQL gateway CLI for AI agents. It parses PostgreSQL SELECT queries with a real SQL AST, validates them against a user-level allowlist policy, and executes approved queries through a read-only database session.
cargo install --path .By default, QueryGate loads configuration from:
~/.queryGate/config.yaml~/.queryGate/config.yml~/.queryGate/config
Override with --config <path> or the QUERYGATE_CONFIG environment variable.
querygate init --url "postgres://user:pass@host:5432/mydb"This introspects PostgreSQL metadata and writes ~/.queryGate/config.yaml. It never overwrites an existing file. Use --output to choose a different path.
Options:
--database <name>— profile name (default:app)--url-env <VAR>— env var for the runtime connection URL (default:QUERYGATE_APP_DATABASE_URL)--default-schema <schema>— default schema for unqualified names (default:public)--include-schema <schema>— repeat to include additional schemas--suggest-safe— opt in to marking columns that do not look sensitive asallowed
Set the connection URL after init:
export QUERYGATE_APP_DATABASE_URL="postgres://querygate_app:...@host:5432/mydb"For databases that require TLS, include the standard Postgres SSL mode in the connection URL:
export QUERYGATE_APP_DATABASE_URL="postgres://querygate_app:...@host:5432/mydb?sslmode=require"If you need the Node.js-style rejectUnauthorized=false behavior for a database with an untrusted or mismatched certificate, QueryGate accepts that URL parameter and disables TLS certificate verification for that connection:
export QUERYGATE_APP_DATABASE_URL="postgres://querygate_app:...@host:5432/mydb?sslmode=require&rejectUnauthorized=false"Only use rejectUnauthorized=false for trusted networks or development databases. It keeps the connection encrypted, but it disables verification that the server certificate belongs to the database host.
Example ~/.queryGate/config.yaml:
version: 1
databases:
app:
description: Safe production app data for support agents.
url_env: QUERYGATE_APP_DATABASE_URL
dialect: postgres
statement_timeout_ms: 10000
max_rows: 1000
policy:
default_schema: public
tables:
public.users:
access: allowed
default_column_access: denied
columns:
id:
access: allowed
created_at:
access: allowed
country:
access: allowed
public.orders:
access: allowed
default_column_access: allowed
functions:
- count
- sum
- avg
- min
- max
- date_truncAccess rules:
- Tables and columns not listed under
policy.tablesare denied. - Missing
accessdefaults to denied. - Missing
default_column_accessdefaults to denied. - A table is queryable only when
access: allowed. - A column is queryable when the table is allowed and either the column has
access: allowedordefault_column_access: allowed. SELECT *is allowed only when the table is allowed anddefault_column_access: allowed.- Functions are denied unless listed under
policy.functions.
querygate schema shows agents the effective allowlist only: denied tables and columns are omitted.
See examples/querygate.yaml for a multi-database sample.
# List configured databases
querygate databases
# Inspect allowlist for a profile
querygate schema --database app
# Validate without executing
querygate validate --database app --sql "select id from public.users"
# Execute and return JSON rows
querygate run --database app --sql "select id, created_at from public.users limit 10"
# Pipe SQL from stdin
querygate run --database app < query.sqlIf only one database profile is configured, --database may be omitted.
Successful execution:
{
"ok": true,
"columns": ["id", "created_at"],
"rows": [{ "id": 123, "created_at": "2026-05-19T10:30:00Z" }],
"row_count": 1,
"truncated": false,
"database": "app"
}Validation failure (stderr):
{
"ok": false,
"error": {
"code": "column_not_allowed",
"message": "Column public.users.email is not allowed by the policy.",
"hint": "Use `querygate schema --database app` to see allowed tables and columns."
}
}| Code | Meaning |
|---|---|
| 0 | Success |
| 2 | Config error |
| 3 | SQL parse error |
| 4 | Policy validation error |
| 5 | Database execution error |
Create a dedicated read-only role per database profile:
CREATE ROLE querygate_app LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE mydb TO querygate_app;
GRANT USAGE ON SCHEMA public TO querygate_app;
GRANT SELECT ON public.users TO querygate_app;
GRANT SELECT ON public.orders TO querygate_app;
-- Prefer column-level grants or safe views for PII tablesSet the connection URL:
export QUERYGATE_APP_DATABASE_URL="postgres://querygate_app:...@host:5432/mydb"QueryGate reduces PII exposure for AI agents, but it is not a substitute for database permissions.
- Use a dedicated read-only PostgreSQL role per profile.
- Grant only
CONNECT,USAGE, andSELECTon approved schemas/tables/views. - Mirror the allowlist with PostgreSQL column privileges or safe views where possible.
- Prefer pre-aggregated views for analytics instead of raw customer tables.
- Agents can still infer sensitive facts from allowed aggregate columns or low-cardinality filters.
- Query logs may contain sensitive literals even when selected columns are safe.
Application-side safeguards:
- AST-based validation (not regex).
- Single
SELECTstatement only. - Read-only transactions (
BEGIN READ ONLY). statement_timeoutper profile.- Automatic outer
LIMITwhen the query has noLIMIT.
cargo test
cargo build --releaseOptional database integration test (requires a reachable PostgreSQL instance):
export QUERYGATE_TEST_DATABASE_URL="postgres://..."
cargo test -- --ignoredMIT