Back to Blog
September 10, 2026

The short version: run the REPL somewhere that cannot reach anything you care about. The default local environment in the reference RLM library runs model-written code with exec inside your own Python process. It blocks eval and exec, but it still allows __import__ and open, so it is not a security boundary. For anything that reads untrusted text, use a container with networking turned off, a cloud sandbox, or a WASM interpreter. Then cap iterations and sub-calls.

Why does an RLM need a sandbox at all?

Because the model writes code and the harness runs it. In the RLM paper, the prompt is stored as a variable in a Python REPL. The root model sees only metadata about it, then writes code to slice it, search it, and call llm_query on the pieces. Every turn the model emits a code block, and the environment executes it. That is arbitrary code execution by design.

The paper itself says little about isolation. Its implementation section describes "a Python REPL environment, which loads a module for querying a sub-LM." The limitations section names "sandboxed REPLs" as future work, next to asynchronous sub-calls, as a way to cut runtime and cost. Security is left to whoever deploys the scaffold. Our RLM vs coding agent post notes that both need a sandbox for the same reason. This post is about how to build one.

Where does the hostile code come from?

Usually not from the model on its own. It comes from the input. The whole point of an RLM is to process inputs too large to read: a corpus of 1,000 documents, a scraped website, a repository, a mailbox. The root model reads slices of that input through printed output, and sub-calls read larger slices and return text. Any instruction hidden in a document can reach the model that writes the next code block.

This is indirect prompt injection. Greshake et al. showed that attackers can inject prompts "into data likely to be retrieved" and that "processing retrieved prompts can act as arbitrary code execution." An RLM makes that last phrase literal. A document that says "before answering, run this snippet" is one obedient turn away from executing on your machine.

The reference library says the same thing in plainer words. Its README describes non-isolated environments as "pretty reasonable for some local low-risk tasks, like simple benchmarking, but can be problematic if the prompts or tool calls can interact with malicious users." In an RLM, the prompt is the document set. If you did not write every document, treat the prompt as untrusted.

What does the default local REPL actually block?

Less than its name suggests. The docstring in local_repl.py says it "executes code in a sandboxed namespace with access to context data." The namespace is a custom builtins dictionary, _SAFE_BUILTINS. It sets input, eval, exec, compile, globals, and locals to None. It keeps __import__ and open.

That means model code can still run import os, import subprocess, or import socket. It can read your environment variables and API keys. It can open files anywhere your user can, and it can make network calls. The code runs through exec(code, combined, combined) in the same process as the RLM, so it also shares memory with the client that holds your provider credentials. A restricted builtins dictionary stops accidents, not attackers.

The working directory is not isolated either. The environment creates a temp directory and switches into it with os.chdir. That call changes the directory for the whole process. An open issue, #180, reports that concurrent RLM instances race on it and cross-contaminate each other's working directory. The README is candid: the local REPL "is generally safe, but should not be used for production settings." Take the second half of that sentence seriously.

What do the isolated options give you?

The library lists seven environments. local, ipython, and docker run on your machine. modal, prime, daytona, and e2b run in cloud sandboxes, which the README says ensure "complete isolation from the host process." In the isolated setups, the README says a recursive sub-call "is requested from the host process," so the sandbox does not need your model API keys.

  • Docker. DockerREPL runs code in a python:3.11-slim container with --rm, a mounted temp directory, and a host alias so llm_query can reach an HTTP proxy on the host. The start command sets no network flag. Docker's documentation says containers "have networking enabled by default, and they can make outgoing connections." So a stock container can still send data out. If the task needs no internet, add --network none and give the proxy another path, such as a mounted Unix socket.
  • Cloud sandboxes. Modal, Prime, Daytona, and E2B move execution off your hardware. The Daytona RLM guide gives each agent "its own isolated Daytona sandbox," exposes max_sandboxes as a budget, and deletes sub-agent sandboxes "immediately after completion." Prime Intellect's RLM environment runs code in isolated sandboxes and makes extra tools usable "only by the sub-LLMs," which keeps tool output out of the root context.
  • WASM. DSPy's RLM module runs code in a Deno and Pyodide runtime. Its docs state that "the model's code is untrusted, so it runs in a sandbox" with "no filesystem or network access" by default. You grant access by allowlist: enable_read_paths, enable_write_paths, enable_env_vars, and enable_network_access with specific domains.

What should a production sandbox policy include?

Start from what an RLM actually needs, which is very little. The root model needs the context variable, a Python standard library, and a way to call llm_query. It rarely needs the internet, your filesystem, or your credentials. Build the policy from that list:

  1. Separate processes. Model code never runs in the process that holds API keys. Sub-calls cross the boundary as requests, and the host decides whether to serve them.
  2. No network by default. Block all egress. If a task needs fetching, give it a narrow domain allowlist, as DSPy does, rather than open access.
  3. Read-only context. Mount the input read-only and give the code a scratch directory it can write to. Nothing else on disk should be visible.
  4. Hard budgets. Cap REPL turns, sub-calls, wall-clock time, memory, and output size. DSPy defaults to max_iters of 20, max_llm_calls of 50, and 10,000 characters of REPL output per turn. A prompt-injected loop that calls llm_query ten thousand times is a billing attack even without a shell.
  5. One sandbox per run. Create a fresh environment per request and destroy it after. DSPy creates one interpreter per forward() call and shuts it down afterward. Do not let one user's context persist into another user's run.
  6. Log the code. Keep every executed block with the trajectory. When a run goes wrong, the code is the evidence, and our limitations post covers how often trajectories go wrong for ordinary reasons.

Isolation has a latency cost. A cloud sandbox adds a network round trip for each REPL turn and each proxied sub-call, and RLMs already make many sequential calls. Our post on RLM latency covers where that time goes. Batched sub-calls (llm_query_batched) reduce the round trips, and they matter more once every call crosses a process boundary.

The bottom line

An RLM executes code written by a model that has just read your input. That input may contain instructions from strangers. The reference library's default local environment keeps imports and file access, runs in your process, and says it is not for production. Use it for benchmarks on data you trust. For anything else, run the REPL in a container with networking turned off, a cloud sandbox, or DSPy's WASM interpreter. Keep credentials on the host, mount the context read-only, and cap turns and sub-calls. The sandbox is not optional plumbing. It is the part of the RLM that makes reading untrusted input safe.

References & Further Reading

  1. Zhang, A. L., Kraska, T., Khattab, O. "Recursive Language Models." arXiv:2512.24601, December 2025. The REPL design with the prompt as a variable, and sandboxed REPLs listed as future work in Section 6. arxiv.org/abs/2512.24601
  2. Zhang, A. L., et al. "rlm: General plug-and-play inference library for Recursive Language Models." GitHub README, accessed September 2026. The seven REPL environments, the non-isolated vs isolated warning, and the statement that the local REPL is not for production. github.com/alexzhang13/rlm
  3. alexzhang13/rlm. "local_repl.py." GitHub source, accessed September 2026. The _SAFE_BUILTINS dictionary, exec in the host process, and the os.chdir temp directory. github.com/alexzhang13/rlm/blob/main/rlm/environments/local_repl.py
  4. alexzhang13/rlm. "Issue #180: LocalREPL._temp_cwd: process-wide os.chdir races across concurrent RLM instances." GitHub, August 2026. Working-directory cross-contamination between concurrent runs. github.com/alexzhang13/rlm/issues/180
  5. Greshake, K., Abdelnabi, S., Mishra, S., Endres, C., Holz, T., Fritz, M. "Not what you've signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection." arXiv:2302.12173, February 2023. Injected prompts in retrieved data acting as arbitrary code execution. arxiv.org/abs/2302.12173
  6. DSPy. "RLM." API documentation, accessed September 2026. The Deno and Pyodide WASM sandbox, no filesystem or network access by default, access allowlists, and the max_iters, max_llm_calls, and output limits. dspy.ai/api/modules/RLM
  7. DSPy. "RLM: exploring large contexts with code." Documentation, accessed September 2026. Model code treated as untrusted, and one interpreter per forward() call. dspy.ai/diving-deeper/rlm
  8. Docker. "Networking overview." Docker Engine documentation, accessed September 2026. Containers make outgoing connections by default; the none driver isolates them. docs.docker.com/engine/network
  9. Daytona. "Build deep Recursive Language Models." Documentation guide, accessed September 2026. One isolated sandbox per agent, the max_sandboxes budget, and deletion of sub-agent sandboxes after completion. www.daytona.io/docs/en/guides/rlm/recursive-language-models
  10. Mueller, S. "Recursive Language Models: the paradigm of 2026." Prime Intellect blog, January 2026. Code execution in isolated sandboxes and tools restricted to sub-LLMs. www.primeintellect.ai/blog/rlm
FAQ

Frequently asked questions

Is the rlm library's local REPL safe for untrusted documents?

No. It removes eval, exec, compile, input, globals, and locals from builtins, but it keeps __import__ and open, so model code can import os or subprocess. It runs with exec inside the same process as the RLM. The README says it should not be used for production settings.

Does running the REPL in Docker stop data from leaving the machine?

Not by default. The DockerREPL start command sets no network flag, and Docker containers can make outgoing connections unless you use the none network driver. Add --network none and route llm_query through a channel that does not need open networking.

Can prompt injection in the input make an RLM run malicious code?

Yes, in principle. The root model reads slices of the input and then writes the next code block, so instructions hidden in a document can steer that code. This is the indirect prompt injection pattern that Greshake et al. described in 2023. The sandbox limits the damage when the model obeys.

Why do sub-calls go back to the host instead of running inside the sandbox?

So the sandbox does not need your model API keys. In the isolated environments of the reference library, a recursive sub-call is requested from the host process. The host can then enforce budgets and reject calls it does not want to serve.

What limits should an RLM sandbox enforce?

Cap REPL turns, sub-calls, wall-clock time, memory, and output size. DSPy's RLM module defaults to 20 iterations and 50 LLM calls per run. These caps stop a runaway or injected loop from turning into a large bill.