Most teams have the data but not the analyst time. Dashboards answer the questions you anticipated; everything else becomes a ticket that waits for someone to write the SQL. I wanted to collapse that long tail of "can someone just pull..." into a chat message, without ever giving a language model permission to touch production data. That is Querion : a strictly read-only, natural-language data analyst that plugs into any platform and runs on the Claude Code CLI.
The hard constraint first
The interesting engineering in Querion is not the language model. It is the promise that it never writes anything, anywhere, even when a user asks it to. You should be able to point it at a production database and sleep. So the design started from that constraint and worked outward, with defense in depth rather than a single check.
- A read-only database role is the strongest guarantee. The application connects as a Postgres user that simply cannot mutate anything.
-
SQL validation permits a single
SELECTorWITHstatement, with a denylist that also catches data-modifying CTEs. - A read-only session plus a server-side statement timeout, so a heavy query cannot hold a connection hostage.
- HTTP egress is GET only, behind a positive, per-source allowlist.
- A write-request firewall refuses any natural-language intent to insert, update, delete, cancel, or sync before the request reaches an executor at all.
Any one of these can fail and the data is still safe. That is the point.
A GET is not automatically safe
The allowlist deserves its own paragraph, because it is the lesson I did not expect to relearn. It is tempting to assume that any HTTP GET is side-effect free. Real APIs disagree. Some expose state-changing operations over GET, and a model that is allowed to call "any GET on this host" can stumble into one. So Querion makes the safe paths explicit and denies everything else by default.
sources:
- name: stripe
base_url: https://api.stripe.com
auth_header: "Authorization: Bearer \${STRIPE_KEY}"
safe_get: # only these read paths are allowed
- ^/v1/charges
- ^/v1/customers
- ^/v1/invoices
docs: docs/sources/stripe.md
Adding a platform is configuration, not code: a secret in the environment, a markdown file describing the read endpoints the way you would brief a new analyst, and a source block like the one above.
The brain never executes anything
Querion is a small read-only loop around the Claude Code CLI. The model is the planner, never the hands. It can only emit fenced, read-only directives that a runner validates and executes, then feeds the results back. The loop repeats until the model returns a final answer with no execute fence.
question
-> write-request firewall (refuses any "change my data" request)
-> the brain emits read-only directives:
run-sql a single SELECT / WITH against Postgres
source:NAME a GET against a configured HTTP API
chart a chart spec from data already fetched
-> the runner executes them read-only and feeds results back
-> repeat until the brain returns a final answer
Each turn is stateless: the full transcript is passed back in, so nothing leaks across users or requests. The separation also means the safety checks live in plain Python that I can read and test, not in a prompt I am hoping the model obeys.
No API key, by design
Querion has no API key to manage or rotate. It shells out to the Claude Code CLI you have already authenticated once on the host, which means the model runs under your existing plan and Opus is available for the multi-step reasoning the analyst actually needs. The setup it is tuned for is deliberately boring: a small AWS instance, the Claude Code CLI logged in once, and Opus as the model.
Every answer shows its work
An analyst you cannot audit is a liability. Every number Querion returns comes with the exact query that produced it, and a chart when one helps. You can trust it, reuse the SQL, or paste it into a dashboard. It can also join history in Postgres with live truth from an API in a single answer, choosing the right source per question.
It gets smarter with a semantic layer
A schema tells you the columns. It does not tell you what "active customer" means or which table is the source of truth. Querion folds in an optional semantic layer maintained by its companion project, Trove , so the answers speak your business language and stay current on their own.
trove:
enabled: true
layer_path: .trove/semantic.md
What I took away from building it
The model was the easy part. The work was the contract around it: making "it never writes" true by construction, keeping the executor dumb and the planner sandboxed, and treating every external API as hostile until an allowlist says otherwise. Querion is open source under MIT. The code is at github.com/anishfyi/querion and there is a live walkthrough at anishfyi.github.io/querion .