securer runs R code written by an LLM in a sandbox, and lets that code call functions you choose. The calls run in your own R session, so the code can reach your data through those functions and nothing else.
The package is experimental, and function names may still change.
A quick look
Give the sandboxed code a function it can call. Here that function is a stand-in for a database query:
library(securer)
tools <- list(
securer_tool("query_db", "Query a database table",
fn = function(table, limit) head(get(table, "package:datasets"), limit),
args = list(table = "character", limit = "numeric"))
)
result <- execute_r('
data <- query_db("mtcars", 5)
mean(data$mpg)
', tools = tools, sandbox = TRUE)When the code reaches query_db(), it pauses. Your session runs the real function, outside the sandbox, and sends the result back. Then the code carries on. The LLM’s code never touches your files, network, or data directly.
Why use it
LLM-written code can do things you didn’t ask for. securer limits what it can do:
| Risk | What securer does |
|---|---|
| The code reads or writes your files | An OS sandbox lets it write only to a temp directory and read only R’s own files and libraries |
| The code makes network requests | Network access is blocked, by namespaces on Linux and Seatbelt on macOS |
| The code needs your APIs or databases | You register tool functions, which run in your session |
| The code runs forever | A timeout stops it, and the session restarts so you can keep using it |
| The code uses all your memory | Resource limits, with ulimit on Linux and macOS and Job Objects on Windows |
| The code doesn’t parse | It’s checked for syntax errors before it’s sent to the sandbox |
Posit’s commons package has its own sandbox for its built-in agents, but you can’t reuse it for an agent you write yourself. securer is meant for that case.
How it works
The two processes talk over a Unix domain socket. A tool call blocks the child until the parent sends back the result.
Features
Sessions
Starting a sandboxed R process takes a moment. Keep one open to run several pieces of code:
session <- SecureSession$new(tools = tools, sandbox = TRUE)
session$execute('query_db("iris", 3)')
session$execute('query_db("mtcars", 5)')
session$close()Session pools
If you run many requests at once, start a few sessions ahead of time:
pool <- SecureSessionPool$new(size = 4, tools = tools, sandbox = TRUE)
result <- pool$execute('query_db("iris", 3)')
pool$close()Resource limits
Cap CPU time, memory, file size, and more. Limits work on every platform, with or without the sandbox:
Timeouts
Code that runs too long is stopped. The session restarts itself, so you can use it again straight away:
session <- SecureSession$new()
session$execute("Sys.sleep(100)", timeout = 5)
# Error: Execution timed out after 5 seconds
session$execute("1 + 1") # still worksSyntax checks
Code that doesn’t parse fails right away, without a trip to the child process:
session$execute("if (TRUE {") # immediate errorellmer
Give an ellmer chat a tool that runs R code in the sandbox:
library(ellmer)
chat <- chat_openai()
chat$register_tool(as_ellmer_tool())
chat$chat("Calculate the mean of 1 through 100 using R")Audit log
Write every session event to a JSONL file, one JSON object per line:
session <- SecureSession$new(audit_log = "session.jsonl")Verbose mode
Print what the session is doing as it happens:
session <- SecureSession$new(tools = tools, verbose = TRUE)
session$execute('query_db("iris", 3)')
# [securer] Session started (sandbox=false, pid=1234)
# [securer] Tool call: query_db(table=iris, limit=3)
# [securer] Tool result: query_db -> ... (0.00s)
# [securer] Execution complete (0.27s)Platform support
| Platform | Sandbox | Network blocked | Filesystem restricted | Resource limits |
|---|---|---|---|---|
| Linux | bubblewrap (bwrap) |
Yes | Yes | Yes (ulimit) |
| macOS | Seatbelt (sandbox-exec) |
Yes | Yes | Yes (ulimit) |
| Windows | Job Objects and a clean environment | No | No | Yes (memory, CPU, process count) |
| docker-spawn (any host) | A new container per session | Yes (--network=none) |
Yes (container filesystem) | Yes (--memory, --cpus, ulimit) |
Tool calls, timeouts, and syntax checks work everywhere.
On Windows, securer can only clean the environment and set resource limits. It can’t stop the code from reading files or using the network. If you need that on Windows, use the docker-spawn backend or a Linux VM.
The docker-spawn backend is off by default. Turn it on with SECURER_SANDBOX_MODE=docker-spawn. It needs the docker command and a running Docker daemon. inst/docker/Dockerfile has a reference image.
Security
The sandbox is one of several protections. securer also checks that only the child it started can connect to the socket, limits resources, strips secrets from the child’s environment, and checks tool arguments. The security model vignette (vignette("security-model", package = "securer")) covers what securer protects against and what it doesn’t.
To report a security problem, email the maintainer instead of opening a public issue.
Related packages
securer is part of a small set of packages for running LLM agents in R more safely:
- secureguard checks prompts, generated code, and outputs for things like prompt injection and leaked secrets.
- securetools has ready-made tools (file access, SQL, web requests) with limits built in.
- securebench measures how well your guardrails work. It complements vitals.
They build on Posit’s tools rather than replacing them. For tracing, use ellmer’s OpenTelemetry support through the otel package. For retrieval (RAG), use ragnar.
Learn more
-
vignette("quickstart", package = "securer"): installing and first examples -
vignette("sessions-and-tools", package = "securer"): sessions, streaming output, and pools -
vignette("deployment", package = "securer"): sandboxes, resource limits, and how the pieces fit -
vignette("security-model", package = "securer"): what securer protects against -
vignette("ellmer-integration", package = "securer"): using securer with ellmer chats -
vignette("integration-examples", package = "securer"): Shiny, Plumber, and batch jobs -
vignette("troubleshooting", package = "securer"): common problems - Function reference
Found a bug or have an idea? Open an issue.