Run any project - or a coding agent in full YOLO mode - in a locked-down container that can't read your secrets, reach outside the repo, or phone home. sluice runs it as a non-root user, seeing only that project directory, behind a default-DROP egress firewall (only the hosts you allow, by name, are reachable) - and ends every run with a receipt of exactly what it reached and what the firewall blocked.
Drop a sluice.config.sh in a directory and run sluice, or just run sluice and let it
detect the stack, scaffold the config, and build + run it. You declare what the project
needs, what it may reach, what ports it serves, and the command to run.
Runs entirely on your machine - no account, no telemetry, nothing uploaded. The only
network call sluice itself makes is an opt-out check to GitHub for a newer release
(SLUICE_NO_UPDATE_CHECK=1 to disable).
brew install Pyronewbic/tap/sluice
# dev stream (latest main commit): brew install --HEAD Pyronewbic/tap/sluice
# or: curl -fsSL https://raw.githubusercontent.com/Pyronewbic/Sluice/main/install.sh | sh
# or, from a checkout: ./install.shThe installer symlinks bin/sluice into ~/.local/bin (ensure it's on PATH). Needs
docker or podman to build and run (sluice init needs neither).
Sandbox a coding agent - non-root, sees only this repo, behind the egress firewall, so YOLO mode is defensible:
brew install Pyronewbic/tap/sluice # needs docker or podman
export ANTHROPIC_API_KEY=sk-ant-... # forwarded into the sandbox, never baked into the image
cd your-project
sluice agent claude # also: codex, gemini, cursor, aider, opencode, amp, qwen, crushOr run any project sandboxed, then see where its egress hit a wall:
cd your-project
sluice # detect the stack, scaffold a config, build + run it sandboxed
sluice egress # what it reached vs. what the firewall blockedUpdate the sluice CLI itself (not to be confused with sluice update, which rebuilds your
project's sandbox) via your installer:
brew upgrade sluice # stable
brew upgrade --fetch-HEAD Pyronewbic/tap/sluice # dev stream (latest main)
curl -fsSL https://raw.githubusercontent.com/Pyronewbic/Sluice/main/install.sh | sh # script install: re-run to git-pullsluice version flags a newer release when one is out (SLUICE_NO_UPDATE_CHECK=1 skips the check).
From anywhere inside a project with a sluice.config.sh (found by walking up, like git
finds .git), sluice builds and runs it sandboxed. No config yet? Just run sluice: it
detects the stack, scaffolds one, shows what it'll do, and on confirm builds and runs it.
cd any-repo
sluice # no config -> scaffold from detection, then (on [Y/n]) build + run
sluice learn # review the hosts the proxy blocked; allow the ones you pick - live, no rebuildWhen a run exits, sluice prints an egress receipt: the hosts it reached (with hit counts)
and any it tried but the firewall blocked - so you see at a glance everything your code or
agent talked to, and a failed fetch points you straight at sluice learn. sluice egress
shows the same per-host on demand, and sluice doctor reports the engine, the mounted project dir,
image freshness, published ports, the effective allowlist, auth env, and the hosts your last run was
blocked from - even before anything is built.
learn is a per-host review, not a rubber-stamp: for each blocked host you allow / skip /
collapse to a .domain wildcard, so a telemetry or exfil host stays blocked while the real
dependency goes through. Picks are written to the config and applied live (no rebuild); --apply
takes them all, --print emits the list for CI. For trusted code whose fetcher aborts on the first
blocked host, sluice learn --audit discovers the full list in one credential-stripped,
egress-open run (loudly warned; see THREAT_MODEL.md). Full walkthrough in the
gallery.
sluice init does just the scaffold step (no prompt, no run); in CI, a bare sluice scaffolds and
stops unless SLUICE_YES=1. It infers the stack, run command, and ports for Node, Python, Deno,
Ruby/Rails, Rust, Go, Java, PHP, .NET, Elixir, and Dart; learn then fills the one thing you can't
guess statically - the egress allowlist. Any other language runs too: set SLUICE_EXTRA_PKGS +
SLUICE_RUN_CMD and the generic base handles the rest (or it sources a Procfile/Makefile run target).
The commands you'll reach for:
sluice # build (if needed) + run SLUICE_RUN_CMD in the sandbox
sluice agent <name> # run a coding agent (run `sluice agent` with no name to list them)
sluice init [--force] # scaffold a sluice.config.sh from the detected stack (--update to re-detect)
sluice learn # propose the egress allowlist from blocked hosts (--print | --apply | --audit)
sluice run <cmd...> # an ad-hoc command instead of SLUICE_RUN_CMD
sluice doctor # health check: engine, image, allowlist, blocked egress (--json)
sluice lock # inventory apk+npm+pip+gem+go+cargo to sluice.lock (--check | --diff | --enforce | --sbom | --scan)Plus build / rebuild / update / stop / rm / prune for lifecycle and shell / ls /
egress / logs / smoke to inspect - sluice help lists them all.
Pre-1.0: the command surface is still stabilizing and may change before the 1.0 lock.
sluice doctor shows the whole posture at a glance - engine, image freshness, the effective
allowlist, and what the firewall blocked this run:
sluice doctor
engine Docker version 27.4.0
config ~/code/blog/sluice.config.sh
desc personal blog
mount ~/code/blog
image sluice-blog built (config current)
lock in sync (142 pkgs)
allowlist api.anthropic.com
base: github.com api.github.com registry.npmjs.org ...
egress 1 host(s) blocked (last run) - run 'sluice learn' to allow:
cdn.tracking.example
sluice ls shows every box on this machine, which one you're in (*), and its security posture -
allowlist size, published ports, and whether it's locked:
sluice boxes
NAME STATUS STACK ALLOW PORTS LOCK PATH DESCRIPTION
* sluice-blog running node/astro 3 4321 locked ~/code/blog personal blog
sluice-api built python 7 8000 - ~/code/api internal API
ALLOW/PORTS come from build-time labels, so they show - until the box is next rebuilt
(sluice rebuild/update). sluice ls --egress adds a BLOCKED column - a live count of
denied-but-unallowed hosts per running box (it execs into each, so it's opt-in and slower).
Filter with sluice ls --running, --orphans (boxes whose project dir was deleted, marked (gone)),
or --stack <name>. sluice prune --orphans removes just the orphans. To act on a box from anywhere
without cd-ing into its project, put -b <name> (the short slug or full sluice-<name>) before the
command, e.g. sluice -b api egress or sluice -b api shell; it echoes the target to stderr.
ls, doctor, and egress all take --json for scripting and CI - e.g. sluice egress --json
emits the box's reached-vs-blocked hosts as a machine-readable audit record.
sluice lock writes a committable sluice.lock - a full inventory of the image (every apk, npm,
pip, gem, go, and cargo package with its version and digest) so what's in your sandbox is reviewable
in a diff, and sluice doctor flags drift. It's an audit artifact, not a reproducibility guarantee
(Wolfi's apk repo is rolling). --check turns drift into a CI gate (--enforce is the strict
variant: it refuses to build or to tolerate a stale image), --diff reviews it locally, --sbom
emits a deterministic SBOM (CycloneDX 1.6 or SPDX via --format), and --scan vuln-checks
the box with a host Grype/Trivy (--fail-on <severity> to gate CI):
sluice lock --check # fail the build if the sandbox drifted from sluice.lock
sluice lock --sbom > sbom.cdx.json # CycloneDX inventory (apk/npm/pip/gem/go/cargo purls), byte-stable
sluice lock --scan --fail-on high # vuln-scan the box; non-zero exit on a high+ CVE (needs host grype/trivy)Image and container are named per project (sluice-<dir>, or SLUICE_NAME), and the image
auto-rebuilds when sluice.config.sh or the core changes.
sluice agent <name> drops you into a coding agent that's non-root, sees only this repo,
and can only reach its own model API - so running it in YOLO mode is defensible:
export ANTHROPIC_API_KEY=sk-ant-... # forwarded into the sluice, never baked into the image
cd my-repo
sluice agent claude # Claude Code, --dangerously-skip-permissions, sandboxed
sluice agent claude -p "fix the test" # one-shot: args after the name are forwarded to the agentPresets ship for claude, codex, gemini, aider, cursor, opencode, amp,
qwen, and crush (see agents/); each is a normal sluice.config.sh declaring the
tool, its API hosts, and which auth env var to forward - so adding an agent is just adding a file. Run
sluice agent with no name to list them (each with its auth var and whether it's set on your host).
If the agent hits a blocked host, sluice learn surfaces it.
Each agent runs in the project's box (named for the directory), so a repo holds one agent at a
time - sluice agent codex in a repo already set up for claude reuses the claude config (sluice
says so). To run several agents in parallel, give each its own checkout or a git worktree (sluice
mounts the git common dir, so each worktree gets an isolated box):
git worktree add ../myrepo-codex
cd ../myrepo-codex && sluice agent codex # isolated box + branch, separate from the claude oneSessions persist across runs (via SLUICE_STATE_DIRS, kept in a per-project host store under
~/.local/state/sluice/), so sluice agent claude resumes where you left off and survives a
rebuild or reboot; sluice doctor shows what's persisted.
Each preset runs the agent in YOLO mode by default (its skip-approvals flag), since the
sluice is the point of the per-action gate being unnecessary. Honest caveat: the sandbox
bounds the blast radius but does not zero it - a YOLO agent can still rewrite the mounted
repo and use any creds you forward, and the allowlist is host-granular. Work on a committed
branch, and see THREAT_MODEL.md for exactly what is and isn't contained.
Everything is driven by sluice.config.sh. Copy sluice.config.example.sh
- it documents every knob - and edit. The ones you'll reach for most:
| knob | purpose |
|---|---|
SLUICE_RUN_CMD |
the command a bare sluice runs (default: a shell) |
SLUICE_EXTRA_PKGS |
extra apk packages baked in at build time |
SLUICE_ALLOW_DOMAINS |
runtime egress domains, on top of the base allowlist |
SLUICE_ALLOW_IPS |
runtime egress IPs/CIDRs for non-HTTP services |
SLUICE_PORTS |
TCP ports to publish (firewall opens a matching inbound rule) |
SLUICE_ENV |
host env var names to forward into the session |
The rest - build-time setup, a central egress policy (SLUICE_POLICY_URL), scoped TLS
interception (SLUICE_BUMP_DOMAINS/SLUICE_BUMP_URLS), persisted state, credential staging
(SLUICE_PRELAUNCH) - are documented inline in sluice.config.example.sh.
sluice.config.sh is sourced by /bin/sh (Docker build), bash (firewall, host), so keep
it POSIX-safe: space/newline-separated strings, no bash arrays.
Credential plumbing (token files, minted cloud tokens, etc.) stays in each project's
config via SLUICE_PRELAUNCH + SLUICE_ENV/SLUICE_MOUNTS - the core stays generic.
The guardrail that makes running untrusted code defensible:
- Default-DROP egress, hostname-filtered. All HTTP/HTTPS is forced through an in-sluice
proxy (squid) that allows by Host / TLS-SNI - spliced, never decrypted - so the
decision is by domain and survives IP rotation. Only the base hosts (npm/yarn
registries, GitHub git/release hosts) plus
SLUICE_ALLOW_DOMAINSare reachable;SLUICE_ALLOW_IPSadds direct egress for non-HTTP services (scope it withip:port). IPv6 and direct-IP are blocked, and DNS for a non-allowlisted name is answered locally (a dead sink, never forwarded) so data can't tunnel out as DNS labels. The firewall self-tests at boot (a denied host must fail; a base host must work). - Non-root (uid 1000) with only
NET_ADMIN/NET_RAW; no Docker-in-Docker. - Filesystem isolation: only the project dir is mounted (plus its git common dir when it's a worktree). The sluice can't see the rest of your machine.
- The allowlist is host-granular (not per-URL); keep it tight, and avoid allowing
shared cloud hosts that could double as an exfil path - sluice flags such a host at run, and
SLUICE_EGRESS_MAX_BYTEScaps a run's egress volume. For a host you control,SLUICE_BUMP_DOMAINSopts into decrypting it for per-URL filtering (off by default; see THREAT_MODEL). - Signed core (opt-in). Build FROM a cosign-signed base image (
SLUICE_BASE_IMAGE) instead of rebuildingcore/locally; sluice verifies the signature and its CycloneDX SBOM attestation first. The image carries no key (the splice cert is generated per-container).
Build-time setup (SLUICE_SETUP_CMDS) runs on the host before the firewall, so clones
and dependency downloads have free egress; the running container is locked down.
Full threat model, trust boundaries, and known weaknesses (host-granular - data can still be laundered through an allowed host):
THREAT_MODEL.md.
A quick taste - serve JupyterLab from the sluice on http://localhost:8888 (a Python/pip
stack that needs no runtime egress at all):
mkdir lab && cp examples/jupyter.config.sh lab/sluice.config.sh
cd lab && sluice # build + serve; then open http://localhost:8888The full gallery has more self-contained demos - a firewall/exfil demo
(the egress block, made visible) and Nix (a reproducible toolchain baked at build,
contained at runtime) - plus the coding-agent presets. It shows the one runtime gotcha: a host the app needs at runtime must be in
SLUICE_ALLOW_DOMAINS (or sluice learn it), or the firewall blocks it - sluice flags which
host at exit (and in sluice doctor) so you can allow it. For any other stack, sluice init
scaffolds the config.
bin/sluice the CLI launcher (one file; generated from src/ by `make build`)
src/ the launcher in ordered slices - edit here, then `make build`
core/ the sandbox image: Dockerfile + squid / firewall / entrypoint
agents/ coding-agent presets (run `sluice agent` to list)
examples/ self-contained gallery demos
test/ acceptance + init-detection (the CI gate) and per-feature verify harnesses
completion/ bash + zsh shell completion
install.sh curl|sh + local installer
sluice.config.example.sh documented config template (every knob)
Shell completion (commands, flags, agent names) auto-installs via brew install / install.sh.
Manual: source completion/sluice.bash (bash), or add completion/ to your fpath before compinit (zsh).
Runs on docker or podman (auto-detected; override with SLUICE_ENGINE). For own-kernel
isolation, SLUICE_RUNTIME=kata runs the box as a Kata micro-VM (Linux + containerd/nerdctl). CI
(.github/workflows/acceptance.yml) runs the harness
on Linux Docker; the Linux/Podman legs are validated there rather than on macOS.
Apache-2.0 - permissive, use it however you like. Found a sandbox escape or egress bypass? See SECURITY.md.

