postgres-readonly-queries
Execute safe read-only SQL queries against PostgreSQL databases with multi-connection support and defense-in-depth write protection.
Install
npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/postgres-readonly-queries
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
git clone https://github.com/sickn33/agentic-awesome-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.
README
postgres
Read-only PostgreSQL query skill. Query multiple databases safely with write protection.
Setup
- Copy the example config:
cp connections.example.json connections.json
- Add your database credentials:
{
"databases": [
{
"name": "prod",
"description": "Production - users, orders, transactions",
"host": "db.example.com",
"port": 5432,
"database": "app_prod",
"user": "readonly",
"password": "secret",
"sslmode": "require"
}
]
}
- Secure the config:
chmod 600 connections.json
Usage
# List configured databases
python3 scripts/query.py --list
# List tables
python3 scripts/query.py --db prod --tables
# Show schema
python3 scripts/query.py --db prod --schema
# Run query
python3 scripts/query.py --db prod --query "SELECT * FROM users" --limit 100
Config Fields
| Field | Required | Default | Description |
|---|---|---|---|
| name | Yes | - | Database identifier |
| description | Yes | - | What data it contains (for auto-selection) |
| host | Yes | - | Hostname |
| port | No | 5432 | Port |
| database | Yes | - | Database name |
| user | Yes | - | Username |
| password | Yes | - | Password |
| sslmode | No | prefer | disable, allow, prefer, require, verify-ca, verify-full |
Safety Features
- Read-only sessions: PostgreSQL
readonly=Truemode blocks writes at database level - Query validation: Only SELECT, SHOW, EXPLAIN, WITH allowed
- Single statement: No multi-statement queries (prevents
SELECT 1; DROP TABLE) - Timeouts: 30s query timeout, 10s connection timeout
- Memory cap: Max 10,000 rows per query
- Credential protection: Passwords sanitized from error messages
Requirements
pip install psycopg2-binary
Skill manifest
PostgreSQL Read-Only Query Skill
When to Use
- Use when querying PostgreSQL databases and access must stay strictly read-only
- Use when exploring schemas, tables, and data across multiple configured connections
- Use when you want defense-in-depth protection against accidental INSERT/UPDATE/DELETE or DDL
Execute safe, read-only queries against configured PostgreSQL databases.
Requirements
- Python 3.8+
- psycopg2-binary:
pip install -r requirements.txt
Setup
Create connections.json in the skill directory or ~/.config/claude/postgres-connections.json.
Security: Set file permissions to 600 since it contains credentials:
chmod 600 connections.json
{
"databases": [
{
"name": "production",
"description": "Main app database - users, orders, transactions",
"host": "db.example.com",
"port": 5432,
"database": "app_prod",
"user": "readonly_user",
"password": "your-password",
"sslmode": "require"
}
]
}
Config Fields
| Field | Required | Description |
|---|---|---|
| name | Yes | Identifier for the database (case-insensitive) |
| description | Yes | What data this database contains (used for auto-selection) |
| host | Yes | Database hostname |
| port | No | Port number (default: 5432) |
| database | Yes | Database name |
| user | Yes | Username |
| password | Yes | Password |
| sslmode | No | SSL mode: disable, allow, prefer (default), require, verify-ca, verify-full |
Usage
List configured databases
python3 scripts/query.py --list
Query a database
python3 scripts/query.py --db production --query "SELECT * FROM users LIMIT 10"
List tables
python3 scripts/query.py --db production --tables
Show schema
python3 scripts/query.py --db production --schema
Limit results
python3 scripts/query.py --db production --query "SELECT * FROM orders" --limit 100
Database Selection
Match user intent to database description:
| User asks about | Look for description containing |
|---|---|
| users, accounts | users, accounts, customers |
| orders, sales | orders, transactions, sales |
| analytics, metrics | analytics, metrics, reports |
| logs, events | logs, events, audit |
If unclear, run --list and ask user which database.
Safety Features
- Read-only session: Connection uses PostgreSQL
readonly=Truemode (primary protection) - Query validation: Only SELECT, SHOW, EXPLAIN, WITH queries allowed
- Single statement: Multiple statements per query rejected
- SSL support: Configurable SSL mode for encrypted connections
- Query timeout: 30-second statement timeout enforced
- Memory protection: Max 10,000 rows per query to prevent OOM
- Column width cap: 100 char max per column for readable output
- Credential sanitization: Error messages don't leak passwords
Troubleshooting
| Error | Solution |
|---|---|
| Config not found | Create connections.json in skill directory |
| Authentication failed | Check username/password in config |
| Connection timeout | Verify host/port, check firewall/VPN |
| SSL error | Try "sslmode": "disable" for local databases |
| Permission warning | Run chmod 600 connections.json |
Exit Codes
- 0: Success
- 1: Error (config missing, auth failed, invalid query, database error)
Workflow
- Run
--listto show available databases - Match user intent to database description
- Run
--tablesor--schemato explore structure - Execute query with appropriate LIMIT
Limitations
- Read-only protections reduce accidental writes but cannot override database-server policy, triggers, extensions, or an over-privileged account. Use a database role with read-only permissions as the primary control.
- Query results can contain personal, confidential, or regulated data. Confirm the intended database and avoid exporting or sharing results without explicit authorization.
- The script is not a replacement for backups, auditing, access reviews, or production change controls.
Files (agentic-awesome-skills)
-
scripts
-
query.py 9.2 KB
#!/usr/bin/env python3 """ Read-only PostgreSQL query executor. Connects to configured databases and executes SELECT queries only. """ import json import os import re import stat import sys import argparse from pathlib import Path from typing import Optional try: import psycopg2 except ImportError: print("Error: psycopg2 not installed. Run: pip install psycopg2-binary") sys.exit(1) # Constants SCRIPT_DIR = Path(__file__).parent.parent CONFIG_LOCATIONS = [ SCRIPT_DIR / "connections.json", Path.home() / ".config" / "claude" / "postgres-connections.json", ] MAX_ROWS = 10000 MAX_COLUMN_WIDTH = 100 QUERY_TIMEOUT_MS = 30000 CONNECTION_TIMEOUT_SEC = 10 NULL_DISPLAY = "<NULL>" def is_read_only(query: str) -> bool: """Basic client-side check. Primary protection is readonly=True session.""" query_upper = query.upper().strip() safe_starts = ('SELECT', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'WITH', '\\D') return any(query_upper.startswith(cmd) for cmd in safe_starts) def validate_single_statement(query: str) -> bool: """Check query contains only one statement.""" # Remove trailing semicolon and whitespace, then check for remaining semicolons clean = query.rstrip().rstrip(';') return ';' not in clean def validate_config_permissions(path: Path) -> None: """Warn if config file has insecure permissions (Unix only).""" if os.name != 'nt': # Skip on Windows mode = path.stat().st_mode if bool(mode & stat.S_IRWXG) or bool(mode & stat.S_IRWXO): print(f"WARNING: {path} has insecure permissions!") print(f"Config contains credentials. Run: chmod 600 {path}") def validate_db_config(db: dict) -> None: """Validate required fields exist in database config.""" required = ['name', 'host', 'database', 'user', 'password'] missing = [f for f in required if f not in db] if missing: print(f"Error: Database config missing fields: {', '.join(missing)}") sys.exit(1) def find_config() -> Optional[Path]: """Find config file in supported locations.""" for path in CONFIG_LOCATIONS: if path.exists(): return path return None def load_config(config_path: Optional[Path] = None) -> dict: """Load database connections from JSON config.""" path = config_path or find_config() if not path: print("Config not found. Searched:") for loc in CONFIG_LOCATIONS: print(f" - {loc}") print("\nCreate connections.json with format:") print(json.dumps({ "databases": [{ "name": "mydb", "description": "Description of database contents", "host": "localhost", "port": 5432, "database": "mydb", "user": "user", "password": "password", "sslmode": "prefer" }] }, indent=2)) sys.exit(1) validate_config_permissions(path) with open(path) as f: return json.load(f) def list_databases(config: dict) -> None: """List all configured databases.""" print("Configured databases:\n") for db in config.get("databases", []): validate_db_config(db) print(f" [{db['name']}]") print(f" Host: {db['host']}:{db.get('port', 5432)}") print(f" Database: {db['database']}") print(f" Description: {db.get('description', 'No description')}") print() def execute_query(db_config: dict, query: str, limit: Optional[int] = None) -> None: """Execute a read-only query against the specified database.""" if not is_read_only(query): print("Error: Only read-only queries (SELECT, SHOW, EXPLAIN) are allowed.") sys.exit(1) if not validate_single_statement(query): print("Error: Multiple statements not allowed. Execute queries separately.") sys.exit(1) # Apply limit using regex to avoid false positives from string content if limit and not re.search(r'\bLIMIT\s+\d+', query, re.IGNORECASE): query = f"{query.rstrip(';')} LIMIT {limit}" conn = None try: conn = psycopg2.connect( host=db_config['host'], port=db_config.get('port', 5432), database=db_config['database'], user=db_config['user'], password=db_config['password'], sslmode=db_config.get('sslmode', 'prefer'), connect_timeout=CONNECTION_TIMEOUT_SEC, options=f'-c statement_timeout={QUERY_TIMEOUT_MS}' ) # Primary safety: readonly session prevents any write operations conn.set_session(readonly=True, autocommit=True) with conn.cursor() as cur: cur.execute(query) if cur.description: columns = [desc[0] for desc in cur.description] rows = cur.fetchmany(MAX_ROWS) truncated = len(rows) == MAX_ROWS # Calculate column widths with cap widths = [min(len(col), MAX_COLUMN_WIDTH) for col in columns] for row in rows: for i, val in enumerate(row): val_str = str(val) if val is not None else NULL_DISPLAY widths[i] = min(max(widths[i], len(val_str)), MAX_COLUMN_WIDTH) # Print header header = " | ".join(col[:MAX_COLUMN_WIDTH].ljust(widths[i]) for i, col in enumerate(columns)) print(header) print("-" * len(header)) # Print rows for row in rows: cells = [] for i, val in enumerate(row): val_str = str(val) if val is not None else NULL_DISPLAY if len(val_str) > MAX_COLUMN_WIDTH: val_str = val_str[:MAX_COLUMN_WIDTH-3] + "..." cells.append(val_str.ljust(widths[i])) print(" | ".join(cells)) msg = f"\n({len(rows)} rows)" if truncated: msg += f" [truncated at {MAX_ROWS}]" print(msg) else: print("Query executed (no result set returned)") except psycopg2.Error as e: error_msg = str(e) # Sanitize to avoid leaking credentials if 'password' in error_msg.lower() or 'authentication' in error_msg.lower(): error_msg = "Authentication failed. Check credentials in connections.json" print(f"Database error: {error_msg}") sys.exit(1) finally: if conn: conn.close() def find_database(config: dict, name: str) -> dict: """Find database config by name (case-insensitive).""" for db in config.get("databases", []): if db.get('name', '').lower() == name.lower(): validate_db_config(db) return db available = [db.get('name', 'unnamed') for db in config.get("databases", [])] print(f"Database '{name}' not found.") print(f"Available: {', '.join(available)}") sys.exit(1) def main() -> None: """Main entry point.""" parser = argparse.ArgumentParser( description="Execute read-only PostgreSQL queries", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s --list %(prog)s --db mydb --tables %(prog)s --db mydb --query "SELECT * FROM users" --limit 100 """ ) parser.add_argument("--config", "-c", type=Path, help="Path to config JSON") parser.add_argument("--db", "-d", help="Database name to query") parser.add_argument("--query", "-q", help="SQL query to execute") parser.add_argument("--limit", "-l", type=int, help="Limit rows returned") parser.add_argument("--list", action="store_true", help="List configured databases") parser.add_argument("--schema", "-s", action="store_true", help="Show database schema") parser.add_argument("--tables", "-t", action="store_true", help="List tables") args = parser.parse_args() config = load_config(args.config) if args.list: list_databases(config) return if not args.db: print("Error: --db required. Use --list to see available databases.") sys.exit(1) db_config = find_database(config, args.db) if args.tables: query = """ SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY table_schema, table_name """ execute_query(db_config, query, args.limit) elif args.schema: query = """ SELECT c.table_schema, c.table_name, c.column_name, c.data_type, c.is_nullable FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name AND c.table_schema = t.table_schema WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY c.table_schema, c.table_name, c.ordinal_position """ execute_query(db_config, query, args.limit) elif args.query: execute_query(db_config, args.query, args.limit) else: print("Error: --query, --tables, or --schema required") sys.exit(1) if __name__ == "__main__": main()
-
-
connections.example.json 931 B
{ "databases": [ { "name": "production", "description": "Main production database - users, orders, transactions, accounts", "host": "prod-db.example.com", "port": 5432, "database": "app_prod", "user": "readonly_user", "password": "your-password-here", "sslmode": "require" }, { "name": "analytics", "description": "Analytics warehouse - aggregated metrics, reports, historical data", "host": "analytics-db.example.com", "port": 5432, "database": "analytics", "user": "analyst", "password": "your-password-here", "sslmode": "require" }, { "name": "staging", "description": "Staging environment - mirrors production for testing", "host": "localhost", "port": 5432, "database": "app_staging", "user": "dev", "password": "dev-password", "sslmode": "prefer" } ] } -
README.md 1.8 KB
# postgres Read-only PostgreSQL query skill. Query multiple databases safely with write protection. ## Setup 1. Copy the example config: ```bash cp connections.example.json connections.json ``` 2. Add your database credentials: ```json { "databases": [ { "name": "prod", "description": "Production - users, orders, transactions", "host": "db.example.com", "port": 5432, "database": "app_prod", "user": "readonly", "password": "secret", "sslmode": "require" } ] } ``` 3. Secure the config: ```bash chmod 600 connections.json ``` ## Usage ```bash # List configured databases python3 scripts/query.py --list # List tables python3 scripts/query.py --db prod --tables # Show schema python3 scripts/query.py --db prod --schema # Run query python3 scripts/query.py --db prod --query "SELECT * FROM users" --limit 100 ``` ## Config Fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | name | Yes | - | Database identifier | | description | Yes | - | What data it contains (for auto-selection) | | host | Yes | - | Hostname | | port | No | 5432 | Port | | database | Yes | - | Database name | | user | Yes | - | Username | | password | Yes | - | Password | | sslmode | No | prefer | disable, allow, prefer, require, verify-ca, verify-full | ## Safety Features - **Read-only sessions**: PostgreSQL `readonly=True` mode blocks writes at database level - **Query validation**: Only SELECT, SHOW, EXPLAIN, WITH allowed - **Single statement**: No multi-statement queries (prevents `SELECT 1; DROP TABLE`) - **Timeouts**: 30s query timeout, 10s connection timeout - **Memory cap**: Max 10,000 rows per query - **Credential protection**: Passwords sanitized from error messages ## Requirements ```bash pip install psycopg2-binary ``` -
requirements.txt 26 B
psycopg2-binary>=2.9,<3.0 -
SKILL.md 4.6 KB
--- name: postgres-readonly-queries description: "Execute safe read-only SQL queries against PostgreSQL databases with multi-connection support and defense-in-depth write protection." category: data risk: safe source: https://github.com/sanjay3290/ai-skills/tree/main/skills/postgres source_repo: sanjay3290/ai-skills source_type: community date_added: "2026-07-09" author: sanjay3290 tags: [postgres, sql, database, read-only] tools: [claude, cursor, gemini] license: "Apache-2.0" license_source: "https://github.com/sanjay3290/ai-skills/blob/main/LICENSE" --- # PostgreSQL Read-Only Query Skill ## When to Use - Use when querying PostgreSQL databases and access must stay strictly read-only - Use when exploring schemas, tables, and data across multiple configured connections - Use when you want defense-in-depth protection against accidental INSERT/UPDATE/DELETE or DDL Execute safe, read-only queries against configured PostgreSQL databases. ## Requirements - Python 3.8+ - psycopg2-binary: `pip install -r requirements.txt` ## Setup Create `connections.json` in the skill directory or `~/.config/claude/postgres-connections.json`. **Security**: Set file permissions to `600` since it contains credentials: ```bash chmod 600 connections.json ``` ```json { "databases": [ { "name": "production", "description": "Main app database - users, orders, transactions", "host": "db.example.com", "port": 5432, "database": "app_prod", "user": "readonly_user", "password": "your-password", "sslmode": "require" } ] } ``` ### Config Fields | Field | Required | Description | |-------|----------|-------------| | name | Yes | Identifier for the database (case-insensitive) | | description | Yes | What data this database contains (used for auto-selection) | | host | Yes | Database hostname | | port | No | Port number (default: 5432) | | database | Yes | Database name | | user | Yes | Username | | password | Yes | Password | | sslmode | No | SSL mode: disable, allow, prefer (default), require, verify-ca, verify-full | ## Usage ### List configured databases ```bash python3 scripts/query.py --list ``` ### Query a database ```bash python3 scripts/query.py --db production --query "SELECT * FROM users LIMIT 10" ``` ### List tables ```bash python3 scripts/query.py --db production --tables ``` ### Show schema ```bash python3 scripts/query.py --db production --schema ``` ### Limit results ```bash python3 scripts/query.py --db production --query "SELECT * FROM orders" --limit 100 ``` ## Database Selection Match user intent to database `description`: | User asks about | Look for description containing | |-----------------|--------------------------------| | users, accounts | users, accounts, customers | | orders, sales | orders, transactions, sales | | analytics, metrics | analytics, metrics, reports | | logs, events | logs, events, audit | If unclear, run `--list` and ask user which database. ## Safety Features - **Read-only session**: Connection uses PostgreSQL `readonly=True` mode (primary protection) - **Query validation**: Only SELECT, SHOW, EXPLAIN, WITH queries allowed - **Single statement**: Multiple statements per query rejected - **SSL support**: Configurable SSL mode for encrypted connections - **Query timeout**: 30-second statement timeout enforced - **Memory protection**: Max 10,000 rows per query to prevent OOM - **Column width cap**: 100 char max per column for readable output - **Credential sanitization**: Error messages don't leak passwords ## Troubleshooting | Error | Solution | |-------|----------| | Config not found | Create `connections.json` in skill directory | | Authentication failed | Check username/password in config | | Connection timeout | Verify host/port, check firewall/VPN | | SSL error | Try `"sslmode": "disable"` for local databases | | Permission warning | Run `chmod 600 connections.json` | ## Exit Codes - **0**: Success - **1**: Error (config missing, auth failed, invalid query, database error) ## Workflow 1. Run `--list` to show available databases 2. Match user intent to database description 3. Run `--tables` or `--schema` to explore structure 4. Execute query with appropriate LIMIT ## Limitations - Read-only protections reduce accidental writes but cannot override database-server policy, triggers, extensions, or an over-privileged account. Use a database role with read-only permissions as the primary control. - Query results can contain personal, confidential, or regulated data. Confirm the intended database and avoid exporting or sharing results without explicit authorization. - The script is not a replacement for backups, auditing, access reviews, or production change controls.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.