senv
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 machine
A 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.
Why senv?
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.
Install
curl -fsSL https://raw.githubusercontent.com/h5i-dev/senv/main/install.sh | sh
The 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/senv
Every one of these gives you the same Rust binary. There is no Python in senv; the PyPI package is a delivery mechanism, and it is worth installing as a tool rather than into a project's environment — an environment that can rewrite the binary confining it is not confined by it.
Building from source needs a C toolchain and OpenSSL headers (libssl-dev on
Debian/Ubuntu). Add --features vendored-openssl to build OpenSSL from source
instead, which is what the published binaries do.
senv 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 doctor
Quick start
Start a new project
mkdir my-project && cd my-project
senv init --python 3.13
senv add requests
senv run python -c 'import requests; print(requests.__version__)'
Adopt an existing uv project
cd my-project
senv init # keeps pyproject.toml and uv.lock as-is
senv sync
senv run pytest
If 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.
Everyday commands
| 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.
What senv enforces
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 |
Sandboxed installs
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.
A read-only runtime environment
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.
Allow only what your program needs
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 --suggest
senv 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.
Policy changes require trust
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.
Configuration
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 code
Inspect the boundary
senv 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 it
Every 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.
How it works
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. senv doctor and
senv status report platform-specific gaps—for example, macOS has no seccomp
and cannot enforce Linux cgroup memory or process limits.
Security boundaries and limitations
- 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.
- macOS Python startup can vary by interpreter. Apple's system Python may
recompile imports on each run because its bytecode cache is outside the
managed environment.
senv init --python 3.13selects a managed interpreter without that behavior. - 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.
License
Apache-2.0. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file h5i_senv-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: h5i_senv-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 3.8 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5d68514bea3913cb5ed2bfd9353078a8d69282fc1e9c62b20d34614b26e0fe9
|
|
| MD5 |
faa649f2ef01334dbfe7970c63eb1fdd
|
|
| BLAKE2b-256 |
49a7cd7bec805744e0a59c3b301a05bdde87ead767db9ef1db944639cc768461
|
Provenance
The following attestation bundles were made for h5i_senv-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yaml on h5i-dev/senv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
h5i_senv-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
b5d68514bea3913cb5ed2bfd9353078a8d69282fc1e9c62b20d34614b26e0fe9 - Sigstore transparency entry: 2466626596
- Sigstore integration time:
-
Permalink:
h5i-dev/senv@64e22f49cd893a624886fbf396c89ecc5e86862a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/h5i-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@64e22f49cd893a624886fbf396c89ecc5e86862a -
Trigger Event:
push
-
Statement type:
File details
Details for the file h5i_senv-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: h5i_senv-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 3.8 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9f5a3cfb20522242d395fb35a8467b6c2a79e677a06c4af728378e56f24852f
|
|
| MD5 |
784e8fe0f75c461bfb48d6d43445d37c
|
|
| BLAKE2b-256 |
7398c5e71a18b4e4ac7f8198394a3c6f940b591e91e24a460eac35d19fc223ae
|
Provenance
The following attestation bundles were made for h5i_senv-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:
Publisher:
release.yaml on h5i-dev/senv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
h5i_senv-0.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
e9f5a3cfb20522242d395fb35a8467b6c2a79e677a06c4af728378e56f24852f - Sigstore transparency entry: 2466626633
- Sigstore integration time:
-
Permalink:
h5i-dev/senv@64e22f49cd893a624886fbf396c89ecc5e86862a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/h5i-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@64e22f49cd893a624886fbf396c89ecc5e86862a -
Trigger Event:
push
-
Statement type:
File details
Details for the file h5i_senv-0.1.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: h5i_senv-0.1.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.7 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72d9b7430cfaa68646fc58323c68aa0da81616b57185397dc30f4c1c906a3ec8
|
|
| MD5 |
f95d5de14252ab445cbf1627beb5dff7
|
|
| BLAKE2b-256 |
bf463b56a642f4a514dfdf0b39436fda7d08e5dd3cd5e83422aed6d58246295e
|
Provenance
The following attestation bundles were made for h5i_senv-0.1.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yaml on h5i-dev/senv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
h5i_senv-0.1.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
72d9b7430cfaa68646fc58323c68aa0da81616b57185397dc30f4c1c906a3ec8 - Sigstore transparency entry: 2466626664
- Sigstore integration time:
-
Permalink:
h5i-dev/senv@64e22f49cd893a624886fbf396c89ecc5e86862a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/h5i-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@64e22f49cd893a624886fbf396c89ecc5e86862a -
Trigger Event:
push
-
Statement type: