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 Structured Query Language (SQL). I wanted to turn 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 command-line interface (CLI).
First principles
A read-only analyst has three parts: a model that reads a question and writes text, executors that turn some of that text into a database query or a Hypertext Transfer Protocol (HTTP) request, and the systems at the other end, which decide what each request may do. Reason from those parts and most of the design follows.
- The model only produces text. It cannot reach a database or an application programming interface (API) on its own. Anything it changes, it changes through an executor.
- Text can be steered. A model can misread a request, ignore an instruction, or follow one hidden in the data it reads back. A rule that lives only in the prompt is a hope, not a guarantee.
- The far end has the final say. A Postgres role with only
SELECTgrants cannot write, whatever query arrives. A read-only API token cannot change anything, whatever path is requested.
So the guarantees belong in the database role, the executors and the allowlists. The model stays a planner that can only ask, and every check in between is a backstop.
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. You connect Querion as a Postgres user with only read grants, so the database itself refuses any write.
- SQL validation permits a single
SELECTorWITHstatement, with a denylist that also catches data-modifying common table expressions (CTEs). - A read-only session plus a server-side statement timeout, so a heavy query cannot hold a connection hostage.
- HTTP requests are GET only, and each source can have an allowlist of the paths that are safe to read.
- A write-request firewall checks the question before anything runs. A deterministic check refuses pasted write statements before any model call. For plain-English requests such as “cancel order 14377”, the model is instructed to refuse and run nothing.
Any one of these can fail and the data is still safe. That is the point. The plain-English half of the firewall depends on the model following its instructions, which is exactly why it sits on top of the other layers instead of replacing them.
A GET is not automatically safe
The allowlist deserves its own section, because it is the lesson I did not expect to relearn. It is tempting to assume that any HTTP GET is free of side effects. Real APIs disagree. Some expose state-changing operations over GET, and a model that may call “any GET on this host” can stumble into one. So Querion lets you make the safe paths explicit: with safe_get set, any path that does not match is refused.
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
A source without safe_get allows any GET path, so set it on every source whose GET endpoints are not all read-only.
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, which a runner validates, executes and feeds back. The loop repeats until the model returns a final answer with no execute fence.
question
-> write-request firewall (refuses "change my data" requests)
-> 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, so the model runs under your existing plan, and Opus is available for the multi-step reasoning the analyst needs. The setup it is tuned for is deliberately boring: a small Amazon Web Services (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 can fold in an optional semantic layer maintained by its companion project, Trove, so the answers use 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 the MIT license. The code is at github.com/anishfyi/querion, and the documentation, with a quickstart walkthrough, is at velofy.co/querion.