Run commands in a restricted user systemd sandbox
Project description
sandboxa
sandboxa runs a command inside a restricted environment scoped to a
target project directory.
It is designed to reduce accidental host exposure while still allowing practical project work.
What it does
- Launches via
systemd-run --user --pty. - Applies isolation properties:
ProtectHome=tmpfsTemporaryFileSystem=$HOME:rw,mode=700,...PrivateTmp=truePrivateDevices=true
- Sets the command working directory to the target project path.
- Binds the target project path into the sandbox.
- Optionally binds extra paths (
--extra-data-path/ YAMLextra_data_paths). - Optionally binds OpenCode configuration paths when
allow_settingsincludesopencode. - Applies
InaccessiblePathsfor YAMLexcluded_pathsentries. - Refuses to run if a marker path exists in project (default:
.profile).
Security model
- Uses
systemd-run --userisolation properties to reduce what the launched process can see from the host (ProtectHome=tmpfs, a writable tmpfs mounted at$HOME,PrivateTmp=true,PrivateDevices=true). - Grants filesystem access by explicit bind mounts only:
- target project path
- optional OpenCode config paths when
allow_settingsincludesopencode - optional
extra_data_paths
- Supports explicit deny-listing inside project scope via
excluded_paths(InaccessiblePaths) for paths that currently exist. - Adds a safety guard against accidentally sandboxing a complete home-like
directory by refusing to run when the marker path exists
(default:
.profile). - Applies strict config validation (
pydantic, unknown YAML keys are rejected) to prevent silent misconfiguration.
Non-goals
- Not a full container/VM boundary; this is a practical isolation wrapper, not a hardened multi-tenant sandbox.
- Not a replacement for system-level mandatory access controls or seccomp policy authoring.
- Not a network isolation tool; networking behavior is not managed here.
- Not a privilege-escalation mechanism; it runs as the invoking user via
systemd-run --user. - Not a generic policy engine; supported controls are intentionally narrow and centered on path exposure for local development workflows.
Requirements
- Linux with user
systemdsession support (systemd-run --user). - Python 3.
- Python packages:
ruamel.yamlpydantic
Install dependencies:
pip install pydantic ruamel.yaml
Install sandboxa:
pipx install sandboxa
Usage
./sandboxa \
[RUN_COMMAND] \
[--project-path PATH] \
[--extra-data-path PATH ...] \
[--excluded-path PATH ...] \
[--allow-settings NAME ...] \
[--refuse-if-exists NAME] \
[--config-file FILE] \
[--no-reconnect] \
[--dry-run] \
[-- COMMAND_ARGS...]
Common examples:
# Run default command (opencode) in current directory
./sandboxa
# Run bash in current directory sandbox
./sandboxa bash
# Target explicit project directory
./sandboxa --project-path /path/to/project
# Pass args to run command
./sandboxa bash -- -lc "echo smoke-ok"
# Add extra bind paths
./sandboxa --extra-data-path /data/shared --extra-data-path /mnt/cache
# Override excluded paths from CLI
./sandboxa --excluded-path build/ --excluded-path secrets/
# Do not check for existing sessions to be resumed.
./sandboxa opencode --no-reconnect
# Preview effective config and command without executing
./sandboxa bash --dry-run -- -lc "echo smoke-ok"
Configuration file
By default the script reads user config from ~/.config/sandboxa.yaml and
project config from .sandboxa.yaml in the target project directory.
Use --config-file to override the project config path; absolute paths are
supported.
Supported keys:
allow_settings: <list[string]>(see Settings Templates below)extra_data_paths: <list[path]>excluded_paths: <list[string]>refuse_if_exists: <string>
Example:
allow_settings:
- opencode
extra_data_paths: []
excluded_paths:
- build/
- secrets/
refuse_if_exists: .profile
Precedence:
- CLI arguments override YAML values.
- Project YAML values override user YAML values for most keys.
- For
allow_settingsandexcluded_paths, user and project YAML values are merged. - YAML values override built-in defaults.
Settings Templates (allow_settings)
Even in a constrained environment there is sometimes the need to use data from your real home directory. This could be the configuration of your favorite editor or the cache of your userspace package manager.
Some popular use-cases are implemented in sandboxa in order to ease the burden of finding the required files to be accessible in the sandbox.
The following contexts are supported.
Keyword for allow_settings |
Purpose | Limitations |
|---|---|---|
opencode |
OpenCode | - |
podman |
Podman | not working for root-less containers (see issue) |
ssh |
OpenSSH agent and key material | exposes ~/.ssh and optionally SSH_AUTH_SOCK if set |
vim |
VIM | without history (use extra-data_path: [~/.vim/] if you really need it) |
License
This project is licensed under GNU GPL v3 or later.
See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 sandboxa-0.2.0.tar.gz.
File metadata
- Download URL: sandboxa-0.2.0.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32a71d470fa74d51a4fef6ec82d3802097072689f44ae3a61ae240d6c6ea403d
|
|
| MD5 |
4136f00656ad8737a2ce1e1ad3d28b6d
|
|
| BLAKE2b-256 |
eb4b15bff167212ffa2b0e607f6792b8748d8880c8565034841d03e449d8bd19
|
File details
Details for the file sandboxa-0.2.0-py3-none-any.whl.
File metadata
- Download URL: sandboxa-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c6d90ec66a071e12a043a0f450ee0c40c9e5dd93567cb021219864869476859
|
|
| MD5 |
276a1b2c709aad8425e9280f2c2225a0
|
|
| BLAKE2b-256 |
da80600097df3bad77f40cc1696a2192d35ee818e7511fea7ee95c7c456d708b
|