
When people talk about running an AI agent's code safely, they usually mean a sandbox. Daytona, e2b, and Fly's Sprites each give the agent a microVM or container that isolates code you have no reason to trust, so a script it generated cannot reach anything it shouldn't. That is containment, the right tool for running untrusted code, but also the half people reach for first and treat as the whole of the problem.
For agent-written backend code, a sandbox leaves a second question unanswered. Even when the code runs somewhere safe, how do you know it is correct before it ships? Backend code an agent writes usually compiles and passes a couple of tests and is still wrong in ways that only show up against real infrastructure, like a query that works on an empty table and falls apart on a full one. An agent works in a loop, changing the code and re-running whatever feedback it can get until nothing complains, so what it ships is whatever that feedback missed. Making it safe to ship is a matter of putting better feedback in that loop and giving the code a real place to run before production. The examples here are in Encore, since that is what we build, though the loop is not specific to it.
The loop has a few places to catch a problem before it reaches a user, and the rest of this post walks through each one.
A change an agent writes passes through several checks before production: types at compile time, a preview environment running real dependencies, the agent's own checks over MCP, and a human review of the result.
The cheapest feedback fires before the code runs at all, and you get more of it by narrowing what the agent can express. When an endpoint's request and response are ordinary types, returning the wrong shape is a compile error on the exact line, caught while the agent is editing rather than as a 400 some caller hits later. The same holds across services, where a call that passes a field the receiving service does not declare fails to compile.
In Encore that surface is the API definition and the infrastructure a service declares. A typed endpoint backed by a database looks like this:
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("todo", { migrations: "./migrations" });
interface Todo { id: number; title: string; done: boolean; }
export const get = api(
{ expose: true, method: "GET", path: "/todo/:id" },
async ({ id }: { id: number }): Promise<Todo> => {
const row = await db.queryRow<Todo>`
SELECT id, title, done FROM todo_item WHERE id = ${id}
`;
if (!row) throw new Error("todo not found");
return row;
},
);
Those request and response types are the contract, so an agent that returns the wrong shape from this handler sees a type error while it is still editing, with the line and the expected type, and fixes it in the same loop where the mistake was made instead of leaving it for staging where it becomes someone else's to debug days later. Infrastructure is declared the same way, in code, which takes a whole category of error off the table, because the agent never hand-writes an IAM policy or a connection string and so cannot get them wrong. The parser that reads those declarations also fails the build when they do not hold together, so a service pointed at a database with no migration directory stops with "The migration directory does not exist" before it deploys.
Types rule out the mistakes that are about shape, but the rest only appear when the code runs against the systems it talks to, where a query can be valid SQL and still return the wrong rows, or a migration can apply cleanly in review and then lock a table under real load. Catching those means running against a real database and a real queue rather than mocks. The gap shows up with something like an upsert:
INSERT INTO todo_item (id, title, done)
VALUES ($1, $2, false)
ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title
RETURNING id, title, done;
Against real Postgres this leans on a unique constraint and on EXCLUDED to update the row on conflict. A SQLite stub or an in-memory fake handles ON CONFLICT ... DO UPDATE differently, or not at all, so the agent's code passes its test and is still wrong once it reaches production. Encore runs real Postgres locally through Docker, so the schema the agent builds against and the query it runs are the ones production will use, after the same migrations.
That run cannot happen in production, so each change needs somewhere real but disposable to go. Encore brings up a preview environment for every pull request, with its own database and infrastructure, when the PR opens, and tears it down when the PR is merged or closed. It can seed that database from a copy of your main one so the data is realistic, and the agent's change runs there end to end before anyone thinks about promoting it. The change's tests run in that environment too, against the real database instead of a mock.
A sandbox contains code you cannot trust, while a preview environment runs code you expect to work and confirms that it does, including against a copy of production data. A system that needs both would run them at different layers, and Encore is the verification side, not a sandbox for untrusted code.
Running the code only helps if something checks the result, and the agent can do a lot of that itself once it can see the running system instead of guessing at it. Encore exposes the running app to an agent through an MCP server.
The schema is the most useful piece, since an agent adding a field to a response can read the real tables and columns and write a correct query the first time instead of guessing at column names, and because Encore runs real Postgres locally, the schema it reads is the one the code runs against after the latest migration. It can also call its own endpoints and read the traces that come back, seeing the queries that ran and the response that came out rather than guessing.
All of this replaces the agent's assumptions with things it can look up, so what it verifies is how the system behaves rather than what the model expected.
A person still reviews each change, but the review is of the result rather than the code. A change that ran in its own environment against real dependencies and passed its checks comes with evidence behind it, so the reviewer sees what it provisioned and how it behaved instead of reading line by line through whatever the agent generated. A change that failed somewhere never reaches review, and since the environment was disposable, the failed attempt costs nothing to drop.
One thing the loop does not catch is whether the business logic is right. The type system says nothing about that, and the agent can always reach for an escape hatch, an as any or a @ts-ignore that quiets a check it could not satisfy. What keeps that in check is that a suppression is a visible line in the diff, so it shows up in review and you can lint it to zero in CI.
None of this rests on the agent being more careful or the model being smarter. The code earns its way to production by running somewhere real and surviving real checks, the same bar you would hold code a person wrote to. As more of the backend is written by agents, the bottleneck moves from writing the code to making sure it is safe to run, and a loop with real feedback and a real place to run is what keeps that from slowing everything down.
Every Encore app gets a preview environment per pull request and an MCP server an agent can read, so the loop is there once you build on it. The quick start is the way in, and the docs cover how Encore provisions infrastructure from your code.


