querying-local-postgres — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited querying-local-postgres (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
User's query: $ARGUMENTS
Scope: This repo uses PostgreSQL for app metadata (teams, projects, flags, Django models, etc.). Analytics event data lives in ClickHouse, not Postgres — use HogQL / ClickHouse tools for events-style questions unless the user explicitly wants Postgres.
EXPLAIN / EXPLAIN (ANALYZE, …) on read-only `SELECT` against Django or app tablesPGOPTIONS='-c default_transaction_read_only=on' to force a read-only connection).Do not run, suggest, or generate any of the following. Refuse and state that this skill is read-only.
INSERT, UPDATE, DELETE, MERGE, TRUNCATECREATE, DROP, ALTER, RENAMECOPY ... TO program, CALL (if it mutates), GRANT/REVOKEWITH … SELECT). Do not wrap DML in EXPLAIN ANALYZE — it would execute the write. The read-only connection below rejects writes, but the agent must not attempt this pattern.Allowed:
SELECT (including WITH … SELECT)EXPLAIN … SELECT (estimate-only plan; no execution)EXPLAIN (ANALYZE, …) SELECT — executes the SELECT once; use only for performance analysis. Must run on the read-only connection below.SHOW, SELECT from catalog views (pg_stat_*, information_schema, etc.) when read-onlyIf the user requests a write operation, say: "This skill is read-only. I can't run INSERT/UPDATE/DELETE or other mutations. Use a DB client or migration tool for writes."
| Goal | What to use |
|---|---|
| Plan shape, estimated costs, no execution | EXPLAIN (FORMAT TEXT, COSTS) or add VERBOSE |
| Actual timings, row counts, buffer hits | EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) on the `SELECT` |
| Buffer + WAL stats | BUFFERS requires `ANALYZE`; WAL requires `ANALYZE` (PostgreSQL 13+) |
Safe pattern: the analyzed statement must be only a SELECT (or WITH … SELECT), run on the read-only connection (see Usage below). Example:
PGOPTIONS='-c default_transaction_read_only=on' psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT TEXT)
SELECT … LIMIT 100;"Optional flags (when useful): SETTINGS (show non-default GUCs), WAL (with ANALYZE), TIMING (default on in recent versions for ANALYZE).
Caveats:
SELECT (e.g. realistic WHERE, LIMIT matching production shape) when exploring.DATABASE_URLUse this hardcoded URL for day-to-day local queries (matches typical Docker Compose + port 5432 on localhost, SSL off):
| Setting | Value |
|---|---|
| Host | localhost |
| Port | 5432 |
| User | posthog |
| Password | posthog |
| Database | posthog |
| SSL | off |
# Prefer this unless the user says their local password/db differs
LOCAL_POSTGRES_URL='postgres://posthog:posthog@localhost:5432/posthog'Equivalent: postgresql://posthog:posthog@localhost:5432/posthog
Other local DBs on the same server: swap the path only, e.g. ...5432/posthog_persons.
Configuration source of truth (app): posthog/settings/data_stores.py (Django DATABASES, optional replica POSTHOG_POSTGRES_READ_HOST, direct POSTHOG_POSTGRES_DIRECT_HOST, PERSONS_DB_WRITER_URL, product DB routing from products/db_routing.yaml).
When not using the hardcoded URL: Connecting from the host with the same credentials is documented in Developing locally (fe_sendauth troubleshooting). Ensure containers are running.
Default env when `DEBUG` is on: Django builds a default DATABASE_URL from PGHOST (default `db`), PGUSER / PGPASSWORD, PGPORT, PGDATABASE — matching in-container hostnames. From the host, use localhost and the same user/password/database name unless your shell already exports DATABASE_URL.
Multiple PostgreSQL databases (same server in local compose; separate logical DBs):
posthogposthog_persons (PERSONS_DB_WRITER_URL / PERSONS_DB_READER_URL)posthog_<name> per products/db_routing.yaml (created by docker/postgres-init-scripts/create-product-dbs.sh)docker/postgres-init-scripts/ if neededPoint psql at the right database by changing the path in DATABASE_URL (e.g. .../posthog_persons).
Rust / sqlx: Some services use rust/.env for DATABASE_URL when working from posthog/rust — see rust/README.md.
Always force the connection read-only via PGOPTIONS='-c default_transaction_read_only=on' so Postgres rejects writes even if the generated SQL is wrong.
Why `PGOPTIONS`, not `SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY`? Apsql -c "..."string with multiple statements runs as a single implicit transaction.SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLYonly sets the default for _subsequent_ transactions — the in-progress one keeps the read-write mode it was given atBEGIN, so a write in the same-cwould not be rejected.PGOPTIONS='-c default_transaction_read_only=on'sets the GUC at connection startup, so every transaction (including the implicit-cone) starts read-only. The inline equivalent isSET TRANSACTION READ ONLY;as the first statement of the-cstring (it affects the current transaction, unlikeSET SESSION CHARACTERISTICS).
Run from the PostHog repo root so relative env paths resolve.
Default — local hardcoded URL (posthog / posthog @ localhost:5432 / db posthog):
PGOPTIONS='-c default_transaction_read_only=on' psql "postgres://posthog:posthog@localhost:5432/posthog" -v ON_ERROR_STOP=1 -c "SELECT 1;"Option A — `DATABASE_URL` already in the shell (e.g. after flox activate or manual export):
PGOPTIONS='-c default_transaction_read_only=on' psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "SELECT 1;"Option B — load from a gitignored env file at repo root (if DATABASE_URL is set there):
npx dotenv -e .env -- bash -c "PGOPTIONS='-c default_transaction_read_only=on' psql \"\$DATABASE_URL\" -c 'SELECT ...'"-c.LIMIT 100 unless the user specifies otherwise.-x: psql ... -x -c "...".posthog/models/ (and product packages under products/). Table names are usually prefixed with posthog_ and snake-cased (e.g. posthog_team, posthog_user). Confirm with \dt posthog_* in psql, or check the model's Meta.db_table if nonstandard.posthog/migrations/ (and product migration paths) define the authoritative DDL over time.PERSON_TABLE_NAME (see data_stores.py); default posthog_person.deleted fields where applicable.POSTHOG_POSTGRES_READ_HOST).EXPLAIN ANALYZE on SELECT for slow Django queries replicated as SQL — mind loading production-sized data.docs/published/handbook/engineering/developing-locally.mdhogli (see .agents/skills/hogli/SKILL.md)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.