The engine never opens your warehouse
Why datahashi stopped holding warehouse passwords — compile and execute are different jobs, and agents make the wrong trust boundary expensive.
The datahashi team4 min read

The default shape of a semantic layer is simple: you hand it a warehouse connection, it compiles metric names into SQL, and it runs that SQL itself.
That is also the wrong trust boundary. We stopped doing it.
Compiling SQL is not the same as running it
datahashi’s job is meaning. You declare entities, measures, and metrics once. Every caller — a dashboard, an app, an MCP agent — asks by name. The engine resolves the joins, applies grants and row-level filters, and writes fan-out-safe SQL. Callers never invent a join. That part has not changed.
Running the SQL is a different job. It requires the warehouse password, a network path to the warehouse, a kill switch, and an audit log of what actually executed. If the same process that compiles also holds those, then:
- We become the credential owner for every warehouse we query.
- Your warehouse has to accept connections from our network.
- The only way to stop us executing is to rotate that credential, or to trust us to stop.
That was acceptable when the callers were a handful of dashboards. It is not acceptable as the product we are building.
Agents make the blast radius worse
The reason datahashi exists is agents. An LLM that writes SQL will invent a join, return a confidently-wrong number, and nobody notices until finance and product are already arguing. The fix is a catalog of named metrics and a compiler that is the only path to a row.
That fix multiplies callers. Agents are easy to stand up, easy to copy into a
config file, and always on. If the engine holds the warehouse password, every
one of those callers is a reason that password is sitting in a hosted process.
A stolen agent token should mean “this principal may ask for revenue.” It
should not mean “this vendor still holds the keys to production.”
MCP auth already keeps warehouse passwords out of the agent. The remaining mistake was keeping them in the engine.
What we changed
Every query now reaches a warehouse through a Query Runner — a small process that executes the SQL the engine compiled and returns rows. The engine never opens a warehouse.
Semantic Query → engine (compile, grants, RLS)
→ Query Runner (DSN, kill switch, audit)
→ warehouse
The runner is a proxy for execution, not a second semantic layer. It does not compile, does not decide catalog access, and does not speak MCP. It sees parameterized SQL and a runner key. It does not see the principal, the catalog, or the Semantic Query.
It owns four things the engine must not:
- The warehouse password.
- The kill switch — pause it and we stop executing, with no credential rotation.
- A physical allow/deny on tables, fields, and values, per key. That
enforcement is PostgreSQL only. Other engines need
allow_unpoliced_backends: true, or every request is denied. - The audit log, on disk you control.
The warehouse does not have to accept connections from our network. You install the runner where it can already reach the warehouse (nine engines, including Snowflake, BigQuery, Postgres, and MySQL).
Two operators, one wire
You can run the Query Runner yourself. That is the zero-custody path: we store a runner URL and a sealed runner key, never the warehouse password.
You can also ask us to operate a runner. That path is the old convenience — give us host, port, and password — moved onto a runner we operate, not restored as the engine dialing your warehouse. The password still never lives in the query engine. The wire is the same either way.
The headless binary (hashi-engine) and the community binary (hashi, when
ENGINE_MODEL_DIR points it at a folder) use this split too. Each reads one
model from a folder and has no control plane. A first try can start the runner
as a loopback child in the same container. That is a boot mode so
docker compose up is one app container, not a second SQL path. Isolate it
when you want the password off that machine.
What we did not give up
Governance still happens before SQL is sent. Row-level filters are bound into the statement on the engine. A role that does not grant raw measures still cannot ask for them. Agents still pick names from the catalog. Metering still records what each query scanned.
The runner cannot widen any of that. It can only refuse to run SQL, or run
less. Pause, policy deny, and a stolen runner key that is still only allowed
SELECT / WITH are all fail-closed.
How to think about it
If a definition should require a review and a published version, it belongs in the model. If a query should be allowed for this principal, it belongs in grants and row filters. If a password, a kill switch, or an audit log of executed SQL should stay on your side of the network, it belongs in the Query Runner.
Thick warehouses hold data. Thin semantic layers hold meaning. The process that opens the warehouse should sit with the warehouse.
That is why the engine never opens yours.
Product notes
An occasional email when we ship something or write something worth reading.