Sandboxed Python environments, with the workflow of uv.
senv adds an OS-level security boundary to Python environments. It keeps
the familiar uv workflow while isolating
dependency installation and application execution from your credentials,
network, and the rest of your machine.
| 📦 Install packages with registry-only network access | 🔒 Run Python with no network by default |
| 🧊 Keep the environment read-only while code runs | 🧾 Review policy, denials, and execution receipts |
senv sync # install dependencies inside the install sandbox
senv run pytest # run code with no network and a read-only environment
senv shell # enter the same boundary interactively
senv status # see exactly what is enforced on this machineA senv project is still a uv project. It uses the same pyproject.toml and
uv.lock, and teammates without senv can continue using uv directly.
venv and uv isolate dependencies, but they do not isolate code. A package
inside a virtual environment can still:
- read files such as
~/.ssh; - access environment variables and credentials;
- connect to arbitrary network destinations; and
- execute build code during installation.
A virtual environment is a PATH convention, not a security boundary. senv
puts a sandbox underneath the Python workflow, with separate policies for the
two moments that carry different risks:
- Install: package build code can reach approved registries, but not your source tree or credentials.
- Run: your project is writable, but the installed environment is read-only and network access is denied by default.
curl -fsSL https://raw.githubusercontent.com/h5i-dev/senv/main/install.sh | shThe script picks the build for your platform, checks it against the SHA-256
published beside it, and installs to /usr/local/bin. Set SENV_INSTALL_DIR
to install elsewhere, or SENV_VERSION=vX.Y.Z to pin a version.
Or install it the way you install your other tools:
uv tool install h5i-senv # also: pipx install h5i-senv, pip install h5i-senv
cargo install --git https://github.com/h5i-dev/senvsenv requires uv on PATH.
- Linux: registry allowlisting during installation also requires
slirp4netnsandnftables(sudo apt install slirp4netns nftableson Debian/Ubuntu). - macOS: no additional sandbox runtime is required; senv uses the built-in Seatbelt sandbox.
- Windows: use WSL2.
Check what your machine can enforce:
senv doctormkdir my-project && cd my-project
senv init --python 3.13
senv add requests
senv run python -c 'import requests; print(requests.__version__)'cd my-project
senv init # keeps pyproject.toml and uv.lock as-is
senv sync
senv run pytestIf the project already has a .venv, senv leaves it untouched because those
packages were installed outside its boundary. It creates a separate managed
environment and explains how to switch. Use senv init --replace-venv when
you are ready to replace the existing link.
| What you normally do | With senv |
|---|---|
uv sync |
senv sync |
uv lock |
senv lock |
uv add requests |
senv add requests |
uv run pytest |
senv run pytest |
source .venv/bin/activate |
senv shell |
| another uv command | senv uv -- <args> |
Flags are passed through to uv, and command exit codes pass back to the caller.
This makes commands such as senv run pytest suitable for CI as well as local
development.
Installing dependencies and running your code use different policies:
senv sync / add / lock |
senv run / shell |
|
|---|---|---|
| Network | PyPI and configured indexes only | denied by default |
| Project source | read-only | read-write |
| Python environment | writable for installation | read-only |
| Credentials | unavailable | only explicitly declared secrets |
| Resource limits | CPU, file size, wall clock; memory and processes on Linux | CPU and file size; memory and processes on Linux |
Supply-chain attacks can execute before a package is ever imported. For example, an sdist build backend runs during installation with the user's permissions.
senv resolves and installs dependencies in a staging area containing the project manifests but none of the project source. Network access is restricted to package registries and pinned at the network layer.
While your code runs, installed packages cannot rewrite the environment to
persist into the next run. The managed environment lives outside the project,
and .venv points to it without exposing a writable parent directory.
senv also compiles bytecode during installation and disables writable bytecode
caches at runtime. This prevents a package from leaving behind a modified
.pyc file that Python could prefer over the protected source.
When senv blocks an operation, it shows what happened and the narrowest command that would permit it:
senv blocked 1 operation(s):
• network access to api.stripe.com
allow it with: senv allow api.stripe.com
You can make the grant persistent or apply it to one command:
senv allow api.stripe.com
senv run --allow-net api.stripe.com pytest
senv report --suggestsenv allow records the change in senv.toml. senv report --suggest only
prints a proposed policy stanza; it never changes the policy for you.
Some denials are intentional guarantees, so senv does not suggest bypassing
them. Runtime code cannot make the environment writable or read ~/.ssh.
The policy file, senv.toml, lives inside the project. Because runtime code can
write the project, a compromised dependency could try to widen that policy for
future commands.
senv therefore keeps the last accepted policy snapshot outside the sandbox:
- an unchanged or narrower policy runs normally;
- a wider policy is refused until you inspect and accept it with
senv trust.
senv: senv.toml grants more than senv recorded, so nothing was run
the policy on disk is wider than the one you last accepted:
+ [run] net: deny → unrestricted
+ [run.env] pass: added AWS_SECRET_ACCESS_KEY
→ if you made this change, run `senv trust`; if you did not,
inspect senv.toml and your recent dependencies first
The same check applies when senv first sees a project whose policy is wider
than the defaults. Changes to pyproject.toml sections that control install
behavior—[build-system] and [tool.uv]—also require trust.
Two settings receive additional protection:
- a
command:secret source requiresallow-command-secrets = true, because the command executes on the host; - the configured uv is rejected if it lives inside the project or senv state, where sandboxed code could rewrite it.
senv.toml is optional. Without it, senv uses fail-closed defaults. The file is
created when you first add a grant.
[env]
python = "3.13"
isolation = "auto" # auto | process | supervised | container | microvm
allow-command-secrets = false # permit command: secret sources on the host
[install]
extra-indexes = ["download.pytorch.org"]
cache = "project" # "shared" saves disk but trades away isolation
[run]
net = "deny" # "deny" | "host" | ["api.example.com", "*.s3.amazonaws.com"]
[run.fs]
read = ["~/datasets"] # additional read-only paths
write = [] # project and scratch space are already writable
[run.resources]
mem = "4G"
wall = "30m" # use "none" for a dev server
[secrets.OPENAI_API_KEY]
source = "env:OPENAI_API_KEY" # env:… | file:… | command:…
phases = ["run"] # secrets are never exposed to install-time build codesenv status # resolved policy, isolation tier, grants, limits, and digest
senv report # commands, denials, and redactions
senv trust # accept the current policy as the new baseline
senv doctor # enforcement available on this host
senv gc # find stale project state; add --prune to remove itEvery command appends a receipt outside the sandbox. Declared secret values are redacted, and h5i's credential scanner checks for additional keys. Each receipt includes a digest of the policy that was actually enforced.
senv uses h5i-sandbox as its confinement
engine and compiles the Python-focused senv.toml into an h5i policy.
| Platform / tier | Enforcement |
|---|---|
| Linux | Landlock filesystem allowlists, seccomp-bpf syscall filtering, namespaces, and rlimits/cgroups. Network allowlists use a private namespace and nftables rules pinned to resolved addresses. |
| macOS | Seatbelt filesystem and network confinement. Allowed egress passes through a DNS-pinned loopback proxy; other name resolution is denied. |
| Container | Optional rootless Podman isolation. |
| MicroVM | Optional VM-grade isolation with a separate kernel. |
senv never silently downgrades. If the host cannot enforce the requested policy, it refuses to run and explains what is missing.
- The default tiers share the host kernel. Landlock, seccomp, and Seatbelt
provide OS-level isolation, not a hypervisor boundary. Use
isolation = "microvm"when a separate kernel is required. - It does not identify malicious packages. Lockfile hashes and dependency review remain important.
- Run the environment through senv.
source .venv/bin/activate, an editor invoking.venv/bin/python, or any other host-side execution bypasses the runtime boundary. Usesenv runorsenv shell. - Install-time code can modify the environment. Installation must write packages and scripts. The install sandbox protects your source, credentials, and non-registry network, but it cannot make the environment itself read-only.
- Denial suggestions are hints, not proof. Kernel-tier reports infer some denials from program output, which untrusted code can influence. Review every suggested grant before accepting it. Container-tier network requests have a direct per-request tally.
- State must remain outside the project. senv refuses
SENV_STATE_DIRorSENV_CACHE_DIRlocations inside the project because runtime code could then alter environments, receipts, or trusted baselines. - Runtime wall-clock limits are not currently enforced. CPU and file-size rlimits apply everywhere; memory and process limits use Linux cgroups and do not apply on macOS.
- Policy tampering is detected, not prevented. A dependency can edit files in the writable project; senv refuses a widened policy until you trust it.
For the full threat model and design rationale, see DESIGN.md.
Apache-2.0. See LICENSE.