What Scala's Type System Can and Can't Stop an AI Agent From Doing

Every team that gives an AI agent real access runs into the same tradeoff. An agent is only useful when it can read your files, query your systems, and call outside services, and that same access is exactly what an attacker goes after when a poisoned web page or email slips instructions into the agent's context.

The usual answer is an AI agent sandbox like a container or virtual machine, and it handles part of the problem well. Scala 3 offers a different kind of sandbox that checks the agent's code before it runs, and it can block leaks a container cannot see. It also has clear limits that the hype tends to skip.

Below, we cover why runtime sandboxes leave a gap, how Scala's type system closes part of it, what the research shows so far, what the approach cannot stop, and when it fits your agents.

TL;DR

Runtime sandboxes like Docker limit which resources an AI agent can touch, but they cannot stop a leak that flows between two actions the agent is allowed to take. Scala 3 capture checking and safe mode let a harness reject agent code at compile time when it tries to use access it was never granted or route classified data to the outside world. In the research behind this approach, no classified data leaked in any trial while agents kept their task performance. The approach does not stop wrong answers, side channels, or risks from external commands, so it works best alongside a runtime sandbox, not instead of one.

Why Runtime Sandboxes Leave AI Agents Open to Data Leaks

A runtime sandbox controls which resources an AI agent can reach, such as a directory, a set of system calls, or a list of network destinations. That protection matters, but a useful agent needs some private data and some outside communication, so the sandbox has to allow both.

The leak happens in the space between those allowed actions. A support agent might be allowed to pull customer records from a CRM and also allowed to call a translation API, and a single injected instruction can join the two by sending a customer's record along with the text to translate. Each call is legitimate on its own, so a sandbox that judges calls one at a time sees nothing to block.

Security researcher Simon Willison calls this combination the lethal trifecta: access to private data, exposure to untrusted content, and the ability to communicate externally. When one agent has all three, an attacker who can get text in front of it can often get data out of it. OWASP ranks prompt injection as the top risk for LLM applications and notes that it is unclear whether any foolproof prevention exists.

How Scala 3 Sandboxes an AI Agent at Compile Time

A research team at EPFL led by Martin Odersky, the creator of Scala, proposed a different model in the paper "Tracking Capabilities for Safer Agents," a version of which was accepted at ACM CAIS 2026. Instead of calling tools directly, the agent writes short Scala 3 programs, and the compiler checks every program before it runs. The team built this as TACIT, an open-source MCP server.

The agent itself does not need to be written in Scala or know anything about the type system. Any MCP-compatible agent, including Claude Code, can connect to TACIT without changes and send Scala snippets as tool calls, and only those snippets get compiled and checked.

Capabilities Decide What an AI Agent Can Touch

In this model, the agent can only act through capabilities it was handed, such as a file system limited to one directory, network access limited to approved hosts, or command execution limited to an allowlist. The agent cannot forge a new capability, and Scala 3 capture checking rejects any code that tries to use access outside its grant or carry a capability past the scope where it was valid.

Pure Functions Keep Classified Data From Leaking

The most important idea in the paper is local purity. In the paper's design, sensitive data arrives wrapped in a Classified container, and the only way to work with the contents is through a function the compiler can prove has no side effects. The example below applies that design to a security log.

scala
val logs: Classified[String] = readClassified("auth-logs.txt")
 
// Allowed: a pure function counts failures, and the count stays classified
val failedLogins: Classified[Int] =
  logs.map(_.linesIterator.count(_.contains("LOGIN_FAILED")))
 
// Rejected at compile time: the function captures a webhook capability
logs.map { text => webhook.send(text); 0 }

A pure function holds no capabilities, so it cannot write a file, open a connection, or print anything back to the agent. Printing a Classified value shows the agent only a redacted placeholder, while the real content goes to a separate channel that only the human user sees. When the agent needs to reason over sensitive content, such as summarizing a confidential report, the work goes to a trusted local model, and the result comes back still wrapped.

Scala Safe Mode Closes the Escape Hatches in Agent Code

Full Scala includes features like unchecked type casts and runtime reflection that can make the compiler "forget" a capability. Scala 3.8.3 added safe mode, a restricted subset of the language for agent-generated and other untrusted code, turned on with import language.experimental.safe. Safe mode blocks unchecked casts and pattern matches, features from Scala's caps.unsafe module, @unchecked annotations, and runtime reflection, and it compiles everything with capture checking, explicit nulls, and mutation tracking turned on. Agent code can only call library modules that are themselves safe, or that a human has reviewed and marked with @assumeSafe.

What the TACIT Research Shows So Far

The paper tested two questions: whether the type system actually prevents leaks under attack, and whether writing typed Scala hurts the agent's ability to get work done.

  • No classified data leaked with type protection on. The team ran 131 trials per model, pairing everyday tasks with 10 prompt-injection attacks of rising sophistication plus direct and social-engineering requests for secrets. With secrets wrapped in Classified, both models tested leaked nothing. Without the wrapper, the share of leak-free trials fell to 98.5% for one model and 91.6% for the other, because safety then depended on how each model was trained.
  • Agents kept their performance. Agents writing typed Scala matched or slightly beat standard tool calling on a customer-service benchmark, by 0.8 to 3.7 percentage points, and dipped slightly on a benchmark of real GitHub issues, 41.7% compared with 43.3%.
  • Compile errors were rare and quick to fix. In the customer-service tests, between 0.32% and 7.93% of agent-written snippets needed a retry, agents usually fixed them in one or two attempts, and compiling a snippet took under a second.
  • The setup cost is modest. The reusable safety infrastructure is about 1,000 lines of Scala, written once and shared across agents.

These results come from the authors' own experiments on research benchmarks, not from independent production deployments. They make a strong case for the approach, and they also leave room for real-world testing to confirm them.

What Scala's Type System Can't Stop an AI Agent From Doing

The paper is direct about its limits, and any team evaluating the approach should weigh them as carefully as the results.

  • Wrong answers. The type system guarantees that an agent's actions are permitted, not that its output is correct. A hallucinated summary passes every check.
  • Influence over the agent's reasoning. A prompt injection can still change what the agent decides to do. The type system only stops that influence from turning into an action outside the agent's grant.
  • Side channels. Covert signals like timing fall outside the guarantee, so a determined agent could in principle signal small amounts of information through behavior the type system does not track.
  • External commands. Once an agent runs an allowlisted shell command, that process sits outside Scala's checks, and the protection drops to whatever the allowlist allows.
  • Exceptions. Safe mode does not track exceptions in types. The harness contains them at runtime instead, by turning them into failure values.
  • Human decisions. The capabilities a team grants, the modules marked @assumeSafe, and the trusted local model are all human choices. A capability that is too broad is a permission that is too broad.
  • Maturity. Safe mode and capture checking are both still experimental in Scala, and TACIT is a research implementation.

Compile-Time Checks and Runtime Sandboxes Work Best Together

The research positions type-based checks as a partner to runtime sandboxing. A container or virtual machine guards against escapes from the runtime itself, such as JVM bugs or native code, while capability tracking blocks information leaks between permitted operations. The paper recommends combining the two for security-critical deployments.

Question Runtime sandbox (container or VM) Compile-time capability checks (Scala 3)
Does it stop a leak between two permitted actions? No Yes, for data wrapped as classified
What does it control? Which files, system calls, and network destinations a process can reach Which capabilities each piece of agent code can use, down to a single function
When is a violation caught? While the code runs Before the code runs
Does it contain runtime escapes and native code? Yes No
Does it cover external shell commands? Yes, within its isolation boundary Only through a command allowlist

When a Type-Based AI Agent Sandbox Fits, and When It Doesn't

When a Type-Based Sandbox Is a Good Fit

  • Your agents combine confidential data, untrusted content, and outside communication, which is the exact combination runtime sandboxes struggle to secure.
  • Your agents already work by writing and running code, since the capability layer replaces raw scripts with checked Scala programs.
  • Your organization needs safety guarantees that hold no matter which model powers the agent, including models that are cheaper or less carefully aligned.
  • Your team runs on the JVM or Scala and can build and own the capability layer that defines what each agent may touch.

When to Hold Off on a Type-Based Sandbox

  • Your agents mostly run shell commands, where the protection drops to the command allowlist.
  • Your main concern is wrong output rather than leaked data, since the type system does not check correctness.
  • Your team needs production-stable tooling today, since safe mode and capture checking are still experimental.
  • Nobody on your team can design capability APIs in Scala 3, and you have no partner who can.

Questions to Ask Before Trusting Any AI Agent Setup

Whatever approach a team chooses, these questions show quickly whether an agent setup actually protects sensitive data:

  • If the agent reads confidential data and can also reach the network, what stops that data from leaving?
  • Which protections are enforced by code, and which depend on the model following its instructions?
  • When a prompt injection succeeds, what is the worst action the agent can still take?
  • Who decides what each agent is allowed to touch, and who reviews those decisions?

A prompt asks an agent to behave, and a filter tries to catch it when it does not. Scala's type system works one step earlier, by refusing to build the code for a specific set of dangerous actions at all. For teams building agents on top of sensitive data, that guarantee is worth understanding now, and our Scala AI and ML development work is where we help teams apply it. For the broader case on how the compiler shapes AI-written code, see our post on when AI writes good Scala code.

Planning an AI agent your team can trust with real data?

Scala Teams provides senior Scala engineers and complete teams who can build your AI agent from the ground up, including the tool integrations, the capability layer that limits what it can reach, and the typed code that keeps sensitive data where it belongs. Tell us what you want your agent to do, and we will talk through how to build it. Talk to a Scala expert.

Frequently Asked Questions

Can Scala's type system sandbox an AI agent?

Scala 3's type system can sandbox a specific class of AI agent behavior. With capture checking and safe mode, a harness rejects agent code at compile time when it uses access it was never granted or sends classified data through side effects. The type system does not stop wrong answers, side channels, or risks from external commands, so it works best alongside a runtime sandbox.

What is TACIT?

TACIT, short for Tracked Agent Capabilities In Types, is an open-source MCP server from an EPFL research team led by Martin Odersky. It compiles Scala 3 code submitted by an AI agent with capture checking and safe mode turned on, and only runs code that passes, through a typed library that controls access to files, processes, and the network.

What is Scala safe mode?

Safe mode is a restricted subset of Scala 3, added in Scala 3.8.3 and turned on with import language.experimental.safe, that keeps capabilities from being forgotten. It blocks unchecked casts, features from the caps.unsafe module, @unchecked annotations, and runtime reflection, and it compiles code with capture checking, explicit nulls, and mutation tracking enabled.

Does an AI agent need to be written in Scala to use this approach?

No. An AI agent connects to a harness like TACIT through standard MCP tool calls, so tools such as Claude Code can use it without changes. The agent only needs to write short Scala snippets, which the harness compiles and checks before running them.

Why can't Docker stop an AI agent from leaking data?

Docker limits which files, system calls, and network destinations an AI agent can reach, but a useful agent needs both private data and outside communication. When an agent combines two permitted calls, such as reading a customer record and sending it to an approved API, each call looks legitimate on its own, so Docker has nothing to block.

Next
Next

Hard to Hire, Hard to Learn, Hard to Break: A CTO’s Scala Case at Scala Days 2026