anishfyi.com Projects All writings

June 14, 202610 min readOpen Source, LLM, Claude Code, Postgres

Building Querion: a Read-Only, Natural-Language Data Analyst

How I gave a language model production database access without ever letting it write: a read-only contract, a sandboxed directive loop, and a default-deny API allowlist.

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.

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 .

Working on something in this territory? Book thirty minutes.