fuselage
Run a command with ephemeral, namespace-private filesystems derived from zip or squashfs archives. No containers, no daemons, no manual cleanup.
Overview
fuselage creates a private Linux mount namespace for a command, unpacks zip or squashfs
archives into it, then execs the command. When the command exits the mounts vanish automatically.
There is no isolation beyond the mount namespace — the process keeps its normal environment, PID space, and network.
Usage
fuselage [OPTIONS...] [--] COMMAND [ARG...]
fuselage [OPTIONS...] --run PATH [ARG...]
Options
| Option | Description |
|---|---|
--dynamic=[NAME:]FILE |
Extract FILE into a fresh, mutable directory at $FUSELAGE_DYNAMIC/NAME/ |
--dynamic-empty=NAME |
Create an empty writable directory at NAME (no archive needed) |
--static=[NAME:]FILE |
Mount or extract FILE into a read-only directory at $FUSELAGE_STATIC/NAME/ |
--cache-static |
Convert zip --static archives to squashfs for faster subsequent runs (requires mksquashfs) |
--run PATH |
Find PATH in extracted archives and execute it |
FILE is a zip file, a squashfs image (.sfs), an ELF binary with an embedded squashfs image, or a
text file containing base64-encoded zip or squashfs data.
NAME defaults to the filename stem (e.g. my-data.zip → my-data, my-data.sfs → my-data).
All archive names must be unique; use NAME: to disambiguate.
NAME may also be an absolute path of the form /run/fuselage/NAME (one component under that prefix),
causing the archive to be mounted at that fixed location. This allows pre-built environments with
hardcoded paths (e.g. Python venvs) to work correctly. Fixed-path mounts require setuid-root mode.
Squashfs images are the most efficient format for --static archives: in setuid-root mode they are
loop-mounted read-only directly from the file, with no extraction to disk. Create them with mksquashfs
from the squashfs-tools package.
Environment variables set for the child process
| Variable | Set when | Value |
|---|---|---|
FUSELAGE_TMPDIR |
Always | Ephemeral scratch directory |
FUSELAGE_DYNAMIC |
Any --dynamic |
Parent of all dynamic extractions |
FUSELAGE_STATIC |
Any --static |
Parent of all static extractions |
Examples
# Ephemeral scratch space
# Cached SDK + throwaway working copy
# Run an executable from inside an archive
# Build a Python venv at a fixed path, then capture it as a squashfs
# Run the captured venv (loop-mounted at the same fixed path)
fuselage-bundle
fuselage-bundle packages a squashfs archive and a baked-in fuselage invocation
into a single self-executing ELF binary that can be distributed and run directly.
The resulting myapp binary locates fuselage on PATH, mounts its own embedded
squashfs at /run/fuselage/myapp, and runs the Python module. Pass --keep to
retain the intermediate build directory for inspection.
uv-bundle
uv-bundle packs a uv-managed Python project into a
single self-executing ELF binary, running the whole pipeline for you: it creates a
private tmpfs with fuselage --dynamic-empty, installs the project's dependencies
into it with uv sync, compresses it with mksquashfs, and packs the result with
fuselage-bundle.
By default the bundle runs python -m <package>, where the package name is read
from pyproject.toml (override the module with --module=MOD). To run a
[project.scripts]
console script directly instead, use --script=NAME; it runs .venv/bin/NAME
rather than python -m, and is mutually exclusive with --module:
Other options: --dev (include dev dependencies), --squashfs=PATH (keep the
intermediate squashfs), --uv-arg=ARG (forward an argument to uv sync,
repeatable), and --verbose (print the squashfs tree and sizes).
uv-bundle always builds against the system Python (it sets
UV_PYTHON_PREFERENCE=system), not a uv-managed download, so the bundled venv
references a stable interpreter path rather than one under the build user's home
directory. Before building, the system Python is checked against
[project].requires-python; a mismatch is an error unless
--ignore-version-mismatch is given.
Requires fuselage (setuid), fuselage-bundle, uv, mksquashfs, and gcc.
Privilege model
setuid-root mode
fuselage is designed to run setuid-root. It needs CAP_SYS_ADMIN to create a
mount namespace, and drops privileges back to the real user before execing the
child — so the child process runs as the invoking user with no UID remapping.
Unprivileged mode
Not all users will be comfortable with the setuid-root requirement. For that
reason, fuselage has a fall-back strategy that does not require setuid-root.
In this case it falls back to a user namespace (unshare --user --mount --map-root-user), which works on most Linux distributions but will remap the
process to uid 0 inside the namespace.
This allows you to use fuselage in a wide variety of scenarios where the
apparent user-id does not matter. However, tools like sudo will behave
unexpectedly in this mode.
Installation
Several installation methods are available. None requires root to install,
but setting the setuid bit (for the recommended operating mode) always does.
cargo install (builds from source)
Requires the Rust toolchain — install via rustup.rs if needed.
# And make it setuid-root (optional).
curl | bash
A convenience one-liner for casual use (installs to $HOME/.local/bin setuid-root
by default). See install.sh security notice before using.
|
Or without setuid (UID remapping fallback):
| FUSELAGE_SETUID=0
Pre-built binary via cargo binstall
Requires cargo-binstall. Downloads a pre-built binary from the GitHub release and verifies its SHA256 checksum before installing. Falls back to compiling from source if no pre-built binary is available for your target.
# And make it setuid-root (optional).
Manual download
Download a pre-built binary directly from the releases page and install setuid-root as shown below.
# Download to (say) ~/.local/bin/fuselage for your architecture. This example
# assumes 64-bit Intel and moves the binary to ~/.local/bin, which is typically
# on your $PATH. IMPORTANT: replace the version number with the current release.
# setuid-root for normal setup (optional).
Building from source
Requires Rust stable (edition 2024, Rust ≥ 1.85) and a Linux kernel with user namespace support (most distributions enable this by default).
# binary at target/release/fuselage
Run the test suite:
Or via just:
Further reading
- Combining fuselage with herescript — embed a filesystem payload directly inside an executable script
License
This project is licensed under the MIT License.