This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.8.0 instead.
PY-SANDBOXES
Protect Python programs without their knowledge.
Applications increasingly run code, or call tools, chosen by an LLM, and a manipulated model can make them read local files, reach the intranet or run commands (LLM05:2025 Improper Output Handling, LLM06:2025 Excessive Agency). Py-Sandboxes restricts what the process may actually do, from a whitelist learned by watching it run.
Not yet on pypi.org. Until the project is published there,
pip install pysandboxesanduvx python-sbdo not work. The pre-releases are on test.pypi.org: declare this alias, then usepython-sbwherever this page saysuvx python-sb:alias python-sb='uvx --prerelease allow \ --find-links https://test.pypi.org/simple/python-sb/ \ --find-links https://test.pypi.org/simple/pysandboxes/ \ python-sb' python-sb --helpOr install it from the sources:
git clone https://github.com/pprados/pysandboxes.git cd pysandboxes pip install -e . python-sb --help
Quick start
Run any module in a sandbox, without installing anything and without touching a single line of your code:
uvx python-sb -m my_module
Or install it:
pip install pysandboxes
python-sb -m my_module
The first run has no rules yet, so it starts in learning mode: use your application normally, and a .py-sandboxes file is written when it stops, populated with the network, disk, module and environment accesses it actually needed. Review that file, then every later run is restricted to that whitelist.
That is the whole integration for complete mode. To sandbox only part of an application, see partial mode.
What it protects against, and what it does not
Py-Sandboxes is built for one threat model: your own application, or the code an LLM makes it run, doing more than it should. It offers three layers, with deliberately different strengths:
- The dynamic-code layer (the
eval-*rules) is the innermost and the weakest of the three, but it is the only one that looks at code arriving as a string — which is exactly the shape of an LLM's answer. The other two layers only see a process that has already decided to run it. A source handed toeval(),exec()orcompile()is parsed, checked against a declared sub-language, rewritten and run under a budget and a timeout, which is what makes it possible to execute LLM-generated Python code with a stated set of capabilities. See theeval-*rules. - The Python layer (py-sandbox) is a guardrail and a readability layer. It catches LLM-generated code that goes off the rails, and it states in a single file exactly what your application is allowed to touch. It is not a boundary against a determined attacker:
ctypes, compiled extensions or direct syscalls can work around API interception. - The OS layer (os-sandbox: landlock, bwrap, firejail, unshare, qemu) is the real security boundary. It is enforced by the kernel, so it also holds against compiled code.
The Python and the OS layers feed each other. Writing an OS-level policy by hand is the tedious part of any sandboxing effort: you have to know, up front, every directory, host, port and variable the process will legitimately need. The Python layer answers exactly that question, because it observes those accesses through the standard APIs while your application runs. The .py-sandboxes file produced by learning mode is therefore not only the Python whitelist, it is also the inventory used to configure the OS layer, from the same declarations and without a second round of trial and error.
Nest them: the dynamic-code layer for the strings an LLM produces, the Python layer for precision and legibility, the OS layer for enforcement. See OS-sandbox vs Py-sandbox for the per-technology matrix.
This is not a defense against a malicious third-party dependency that you installed yourself.
Use cases
python-sb replaces python on a command line, so anything launched as a Python script runs with the rights
written in its .py-sandboxes file:
- Agent skills, hooks and plugins: the scripts of a skill run with all the rights of the user. A rule file shipped with the skill bounds them, and doubles as its permission manifest.
- Tools, CLIs and MCP servers: a tool driven by a manipulated LLM can only reach what its function needs.
- AI coding agents: the rule file is versioned, so
git diff -- '*.py-sandboxes'shows every right the agent added between two commits. - Dependency updates: a run without
--learnturns a new behavior of a dependency into a visible violation. - Untrusted tests in CI, audit of an unknown script with
--learn, LLM-generated code.
See use cases for each scenario, the protection level it needs, and what the rule file does not show.
Platform support
For now, Linux and WSL only (at this time). Every OS-level backend (landlock, bwrap, firejail, unshare) is a Linux technology. On macOS and Windows, only the Python layer is available, without the OS boundary.
Table of Contents
- Quick start
- What it protects against, and what it does not
- Use cases
- Platform support
- Principle
- Usage
- Security Filters
- OS-sandbox vs Py-sandbox
- Samples
- Documentation
Principle
It is not easy to control the code that an LLM will execute. It can generate Python code that will be directly invoked, or describe the launch of a tool that the application will run, with parameters provided by the LLM (directly via a function or indirectly via an API or an MCP call).
Furthermore, developers are increasingly using AI to improve code. Without rigorous verification, the generated code can open up security vulnerabilities.
It's time to control, as much as possible, the allowed capabilities for your application.
The Py-Sandboxes project proposes to add multiple layers of security to limit the actions of your application, and thus, indirectly, the actions caused by an LLM or a malicious user of your application.
For example, an MCP server that exposes a service to view a WEB page can be abused to request it to view a page on localhost, an address on the intranet, a swagger documentation, or file:/// to read local files. Code generated by an LLM can generate a specially crafted regular expression invocation to cause a denial-of-service or an infinite loop. You can find some demo here.
Among these risks, some can be reduced if a part of the application code is executed in one or more dedicated sandboxes.
The idea is to use a defense-in-depth approach, where multiple layers support each other to limit the capabilities and necessary privileges for each component as much as possible. Is it wise to allow every piece of Python code or every dependency to have access to all application files?
The Py-Sandboxes solution we propose aims to address these difficulties. The idea is to offer a Python Sandbox mechanism, allowing the execution of Python code, but limited in its capabilities. This is a similar approach to AppArmor or capabilities under Linux.
The approach consists of filtering and strengthening standard Python APIs to limit the application's action capabilities. This API interception approach is effective but cannot guarantee that there are no workarounds. This is why our solution allows the nesting of other technologies, such as os-sandbox. These technologies rely on the OS's ability to limit network, disk, resource, and other accesses. Nesting an os-sandbox with a py-sandbox is an interesting combination for controlling application security.
Like TypeScript Deno, our solution helps reduce the following risks:
- Path Traversal: Only authorized directories can be accessed.
- Remote Code Execution (RCE): Sensitive APIs are not available.
- Reverse Shells: Network connections are limited.
- Excessive Permissions: All code is under the control of the Python sandbox.
- Token Theft: Accessible files and environment variables are filtered.
- Remote Access: Network and code actions are limited.
- Malicious Execution:
eval(),exec()andcompile()are refused unless the profile declares the sub-language they may run, via theeval-*rules. - Denial of Service: An evaluated string runs under an iteration budget, a recursion bound, an allocation ceiling and a timeout the caller can recover from (
eval-timeout=,eval-max-iterations=). - Malicious syntax: The syntax of a dynamically evaluated string is filtered against a declared whitelist of constructs (
eval-syntax=)
The approach is based on the principle of Least Privilege and Defense in Depth, with an exclusively "whitelist" configuration. By default, everything is forbidden. You must explicitly authorize actions.
A learning mechanism allows for continuous improvement of security rules and rapid implementation.
Usage
Installation is covered in Quick start. To track the development version instead:
pip install pysandboxes
We propose only four things:
- A Command Line Interface:
python-sb - A function:
run - A resource provider:
sandboxes - An annotation:
@sandbox
There are two usage modes:
- Apply the sandbox to the entire application (complete mode).
- Apply the sandbox to a part of the application, with the rest being free (partial mode).
Apply the sandbox to the entire application (complete mode)
flowchart TD
subgraph OSSandbox ["OS-sandbox"]
direction LR
subgraph PythonSandbox [Python Sandbox]
A[Caller Code]
C["<s>@sandbox</s><br/>my_function(...)"]
end
end
A -- "1- my_function(param)" --> C
C -- "2- Return to caller" --> A
%% Custom style for OSSandbox
style OSSandbox fill:#ebe0d0,stroke:#2f2617,stroke-width:4px
style PythonSandbox fill:#aa7c52,stroke:#2f2617,stroke-width:4px
This scenario is the simplest. You just need to replace the launch of your application (python -m my_module) with a launch in the sandbox (uvx python-sb -m my_module). The @sandbox annotation is ignored. It's possible to add some py-sandboxes parameters, at the beginning:
uvx python-sb --learn -m my_module
Note: if you install the module with
pip install pysandboxes, you can usepython-sb -m ....
You can use it in interactive mode and continue to use the help shortcut.
> uvx python-sb
SANDBOXES Python 3.13.5 | packaged by Anaconda, Inc. | [GCC 11.2.0] on linux
**APIs are LIMITED according to the rules in '.py-sandboxes'**
Type "help", "copyright", "credits" or "license" for more information.
IPython 8.37.0 -- An enhanced Interactive Python. Type '?' for help.
⚠ In [1]: import os
⚠ In [2]: os.open??
Signature: os.open(path, flags, mode=511, *, dir_fd=None)
Docstring:
Open a file for low level IO. Returns a file descriptor (integer).
If dir_fd is not None, it should be a file descriptor open to a directory,
and path should be relative; path will then be relative to that directory.
dir_fd may not be implemented on your platform.
If it is unavailable, using it will raise a NotImplementedError.
Type: function
If IPython is installed, it's used. All the standard python parameters are available.
It is recommended for launching an MCP server, for example. It is easy to offer a precise or symbolic mathematical calculation tool by generating code and executing it in an environment limited to numpy, scipy, and sympy.
For more information on using sandboxes with MCP, see here.
The first launch learns the rules, as described in Quick start: exercise every feature of your application, so that nothing it legitimately needs is missing from the whitelist.
If you want to restart a learning session to add missing rules:
- activate the
learnparameter in the configuration file - or add
--learn=.py-sandboxes(or just--learn) when you startpython-sb
This way, only the missing rules will be added to the file.
This approach allows for application isolation, but requires granting privileges to the entire application, such as access to API tokens. It's likely that only a small part of the application needs these privileges, but not the rest.
Use with uvx
uvx is a solution for running a Python tool without installing it in the project. A temporary environment is created for the duration of the tool's execution.
The python-sb command is published on PyPI as its own package, which simply depends on the latest pysandboxes and launches it. So uvx can run it directly, with nothing to install and no --from to spell out:
uvx python-sb --help
Apply the sandbox to a part of the application (partial mode).
Often, the application needs all privileges, has access to all API tokens, etc. Only a part of the application should be executed in a sandbox. For example, tools invoked by an LLM should not have access to all files or all environment variables. This makes it more difficult to abuse them. The granularity is finer.
In this scenario, your application will be split into two parts:
- The core of the application, with all privileges.
- A sandbox, where the code annotated with
@sandboxwill be executed.
flowchart TD
subgraph MainApplication [Main Application]
subgraph WithSandboxes ["with sandboxes()"]
direction TB
A[Caller Code]
end
end
subgraph OSSandbox ["OS-sandbox (Docker,...)"]
direction TB
subgraph PythonSandbox [Python Sandbox in a Child Process]
B(SSE<br/>Server)
C["@sandbox<br/>my_function(...)"]
end
end
A -- "1- my_function(param)" --> B
B -- "2- Execute via IPC (Pickle)" --> C
C -- "3- Return result/<br/>exception" --> B
B -- "4- Propagate to caller" --> A
%% Custom style for OSSandbox
style OSSandbox fill:#ebe0d0,stroke:#2f2617,stroke-width:4px
style PythonSandbox fill:#aa7c52,stroke:#2f2617,stroke-width:4px
Launching the Sandbox
In your program, you must launch the sandbox before you can isolate certain parts of the code.
# Synchronous version
from pysandboxes import sandboxes
with sandboxes(init_fn=init_sandbox):
...
# Asynchronous version
from pysandboxes import sandboxes
async with sandboxes(init_fn=init_sandbox):
...
The init_fn parameter is optional. It can contain a function that will be invoked during the initialization of the sandbox. This is the ideal place to put:
- log level initialization
- opening database connection pools
- all important initializations that need to be done on the sandbox side.
def init_log_level():
sandboxes_level = logging.WARNING
format = '[%(process)d] %(levelname)-5s %(name)s %(message)s'
if is_in_sandbox():
format = " " + format
logging.basicConfig(
level=min(sandboxes_level, logging.INFO),
format=format
)
logging.getLogger("Pysandboxes").setLevel(logging.INFO)
logging.getLogger("pysandboxes").setLevel(sandboxes_level)
def init_fn():
init_log_level()
...
For asynchronous use, you can more simply initialize your application this way:
import pysandboxes
...
async def main():
...
if __name__ == "__main__":
pysandboxes.run(main()) # in place of asyncio.run(main())
Note the following pattern, which involves calling the same initialization function in main() and in the sandbox.
async def init_app():
# Run in main() and in the sandbox
...
async def main():
await init_app()
...
if __name__ == "__main__":
pysandboxes.run(main(), init_fn=init_app) # in place of asyncio.run(main())
If you want to restart a learning session to add missing rules:
- Add
learn='.py-sandboxes'withsandboxes() - Or add
learn='.py-sandboxes'withrun()
pysandboxes.run(main(),
learn='.py-sandboxes',
)
Executing a function in the sandbox (partial mode)
To declare that a function must run in the sandbox, simply annotate it with @sandbox.
# file my_module.py
from pysandboxes import sandbox
@sandbox
def my_function_in_sandbox(param):
...
Once the sandbox is launched, upon invocation of this function, the component handles converting the call into a Pickle-formatted request, sending it to the sandbox, and waiting for the result or exception to be returned to the caller.
All parameters and the return type must be Pickle-compatible.
The sandbox receives the request, loads the corresponding module, finds the function, and invokes it. The response is then also converted into a Pickle-formatted response before being returned to the caller.
There are a few peculiarities to note:
- If an exception is raised in the sandbox, the stack trace is propagated to the main application to allow for a stack analysis as if the call had been made directly. This facilitates debugging.
- If the application writes to stdout or stderr, the stream is captured by the sandbox and returned to the caller. The caller will then write to its own stdout and stderr streams. Thus, the capture of your application's prints includes all information, without forgetting those from the sandbox or mixing the different outputs between several threads. They are executed in the correct process, in the same async loop.
Security Filters
What are the security filters offered by Py-Sandboxes?
- Environment variable control: The environment variables visible in the sandbox are limited. Mapping rules allow easily forwarding sets of variables from the outside to the inside of the sandbox (e.g.,
env=*_API_KEY=${*_API_KEY}). - Network access control: It is possible to control the direction, IP addresses, domain names, and ports available to the sandbox.
- Disk access control: It is possible to map directories to their equivalents in the sandbox. The mapping can be read-only or read and write. Finally, it is possible to specify file filters that should be ignored by the sandbox (e.g.,
ignore=.*). - Imported module control: A whitelist of Python modules accessible to the sandbox must be provided. Importing other modules is rejected.
- Sensitive API call control: A registry of sensitive functions (
os.system,subprocess.Popen,os.kill, ...) is denied by default, whateverpython-import=allows: an import right is not a call right. Permissions are granted per function or per category, and learning mode generates them from the application's real behaviour. - Dynamically evaluated code control: a string handed to
eval(),exec()orcompile()is refused unless the profile declares the sub-language it may use. See Dynamically evaluated code.
Consult the parameter file generated during the first execution for more details.
For a red-team view of these filters — attack by attack, what hostile code can and cannot reach once a profile is armed, which layer stops each attempt and what the layer does not claim to cover — see the security assessment of the Python layer.
Dynamically evaluated code
The two diagrams above show two nested boundaries: the OS sandbox and, inside it, the Python sandbox. A string handed to eval(), exec() or compile() adds a third one. Unlike the first two, it is not a process boundary: the source is parsed, checked against the eval-* rules and rewritten inside the very process that calls eval(), which is why it is drawn with a dashed border.
flowchart TD
subgraph OSSandbox ["OS-sandbox"]
subgraph PythonSandbox ["Python Sandbox"]
subgraph DynamicCode ["Eval Sandbox"]
D["eval() / exec() / compile()<br/>parsed, checked against eval-*,<br/>rewritten, run under budget and timeout"]
end
end
end
%% Custom style for OSSandbox
style OSSandbox fill:#ebe0d0,stroke:#2f2617,stroke-width:4px
style PythonSandbox fill:#aa7c52,stroke:#2f2617,stroke-width:4px
style DynamicCode fill:#6e4a2c,stroke:#2f2617,stroke-width:4px,stroke-dasharray:6 4,color:#ffffff
Every rule of the eval-* family is described key by key, with a valid and an invalid example for each, here. For a red-team view — attack by attack, what a hostile string can and cannot reach and which layer stops it — see the security assessment of the eval-* guard.
OS-sandbox vs Py-sandbox
The Python layer (py-sandbox) cannot stop compiled C/C++/Rust code or direct calls to the kernel. An OS-level provider (os-sandbox), nested around it, closes that gap: subprocess, landlock, unshare, bwrap, firejail or qemu. Select it with the os-sandbox parameter of the config file, or with the OS_SANDBOX environment variable.
OS_SANDBOX=unshare python-sb -m my-module
What each technology enforces, its container and Kubernetes compatibility, and the paranoia levels (which Python and OS combination for which threat): see here
Samples
Twelve samples, one per MCP or agent framework (LangChain, CrewAI, smolagents, ...), run the same two tools under pysandboxes: one fetches a web page, the other evaluates an expression. See here
Documentation
The wiki holds the rest of the documentation: FAQ, configuration files and integration in a module, implementation, weaknesses, security assessments, OS providers, roadmap and related CVEs. Its index is the menu to start from.
Metadata
Release files for pysandboxes 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pysandboxes-0.0.1.tar.gz | 10.7 MB | With provenance | Details for pysandboxes-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pysandboxes-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for pysandboxes-0.0.1-py3-none-any.whl |
Total release size: 11.0 MB
Release files / pysandboxes-0.0.1.tar.gz
| Download URL | pysandboxes-0.0.1.tar.gz |
|---|---|
| Size | 10.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f1219feb4839ae5f7e20dc946668584586c1a79cd9b60b83a9007f8c24cd89d8
|
|
BLAKE2b-256 checksum How to use checksums |
36e2d5ad6ea9d8f17053dd58df6218142e90881bf36efcf5cb47665c836f0422
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logRelease files / pysandboxes-0.0.1-py3-none-any.whl
| Download URL | pysandboxes-0.0.1-py3-none-any.whl |
|---|---|
| Size | 299.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ab8c5d08536396e3403af76f751acf122cd6fb3b7d46eb4ab3c7b9aace0588b9
|
|
BLAKE2b-256 checksum How to use checksums |
4d1695bc980598b861a956baf7c9fe7a433aa1c799ff5b0dcca2fb9ce091c24c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logSecurity for pysandboxes 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pysandboxes-0.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.