Part of our security series — the full argument lives at Secure AI Code Execution.
Every AI platform on the market claims to have a sandbox. Ask to see it, and you usually get a policy document — a description of intentions, written in the future tense.
Here is mine. It is a file in the repository, and the part that does the work is a branch, a timeout, and a list.
The sandbox is a file
This is /system/workflows/workflows/generate-and-execute-hyperlambda.hl. It is the endpoint where a language model writes Hyperlambda and the cloudlet executes it — the mechanism behind agents that grow their own tools. If anything on this platform deserves paranoid attention, it is this file.
The whole security decision is one branch:
if
auth.ticket.in-role:root
.lambda
// User is root, executing "whatever".
invoke:x:@hyper2lambda
else
/*
* User is not root, restricting execution to valid slots,
* and restricting execution time to 20 seconds.
*/
execution.timeout:20000
add:x:./*/whitelist/*/.lambda
get-nodes:x:@hyper2lambda/*
whitelist
vocabulary
add
and
apply
// ... 175 more ...
validators.url
.lambda
I will come back to that root branch, because it is the least comfortable paragraph in this article and therefore the one worth reading.
For everyone who is not root, the generated code runs inside [whitelist] with a vocabulary of exactly 178 slots, under a twenty-second execution timeout. Not 178 categories, or 178 rules. 178 names, listed one per line, in a file you can read.
What is on the list, and what is not
The list is the security model, so the useful question is what it leaves out. I went through it:
| Absent | Meaning |
|---|---|
eval, invoke, execute, execute-file, signal | No dynamic execution of anything |
whitelist | The sandbox cannot re-enter itself |
every io.file.* | No filesystem, in either direction |
every data.* | No database — not even a read |
system.execute | No shell |
slots.create, config.get, sockets.* | No new slots, no configuration, no broadcast |
http.post, http.put, http.patch | No writes to the outside world |
And a few present entries that look surprising until you think about them. http.get and http.delete are in, so egress is possible and is a policy decision you own. hyper2lambda is in — sandboxed code can parse Hyperlambda into a node tree all day, and with no eval and no invoke, it can never run a line of it. slots.vocabulary is in, which means the code can enumerate its own vocabulary: it is allowed to discover the exact shape of its cage, and still cannot reach through the bars.
That fourth row is the one the title rests on. Because whitelist is not in its own vocabulary, code inside the sandbox cannot open a second sandbox with better terms. Authority in there only ever goes down.
Checked, not filtered
The distinction that matters is when the check happens. Scanning generated text for dangerous-looking strings is filtering, and filtering loses to anyone patient. This is not that. From Eval.cs, the loop that runs every Hyperlambda statement in existence:
var whitelist = signaler.Peek>("whitelist");
foreach (var idx in GetNodes(input))
{
if (whitelist != null && !whitelist.Any(x => /* name and value must match */))
throw new HyperlambdaException($"Slot [{idx.Name}] doesn't exist in current scope");
await signaler.SignalAsync(idx.Name, idx);
}
The check sits between the statement and its dispatch, and there is no second path to dispatch. That is the entire trick — there is no clever route around the gate, because the gate is the road.
It also means nesting cannot smuggle anything. add is on the list, and add is implemented as signaler.Signal("eval", input) — it evaluates its own children. So a payload that hides a forbidden slot beneath an allowed one gets checked on the way down, by the same loop, with the same vocabulary on the stack. There is no depth at which the rules relax.
This is the second of two gates. The first one runs earlier, at generation time, and refuses to emit code containing functions that do not exist — I wrote that one up separately in Zero-Hallucination Code Generation. The full argument for why this belongs in a runtime rather than a prompt is in Why Secure AI Code Execution Requires Runtime Whitelisting.

You can whitelist a slot and its argument
Here is the feature almost nobody knows exists. A vocabulary entry can carry a value, and if it does, the value is part of the match:
if (x.Name == idx.Name)
{
if (x.Value != null && idx.Value != null && x.Get() != idx.GetEx())
return false; // right slot, wrong argument
return true;
}
Write data.connect in a vocabulary and you have granted database access. Write data.connect:crm and you have granted one database, by name, and every other connection string in the world is now a thrown exception. The unit of authority stops being the capability and becomes the specific resource — which is the difference between telling an agent it may query, and telling it what it may query.
If you are pointing agents at real databases, that distinction is most of the story; there is more on that in Database AI Agents.
To be precise about my own file: the production vocabulary above uses no pinned values, because it grants no data.* slots at all. Pinning is what you reach for when you do need to hand something over.
Isolation, and the one way out
Restricting the verbs is only half of it. If the code could still read and write the surrounding tree, a narrow vocabulary would just be a slower way to lose. So [whitelist] declares ClonesLambda = true and runs like this:
var result = new Node();
await signaler.ScopeAsync("slots.result", result, async () =>
{
await signaler.ScopeAsync("whitelist", vocabulary, async () =>
{
await signaler.SignalAsync("eval", lambda.Clone());
});
});
Three things happen there. The body is cloned, so the code is manipulating a copy and the original is beyond its reach. A fresh slots.result is pushed, so return lands in a private mailbox instead of the caller's. And the vocabulary goes on the stack for exactly the duration of that call.
The practical consequence surprises people the first time: inside a whitelist, set-value aimed at an outer node does not change it. Nothing is thrown, nothing is mutated — the reference simply does not reach out of the clone. The only thing that crosses the boundary is what the code deliberately returns. One controlled channel, in one direction. My earlier walkthrough of the semantics is here.
The timeout works the same way. execution.timeout:20000 arms a cancellation token that Eval checks twice per statement, so a runaway loop dies on its next instruction rather than on someone's pager.
The sandbox is the innermost of six
[whitelist] gets the headline, but an agent aiming at a Magic cloudlet hits five other boundaries first, and every one of them is a mechanism rather than a convention:
| Boundary | What it does |
|---|---|
| URL space | Anything not under modules/ or system/ returns 401 before a file is resolved |
| Path resolution | Every path normalises through AbsolutePath, which throws Path traversal attempt detected if it escapes /files/ |
| Interceptors | interceptor.hl applies recursively upward through the folder tree — an endpoint cannot opt out of the auth check its parent folder imposes |
| Argument schema | A declared [.arguments] is closed: an undeclared query parameter is refused outright, and declared ones are coerced to their stated type |
| RBAC | Enforced at execution time, on the ticket, not at generation time |
| The vocabulary | Everything above |
One detail in that RBAC row is worth pulling out, because it is the kind of thing that only shows up after you have run this in production for a while. An unauthenticated caller gets 401; an authenticated caller who simply lacks the role gets 403. They are different failures and they need different codes, because a client that receives 401 will helpfully log the user out — and being logged out for asking a question you were never allowed to ask is a bug, not security.
The interceptor row is the one I would point a sceptic at. Endpoint-level auth is only as good as the developer who remembered to write it. A folder-level interceptor is applied by the executor before the endpoint's own lambda runs, walking up the hierarchy, and there is nothing an endpoint file can say to decline it. That is what it looks like when a permission boundary is structural instead of advisory — a theme I have written about at more length in Agentic AI Without Permission Boundaries, and which the gym in this story learned the expensive way.
Below all of it sits the ordinary, boring hygiene: parameterised ADO.NET everywhere, BCrypt for passwords, and — as of this week — login throttling at ten attempts per minute per IP with a global ceiling behind it, CORS that hands credentials only to same-site or explicitly configured origins, and an auth.token.verify that now refuses to fetch OpenID metadata from a token's own issuer until that issuer has been matched against a list the caller supplied. The issuer of an unverified token is, by definition, attacker-controlled. That fix came out of the OAuth work Claude did here.
Your agent gets the same checks your browser does
Because all of this lives in the invocation path rather than in a frontend, the MCP server inherits it for free. Calling a tool over MCP runs the endpoint — the actual endpoint, carrying the caller's own identity — so the role check that guards it from a browser is the role check that guards it from an agent. There is no second implementation to drift out of sync.
The tool catalogue is assembled per caller from the roles on the ticket, confined to modules/, and excludes the MCP and OAuth plumbing so the transport never lists itself. I made that case properly in From Prompt to MCP Tool in 5 Seconds and Convert Your OpenAPI Specification to a Secured MCP Tool, so I will leave it there.
The honest edges
A security claim with no caveats is marketing. Here are mine.
Root is not sandboxed, by design. You saw the branch at the top of this article: if the caller holds root, the generated Hyperlambda goes straight to invoke with the entire runtime available and no timeout. That is deliberate. Root is the administrator of the cloudlet, and an administrator who cannot administer is just a broken account. But it means the sandbox protects you from your agent, not from yourself — so if you connect an agent to a cloudlet using root credentials, you have opted out of everything described above. Give agents their own user and their own role. [whitelist] cannot save a caller who was handed the keys.
Whitelist a whitelist and you have handed over the key. The vocabulary lives on a stack, and lookups take the innermost scope. A nested [whitelist] therefore replaces the vocabulary rather than intersecting with it — so if you grant whitelist inside a vocabulary, the code you are constraining can declare a wider one for itself. My production list does not include it, and yours should not either unless you have thought hard about why.
Egress is on you.http.get is in the vocabulary. Sandboxed code cannot read your files or your database, but it can make an outbound GET, and a determined payload can encode things into a URL. If that matters in your threat model, drop it from your vocabulary — it is one line.
Timeouts bound compute, not cleverness. Twenty seconds stops a runaway loop. It does not make a wrong answer right.
None of this is theoretical, and I would rather you did not take my word for any of it. In April I pointed Claude Code at the entire codebase and told it to break in. It found four real hardening issues, all fixed — body-size limits, timeouts, a path-resolution edge case, a leaky debug statement — and zero sandbox escapes.
Go break it
The claim in the title is falsifiable, which is the only kind worth publishing. So there is a standing $100 bounty for a verified escape, and nobody has collected it yet.
Everything here is MIT licensed — the runtime you would be attacking, the file quoted at the top, all of it. Read it at github.com/polterguy/magic, or run the whole platform locally in one command:
curl -fsSL https://hyperlambda.dev/docker-compose.yaml | docker compose -f - up
Open localhost:5555, point it at localhost:4444, log in with root / root — and then, before you connect an agent to anything that matters, go and make it a user that is not root. If you would rather someone else kept it patched, that is what a managed cloudlet is.
Most sandboxes are a promise about behaviour. This one is a list of 178 names, and a loop that reads it before every instruction.
Related reading
- Secure AI Code Execution — the full argument
- How I Use Whitelist to Execute Partially Untrusted Hyperlambda Safely
- Why Secure AI Code Execution Requires Runtime Whitelisting, Not Prompt Filtering
- Zero-Hallucination Code Generation: A Vocabulary Your AI Cannot Escape
- Claude Code Tried to Break Magic Cloud, and Mostly Ended Up Confirming Its Security
- Break My AI Sandbox and Make $100
- For AI Agent Builders