use cases - pprados/pysandboxes GitHub Wiki
python-sb replaces python on a command line. Anything started as a Python script or module can therefore be
run with a stated set of rights, and that statement is a text file that can be read, reviewed and versioned.
This page lists the situations where that is worth doing.
Two levels of protection must be kept apart, because the scenarios below do not need the same one:
- Against legitimate code that is misused (a tool driven by a manipulated LLM, a generated script that goes off the rails), the Python layer is enough: it refuses what the rules do not grant, and says so.
- Against code written by someone hostile (a third-party skill, a plugin, a contributor's pull request), the
Python layer is only a guardrail:
ctypes, compiled extensions or a shell command bypass it. A kernel provider (os-sandbox=bwrap,landlock,unshareorqemu, see Choosing a provider) is then required, not optional.
Several scenarios read the .py-sandboxes file as the policy of a program. It is only the part that is written
down. The effective rights also depend on:
- the
includelines: a path starting with./is resolved from the current directory, soinclude "./.local.py-sandboxes", commented out in the template and active only once the user uncomments it, lets whoever runs the command add rules. A bare name (include "common") is resolved next to the including file. The~/.config/...and/etc/...profiles are outside the repository; - the environment:
os-sandbox=${OS_SANDBOX:-landlock}lets a variable downgrade the provider; - the command line: every
--key=valuegiven topython-sbis a rule that takes precedence over the file (--learn,--expose-ro=/etc,--os-sandbox=none), and--pysandboxes-config=chooses the file itself; - the absence of the file: a missing rule file starts the learning mode, where the Python layer records accesses instead of refusing them. Deleting the file opens everything.
When the rights of a program matter, remove the ./ includes from its rule file, write os-sandbox= without a
variable, and add learn=false. The configuration is then refused in three cases: the learning mode is requested,
the rule file is missing, or the command line or the code adds a rule (see Lock the rules). Only
--pysandboxes-config= remains, so pin the full command line where the program is launched.
An agent skill (SKILL.md and a scripts/ directory, as used by Claude Code and other agents) often ships Python
scripts. The agent runs them with all the rights of the user: every file of the home directory, the network, the
API tokens of the environment. Shipping a rule file next to the scripts, and launching them with python-sb,
bounds what a skill can do. For a script, python-sb reads the .py-sandboxes of the script's directory, whatever
the current directory, and falls back to ./.py-sandboxes only when there is none:
my-skill/
├── SKILL.md
└── scripts/
├── .py-sandboxes
└── report.py
<!-- in SKILL.md -->
Run `<skill dir>/scripts/report.py <input>`.A shebang puts the python-sb command line in the script itself, so the agent has nothing to choose, and the line
names no path: it works wherever the skill is installed.
#!/usr/bin/env -S uvx --with tabulate python-sb
# Launched by the shebang through python-sb, never by a bare `python`.
# Needs on the host: uv (for uvx). uvx fetches python-sb, pysandboxes and the third-party imports below.
# Third-party imports: tabulate (the `--with tabulate` of the shebang).
# Rules: ./.py-sandboxes, next to this script, the permission manifest of this skill.
import csv
import sys
from tabulate import tabulate- Under
uvx, the script sees only the environment of the tool: every third-party import needs its--with. The comments under the shebang say what has to be installed, so that an agent reading the script can tell the user before running it, rather than meeting aModuleNotFoundErrorin the sandbox. -
env -Ssplits the line into arguments. The script needs its execute bit, and the whole first line must fit in the 255 bytes Linux reads. - Until the final release reaches PyPI, put the TestPyPI options of the alias in Releasing
(
--prerelease allow --find-links ...) in the shebang, beforepython-sb: 175 bytes for this example.
Launched through this shebang from another directory, with os-sandbox=bwrap, the script prints its report; a
script of the same skill that tries import socket is stopped by the rule file, and a .py-sandboxes or a
.local.py-sandboxes in the current directory changes nothing.
The author of the skill generates the rule file once on their own machine, then reviews it and ships it. Name the
file for learning mode, since a bare --learn writes into the current directory:
python-sb --learn=<skill dir>/scripts/.py-sandboxes <skill dir>/scripts/report.py <input>. Pin the provider, and
keep the exposed directory of the skill: otherwise the script itself is hidden, and python reports
can't open file:
# my-skill/scripts/.py-sandboxes
py-sandbox=true
os-sandbox=bwrap
learn=false
expose-ro=~/.claude/skills/my-skill/scripts
python-import=codecs, csv, encodings, json
# ... the rest of what learning mode wroteThe rule file becomes the permission manifest of the skill: before installing a skill written by someone else,
read it, as one reads the permissions of a mobile application. A skill that asks for net=ALLOW|...|OUT or
expose-rw=~ says so in one line. If a new version of the script tries more, the run stops at the first access
the file does not grant. A script that starts opening connections is stopped at its import socket:
RuleModuleNotFoundError: Module named 'socket' is not allowed by a rule
A skill shipped without its rule file is not contained: the first run on the user's machine learns, and grants, whatever the script does.
The user and the administrator can restrict every skill at once. A rule file written by learning mode keeps the
include lines of the template, among them ~/.config/py-sandboxes/user-py-sandboxes.profile for every project of
the user, and /etc/py-sandboxes/global-py-sandboxes.profile for the whole node. A net=DENY written there wins over
any net=ALLOW of the skill, whatever the order, for instance to keep every skill away from the intranet:
# ~/.config/py-sandboxes/user-py-sandboxes.profile, or /etc/py-sandboxes/global-py-sandboxes.profile
net=DENY|*|10.0.0.0/8|*|OUT
net=DENY|*|172.16.0.0/12|*|OUT
net=DENY|*|192.168.0.0/16|*|OUTA skill whose rule file grants net=ALLOW|*|*|*|OUT is then refused the intranet, and the message names the rule
that refused it:
Connection to [10.255.255.1]:80 DENIED by explicit rule 'net=DENY|*|10.0.0.0/8|*|OUT' from ~/.config/py-sandboxes/user-py-sandboxes.profile(1)
Only net= works this way. A python-api=DENY: in these profiles wins only at equal specificity: a skill's
python-api=ALLOW:os.system beats a user's python-api=DENY:process-exec. python-import and expose-* have no
DENY at all.
These profiles apply only through the include lines: a rule file that drops them escapes them. Check that they
are still there when reading the manifest. Conversely, a skill's rule file must not uncomment the
include "./.local.py-sandboxes" line: that path is resolved from the current directory, not from the skill's, so a
repository the agent works in could add python-import=socket and net=ALLOW|*|*|*|OUT to the skill's rules with a
.local.py-sandboxes of its own.
This containment does not replace a static analysis of the skill, and the static analysis does not replace it. A
scanner such as semgrep reads the scripts before the skill is installed, and can reject an
obvious exfiltration, but it misses what the code builds at run time (a module name in a string, an eval, a
dependency whose new version behaves differently) and stops nothing. python-sb stops at run time what the rule
file does not grant, but says nothing of what the granted rights are used for. Use both: the analysis to decide
whether to install the skill, the sandbox to bound what its scripts can do.
The instruction in SKILL.md is only a request: nothing stops the agent from running python scripts/report.py
directly. The containment holds only if the agent harness enforces it, with a permission rule that allows the
exact python-sb command line and refuses a bare python on the skill's scripts. A rule that allows any
python-sb ... is not enough, since the agent could point to another configuration; learn=false in the
skill's rule file refuses the other options, --learn included. The
coding-agents directory packages a skill and a
pre-execution hook that refuses a bare python, for Claude Code, Codex, Gemini CLI, Cursor and Copilot CLI.
With the shebang above, python scripts/report.py ignores the first line and runs without the sandbox: the hook
refuses it, which leaves scripts/report.py, and that goes through python-sb. A shebang is out of the
hook's sight, though: scripts/report.py runs whatever interpreter its first line names, so a skill script whose
shebang says python3 is not contained. Read that first line when reviewing the skill.
The same reasoning applies to hooks and plugins of an agent written in Python: they run third-party code with the rights of the user, and the same rule file pattern contains them.
A tool exposed to an LLM does what the LLM asks, and the LLM does what its context asks, including a prompt
injected in a web page or a document. A page fetcher can be steered towards localhost or the intranet, a file
reader towards ~/.ssh, a calculator towards os.system. The tool's code is legitimate; its use is not.
Launch the server or the CLI under python-sb, so that it can only reach what its function needs, whatever it is
asked:
python-sb -m my_mcp_servernet=ALLOW|TCP|0.0.0.0/32|8000|IN # The MCP endpoint
net=ALLOW|TCP|api.example.com|443|OUT
expose-ro=./dataSee Integrating with the MCP SDK for the partial mode, where only the functions of the tools run in the sandbox, and Samples for the same two tools under ten agent frameworks.
An AI coding agent that hits a refused access is tempted to widen the rule file rather than question its own code.
Because .py-sandboxes is versioned with the code, every new right appears in the history:
git diff develop..my-branch -- '*.py-sandboxes'
git log -p -- '*.py-sandboxes'The lines that deserve a human look are learn=, os-sandbox=, include, expose-rw=, outgoing net= rules,
env= and python-api=ALLOW:. Making a human reviewer mandatory on these files (a CODEOWNERS entry, a branch
protection rule) turns the diff into a gate. The diff only covers the file: see
what the rule file does not show.
After uv lock --upgrade or any version bump, run the application or its tests under python-sb without
--learn. A new violation means a dependency now does something it did not do before: a new host, a new file, a
new subprocess. This is a detection, not a protection: a malicious dependency installed by yourself is outside the
threat model (see Weaknesses). The FAQ
gives the procedure.
A pull request from a fork brings code that the CI runs, tests included. Running them with
python-sb --pysandboxes-config=ci.py-sandboxes -m pytest keeps the secrets of the runner out of reach, since only
the variables listed by env= are visible, and blocks the network except the hosts granted. This is hostile code by
definition: a kernel provider is required.
--learn answers the question "what does this script actually touch?": the rule file it writes lists the files,
hosts, modules, variables and sensitive calls used during the run.
python-sb --learn=audit.py-sandboxes script.pyIn learning mode the Python layer allows every access while recording it. Use it on code you trust, or inside a
disposable environment (os-sandbox=qemu), never as a way to run a script you suspect.
This is the core use case: a code interpreter tool, a data analysis agent, a calculator built on sympy. The
eval-* rules check and bound the source before it runs; see Dynamically evaluated code.
The same pattern applies to a notebook downloaded from the web, or to the plugins that each customer of a SaaS uploads: one rule file per origin, and a kernel provider since the author is unknown.
Only Python is covered by the Python layer. Starting a process is denied by default, but a script granted
python-api=ALLOW:process-exec to start bash, curl or a compiled binary escapes it, and only a kernel provider
still holds. On macOS and Windows, no kernel provider exists: the Python layer runs
alone, with none or subprocess.