Skip to main content

buddelkiste

Run a command in a rootless sandbox (bk): filesystem and process isolation via bubblewrap, plus optional network controls — share the host stack, cut connectivity entirely, or filter egress with pasta/slirp4netns and in-namespace nftables (IP/CIDR and hostname rules, no root required).

Usage

# Run a command in the sandbox
bk run /path/to/executable [args...]

# Inspect the sandbox (login shell)
bk run

# Catalog helpers
bk list-features
bk list-net-presets

# PATH shims for configured [executables.*] (in ~/.local/bin)
bk shims install
bk shims check

# Topic features (all on by default)
bk run --no-feature gui --no-feature google cursor-agent
bk run --feature python --feature ssh python myscript.py

# Network modes
bk run --network host …                 # share host network (default)
bk run --network none …                 # no connectivity
bk run --network filter \
  --net-policy deny \
  --net-allow 1.1.1.1/32 \
  --net-allow api.github.com \
  --net-allow '*.pypi.org' \
  --net-deny-preset metadata \
  --net-deny-preset private …

bk run wrapper options: --debug, --feature / --no-feature, --network, --net-policy, --net-allow, --net-deny, --net-deny-preset. Everything else is the sandboxed command.

Features

Optional permission sets are grouped by topic. Built-ins default to on. Toggle with CLI flags or config; see bk list-features for the live catalog (including each feature's module path).

Feature What it grants
asdf ~/.asdf, ~/.tool-versions, ASDF_DIR
cursor Cursor IDE/CLI install and state dirs
dbus session/system bus sockets, DBUS_SESSION_BUS_ADDRESS
docker ~/.docker, Docker socket / DOCKER_HOST
git ~/.gitconfig, ~/.config/git
google /opt/google
gui display, GPU, audio, fonts, related env
java OpenJDK /etc/java-*-openjdk configs
locale LANG, LC_NUMERIC, LC_TIME
node npm/nvm/bun paths, BUN_INSTALL
nvim ~/nvim
python pip/virtualenv paths and env
ssh ~/.ssh/config plus a dedicated agent with ~/.ssh/sandbox_* keys
term TERM, TERMINFO, COLORTERM, TERM_PROGRAM, EDITOR
user ~/.local (ro), ~/.cache (rw)
xdg-open host xdg-open via flatpak-xdg-utils

CLI

bk list-features

# Tighten a desktop-ish run
bk run --no-feature gui --no-feature dbus --no-feature xdg-open curl https://example.com

# Minimal toolchains for a script
bk run --no-feature cursor --no-feature google --feature python --feature git python app.py

# Allowlist-style: disable broadly in config, then enable what you need
bk run --feature ssh --feature git git fetch

Precedence (later wins for CLI flags): feature defaults → global config features → per-executable executables.<name>.features → --feature / --no-feature.

Config

Configuration is read from ~/.config/buddelkiste/config.toml, then merged with .buddelkiste.toml from the current directory or an ancestor (project values win). The project file is masked inside the sandbox (empty file bound over the path) so the sandboxed command cannot read it.

# Global allowlist (exactly these features), or use a table of overrides:
# features = { gui = false, google = false }
features = ["git", "ssh", "python", "rust", "term", "locale", "user"]

[feature.rust]
description = "Rust toolchain directories"
default = false
env = ["CARGO_HOME", "RUSTUP_HOME"]

[[feature.rust.binds]]
source = "$CARGO_HOME"
mode = "rw"

[[feature.rust.binds]]
source = "${HOME}/.rustup"
mode = "ro"

[[feature.rust.binds]]
source = "${HOME}/.cargo/registry"
mode = "overlay"   # persistent upper under $XDG_CACHE_HOME/buddelkiste/overlays/<hash>

[executables.cursor-agent]
features = ["cursor", "git", "ssh", "gui", "dbus", "xdg-open", "term"]
args = ["--force"]

[executables.cursor-agent.network]
mode = "filter"
policy = "deny"
allow = ["1.1.1.1/32", "api.github.com"]

[executables.python]
features = { python = true, git = true, gui = false }
shim = false

# shims = false  # disable PATH shims globally (per-entry shim = true still wins)

[[binds]]
source = "/path/to/directory"
mode = "ro"   # or "rw", "tmp-overlay", "overlay", "overlay:$XDG_CACHE_HOME/bk-upper"

[[binds]]
source = "${HOME}/scratch"
target = "/mnt/scratch"
mode = "tmp-overlay"   # writable in the sandbox; host tree unchanged

[[envvars]]
name = "MYENVVAR"
value = "example value"   # omit value= to take it from the process environment

[network]
mode = "filter"
policy = "deny"
allow = ["1.1.1.1/32", "api.github.com", "*.pypi.org"]
deny = ["203.0.113.0/24"]
deny_presets = ["metadata", "linklocal", "corp"]

[network.presets]
corp = ["10.50.0.0/16", "*.internal.example.com"]

Bind mode values:

Mode Meaning
ro read-only bind (default)
rw read-write bind
tmp-overlay overlayfs; writes are ephemeral
overlay overlayfs; upper under $XDG_CACHE_HOME/buddelkiste/overlays/<sha256(source)>
overlay:<path> overlayfs with an explicit upper path ($VAR / ${VAR} / ~ ok)

Paths in source, target, and overlay:<path> may use $VAR / ${VAR}.

The current working directory must be visible inside the sandbox. If it is not covered by any bind, bk asks whether to whitelist it for this run or permanently (appends a [[binds]] entry). Non-interactively it errors instead.

PATH shims

For each [executables.<name>] entry with shimming enabled, bk shims install writes a small sh wrapper into $XDG_BIN_HOME (default ~/.local/bin). The shim resolves the real binary (skipping its own directory) and runs bk run <real> "$@". Shims include a # buddelkiste-shim: marker so later installs can refresh our own files without overwriting unrelated binaries.

Toggle with a global shims boolean (default true) and optional per-entry shim = true/false overrides. Disabled entries remove a managed shim on the next install.

shims = true

[executables.cursor-agent]
features = ["cursor"]

[executables.python]
shim = false
bk shims install   # create/update/remove shims per config
bk shims check     # warn if an original binary appears earlier on PATH

Ensure ~/.local/bin is early on your PATH. bk shims check exits non-zero when something would bypass the sandbox.

Python features (entry points)

Built-ins and third-party packages register features under the buddelkiste.features entry-point group. The entry point value must be a Feature instance, or a zero-argument callable that returns one. When loaded, name and origin are set from the entry-point name and value (e.g. rust / mypkg.features:RUST).

Feature fields:

Field Type Default Meaning
name str (required) Feature id used in CLI/config (--feature, features = [...]). Overwritten by the entry-point name at load time.
description str (required) One-line summary shown by bk list-features.
default bool True Whether the feature is enabled before config/CLI overrides.
env_vars tuple[str, ...] () Host env var names to forward into the sandbox when the feature is on.
binds Callable[[], list] lambda: [] Zero-arg callable returning bind objects (ROBindConfig, RWBindConfig, DevBindConfig, overlay configs, Tmpfs, or raw bwrap arg tuples). Called each run.
setup Callable[[], AbstractContextManager[Sequence[str]]] | None None Optional factory returning a context manager. Entered while the sandbox runs; its yielded sequence is appended as extra bwrap args (binds, --setenv, …). Use for sockets/agents that need lifecycle.
origin str "" Shown in bk list-features. Overwritten by the entry-point value at load time (e.g. mypkg.features:RUST).
# mypkg/features.py
from collections.abc import Iterator, Sequence
from contextlib import contextmanager
from pathlib import Path

from buddelkiste.binds import ROBindConfig, RWBindConfig
from buddelkiste.features import Feature


def rust_binds() -> list:
    home = Path.home()
    return [
        RWBindConfig(home / ".cargo"),
        ROBindConfig(home / ".rustup"),
    ]


@contextmanager
def rust_setup() -> Iterator[Sequence[str]]:
    # Optional: start helpers, yield extra bwrap args, clean up on exit.
    # Built-in ssh uses this pattern for a dedicated ssh-agent.
    yield []


RUST = Feature(
    name="rust",  # replaced by entry-point name "rust" when loaded
    description="Rust toolchain directories",
    default=False,
    env_vars=("CARGO_HOME", "RUSTUP_HOME"),
    binds=rust_binds,
    setup=rust_setup,  # or omit / None
    origin="mypkg.features:RUST",  # replaced by entry-point value when loaded
)
# pyproject.toml
[project.entry-points."buddelkiste.features"]
rust = "mypkg.features:RUST"

After install, bk list-features shows the entry and its origin. Pure-TOML [feature.<name>] covers env/bind cases without a package; use Python entry points for custom bind logic or setup hooks. Config feature names must not collide with an existing entry point.

Network filter dependencies

network.mode = "filter" needs pasta (from passt) or slirp4netns, plus nft and setpriv. No root and no reserved host subnets are required. Hostname rules start a DNS proxy that redirects UDP/53 and updates dynamic nftables allow sets from resolved A/AAAA records. Nameserver IPs from /etc/resolv.conf are auto-allowed under a default-deny policy.

Development

uv sync --group dev
uv run bk --help
uv run pytest                 # unit + nested-safe integ + coverage; TUN e2e skipped if unavailable
uv run pytest -m integration  # real bwrap host/none + nested nft
uv run pytest -m requires_tun # filter/pasta e2e (needs /dev/net/tun)

Integration layout:

  • tests/ — unit/contract tests (mocked subprocess where needed)
  • tests/integ/ — nested-safe real bwrap (host/none), binds/env isolation, nested nft + DNS proxy/nft add element/UDP/53 redirect against real tools
  • tests/integ_net/ — filter-mode e2e via pasta/slirp (allow/deny IP & host, guest caps); skipped without /dev/net/tun

Metadata

Release files for buddelkiste 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for buddelkiste 0.1.0
File Size Uploaded
buddelkiste-0.1.0.tar.gz 84.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for buddelkiste 0.1.0
File Interpreter ABI Platform
buddelkiste-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 120.0 kB

Release files / buddelkiste-0.1.0.tar.gz

Download URL buddelkiste-0.1.0.tar.gz
Size 84.1 kB
Tags Source
SHA-256 checksum
How to use checksums
52236aba532974e74750ad874fae83b292f468a7083cec8cbe0ff1c89a729f74
BLAKE2b-256 checksum
How to use checksums
ba7c129d5095ffb04c6253cee0c1dd71ed449ecd73ccdd328e92ed2ca97a6306
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / buddelkiste-0.1.0-py3-none-any.whl

Download URL buddelkiste-0.1.0-py3-none-any.whl
Size 35.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c5d27ea23b4c55bbf4d8f7b8559ae859d08c64334f2e4ca5ba5bc4376b22d45c
BLAKE2b-256 checksum
How to use checksums
cc1d8f68268bf27e0e552851d46af514400498b5c9a6f4e341f0bf4726fecc7f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page