lash-box
lash-box runs a command inside the restrictions a security profile
describes — filesystem scope, network policy, resource limits, environment rules —
and then gets out of the way.
It works from any shell. No daemon, no ~/.lash, no lash process:
lash-box --profile ci -- npm iYou do not have to adopt lash to use it. If you want a fenced-in npm i in a
bash script or a CI job, that is the whole setup.
How it runs your command
Section titled “How it runs your command”lash-box applies the profile to itself and then execs your command. It never
forks. The process id stays the one your shell spawned, so job control, signals,
exit status and pipes all behave exactly as if lash-box were not there:
lash-box --profile ci -- sleep 30 &kill %1 # works: %1 is the sleep, not a wrapperThe -- rule
Section titled “The -- rule”A -- anywhere means the rest is the command to run. Without one, the first
word is a subcommand:
lash-box profile list # prints the profile tablelash-box --profile dev -- profile list # runs the `profile` binary, sandboxedOptions
Section titled “Options”| Option | Meaning |
|---|---|
--profile NAME | Profile to apply. Defaults to $LASH_PROFILE, then your configured default, then the persisted one, then default. |
--force-env KEY=VALUE | Force an environment variable. Repeatable. |
--allow-rw PATH | Add a read-write path. Repeatable. |
--tun-fd N | Use an inherited TUN device for network interception. |
--explain | Print the resolved container before running. |
--warn-only | Run even when the kernel cannot enforce what the profile asks for. |
-h, --help | Show usage. |
--version | Show the version. |
Seeing what a profile will do
Section titled “Seeing what a profile will do”--explain prints the container before the command runs, which is the fastest
way to answer “why did my build fail in here”:
$ lash-box --explain --profile ci -- trueprofile: ci read-only: /usr, /lib, /etc read-write: /tmp allow hosts: registry.npmjs.org, github.com allow ports: 443 network: blocked except the rules above strip env: AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, LD_PRELOAD max cpu: 60sWhen the kernel cannot deliver
Section titled “When the kernel cannot deliver”If a profile asks for something the running kernel cannot provide — Landlock on
an old kernel, a memory cap on macOS — lash-box refuses rather than running
your command unconfined:
$ lash-box --profile ci -- npm ilash-box: profile requires enforcement this system cannot provide: filesystem sandboxing: Landlock unavailable (kernel too old or not enabled)Refusing to run unconfined. Re-run with --warn-only to override.This is deliberately stricter than the interactive shell, which warns once and carries on. At a prompt someone is watching and can decide; in a CI job nobody is, and silently dropping the sandbox is the wrong default.
Pass --warn-only when you want it to proceed anyway.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
2 | The command line was malformed. |
126 | The profile or the container could not be established. |
127 | The command was not found. |
| anything else | Your command’s own exit status. |
126 and 127 are kept distinct on purpose: a sandbox that failed to build
should never look like a typo in a command name.
An unknown profile name is a 126, never a silent fall back to running
unconfined:
$ lash-box --profile prod-ci -- ./deploy.shlash-box: unknown profile 'prod-ci'. Run 'lash-box profile list' to see what is available.Without a lash config
Section titled “Without a lash config”The built-in profiles (default, developer, script, ci, restricted)
resolve even with no ~/.lash directory, so a container image that has never
run lash can still do:
lash-box --profile ci -- makeProfiles you write in ~/.lash/profiles/ take precedence over the built-ins of
the same name.
The network proxy
Section titled “The network proxy”A profile with allow_hosts needs a proxy running for the whole session to
filter by hostname. That proxy is lash-box too:
lash-box proxy --profile ciIt prints the port it bound on stdout, then streams audit records for every allowed and denied connection. It exits when its stdin closes, so the daemon that started it cannot leave one behind.
You rarely run this yourself — the daemon starts one per session — but it is the same binary and the same profile.
Not a security boundary
Section titled “Not a security boundary”lash-box takes the profile name from its command line and runs as you. Anyone
who can run it can run lash-box --profile default -- anything. The
restrictions are ones it applies to itself before exec — nothing here is
setuid.
It is a tool for containing what you chose to run, not for containing someone else.
See also
Section titled “See also”- Security Profiles — the profile file format
- Security and Audit — how enforcement fits together