Give the Agent a Shell: Why psql + pandas Beat Piping Data Through the LLM
Why we replaced SQL-over-MCP with a sandboxed shell with psql, pandas, and files — and kept the model's context for judgment instead of data transport.
We run a fleet of commerce agents over a Postgres warehouse — orders, ads, refunds, creators across four platforms. Our first architecture looked like everyone's first architecture: MCP tools that run SQL for the model and return rows as JSON into its context. execute_ecom_sql(sql) → rows. It worked, and it was quietly wrong in ways that took months to surface.
The failure modes of data-through-context
Silent truncation. Tool responses have size caps. A review agent asked "enumerate everything you must review" via SQL and got the first N rows of the answer, trimmed without an error. Entities past the cut were never seen and never reviewed — invisible incompleteness, discovered only by auditing. We built a dedicated paging tool ("complete-or-honest") to compensate. That tool was treating the symptom: the response cap only exists because results were being routed through the model's context at all.
Context as a data bus. Rows in context are paid for three times: once to produce, once on every subsequent turn they ride along, once in degraded attention. An agent comparing 5,000 order rows against a rate card doesn't need to read 5,000 rows — it needs to reason about the twelve that mismatch. The LLM is a judgment engine being misused as a transport layer.
Guardrails duplicated in the wrong place. Our SQL tools enforced read-only, timeouts, row caps. All three belong to the database. A Postgres role with default_transaction_read_only=on and statement_timeout=60s is a stronger guarantee than any wrapper, and it can't drift out of sync with a tool implementation.
The replacement: a sandbox with psql, python, and files
Each agent session now runs in an isolated VM with a shell: psql on a read-only DSN, python3 + pandas, and a /work directory. The rule we teach is one sentence: the shell is the workspace; the conversation is for judgment.
psql "$DATABASE_URL" -c "\copy (SELECT ...) TO '/work/q.csv' CSV HEADER"
python3 -c "import pandas as pd; df = pd.read_csv('/work/q.csv'); ..."The determinism this buys is the point:
- Completeness by construction. A file on disk has no response cap. Our review worklists are now frozen server-side into a table, and the agent's first act is
\copy-ing its complete obligation to/work/worklist.csv. It iterates with pandas, dispositions each row through write tools, and a post-hoc lint verifies row count vs dispositions. "Complete-or-honest" became "complete-or-caught" — auditing what the agent did, not what it was shown. - Exact computation. Aggregation, joins, percentile math happen in Postgres and pandas, which do not hallucinate arithmetic. The model composes the query and interprets the result; it never sums a column.
- Cheap iteration. A wrong first query costs one shell round-trip, not a context-window's worth of re-piped rows.
The security model got simpler, not scarier: the DSN is a role that can only read, the sandbox egress is allow-listed, and writes go through a handful of deliberately guarded tools. We deleted two SQL MCP servers.
Where the knowledge goes instead
The tools' hidden value was their descriptions — schema guidance the model saw every session. We split that knowledge by scale:
- Cross-cutting rules (maybe 40 lines: which timestamp is business time, the join key that prevents double-counting, which revenue reads are gross vs net) live in a skill injected into every agent's prompt.
- Per-column semantics live in the database itself as
COMMENT ON, surfaced just-in-time by the agent's own\d+ tablehabit — unbounded documentation at zero prompt cost until the moment it's needed. When a transcript shows an agent guessing wrong about a column, the fix is a one-line comment migration, not prompt growth.
The feedback loop is concrete. First transcript after the switch: agent filters on created_at (sync-load time), gets zero rows, runs \d+, finds placed_at, self-corrects — right answer, three wasted round-trips. We added one comment. Next transcript: right column, but cancelled orders counted. One more comment on the status column. Each miss becomes a permanent, centrally-versioned fix that every future session reads at exactly the point of use.
Results
The clearest test is one question asked of both architectures: how many TikTok orders were placed in June? The old tool pipe returned a headline number that quietly counted $0 creator samples, and someone had to catch it. The shell agent runs the same deterministic query we would run by hand — samples split out, cancellations excluded — and its answer reconciles exactly with our own verification. Review runs enumerate their full obligation instead of a truncated prefix. And the model's context carries analysis, not payload — which is what it was good at all along.
The pattern generalizes: wherever an agent architecture routes bulk data through the model to get it to a computation, put the computation next to the data, give the model a shell, and reserve its context for the only thing it uniquely does — judgment.