Run shell commands inside isolated, on-demand Docker sandboxes — transparently, from your terminal.
pippin -c "npm test"
pippin -c "cargo build"
pippin -c "python main.py"Pippin launches a Docker container the first time you run a command, streams I/O back to your terminal in real time, and shuts the container down after it sits idle. Full PTY support means interactive apps like vim, htop, and bash work exactly as they do on the host. No manual docker run.
AI coding agents can run arbitrary shell commands on your machine with your full user permissions. That means package installs with postinstall scripts, builds that execute arbitrary code, file modifications outside your project, and network requests to unknown endpoints. Pippin contains that blast radius.
You can use it three ways:
pippin codexandpippin copilotrun those CLIs fully inside the sandbox.pippin -c "<cmd>"runs any command inside the sandbox.- You can point another tool at
pippinas its shell so its command execution is sandboxed.
Either way, you get filesystem isolation and network containment without changing how the agent behaves.
pippinselects a named sandbox from~/.config/pippin/config.json.- If no sandbox name is provided, it uses the sandbox named
default. - If the sandbox is not running, Pippin starts a container with that sandbox's configured
rootmounted. - A lightweight server inside the container receives the command over WebSocket and executes it.
- stdout/stderr stream back to your terminal live. PTY mode forwards stdin, signals, and resize events.
- The container exits automatically after an idle timeout.
- Docker
- macOS or Linux — x64 or arm64
Run pippin doctor after installing to verify your setup.
curl -fsSL https://raw.githubusercontent.com/malaporte/pippin/main/scripts/install.sh | bashRun pippin init to create a default sandbox config if you do not already have one:
pippin initBy default this uses ~/Developer when that directory exists, otherwise it uses your current working directory.
You can also create the config manually. Configure at least one named sandbox, and make sure one is called default:
{
"sandboxes": {
"default": {
"root": "~/Developer"
}
}
}Then run commands normally from anywhere under that root:
cd ~/Developer/my-project
pippin -c hostname
pippin shellTo target a non-default sandbox:
pippin --sandbox work -c "npm test"
pippin --sandbox work shellPippin validates that your current working directory is accessible inside the selected sandbox. If your cwd is outside the sandbox's root and extra mounts, it errors out.
| Command | Description |
|---|---|
pippin -c "<cmd>" |
Run a command inside the default sandbox |
pippin init |
Configure a default sandbox if one is missing |
pippin --sandbox <name> -c "<cmd>" |
Run a command inside a named sandbox |
pippin shell |
Open an interactive shell in the default sandbox |
pippin --sandbox <name> shell |
Open a shell in a named sandbox |
pippin status |
Show status for the default sandbox |
pippin status --sandbox <name> |
Show status for a named sandbox |
pippin status --all |
Show all running sandboxes |
pippin stop |
Stop the default sandbox |
pippin stop --sandbox <name> |
Stop a named sandbox |
pippin stop --all |
Stop all running sandboxes |
pippin restart [--sandbox <name>] |
Restart a sandbox |
pippin doctor [--sandbox <name>] |
Check prerequisites and validate configuration |
pippin update [--force] |
Update pippin |
Pippin uses a single global config file at ~/.config/pippin/config.json.
Each sandbox is configured under the top-level sandboxes map.
{
"sandboxes": {
"default": {
"root": "~/Developer",
"idle_timeout": 900,
"init_timeout": 60,
"init": "echo ready",
"shell": "zsh",
"image": "my-registry/dev:latest",
"dockerfile": "~/.config/pippin/Dockerfile.dev",
"host_port_forwards": [
{ "host_port": 6379 },
{ "host_port": 3306 }
],
"tools": ["git", "gh"],
"host_commands": ["git", "ssh"],
"ssh_agent": true,
"mounts": [
{ "path": "~/shared", "readonly": true }
]
},
"work": {
"root": "~/Work"
}
}
}Notes:
sandboxes.defaultis required unless you always pass--sandbox.rootis required for every sandbox.rootandmountsdefine which host paths are reachable inside the container.initis optional and runs when a fresh sandbox starts.host_port_forwardsbinds127.0.0.1:<sandbox_port>inside the sandbox and forwards tohost.docker.internal:<host_port>.- There is no workspace auto-detection and no package-manager auto-install behavior.
The only global setting is portRangeStart.
{
"portRangeStart": 9111,
"sandboxes": {
"default": {
"root": "~/Developer",
"tools": ["git", "gh", "aws"]
}
}
}You can override the bundled sandbox image with either:
- a pre-built image via
image - a local Dockerfile via
dockerfile
Priority is:
- sandbox
image - sandbox
dockerfile - bundled default Dockerfile
When a Dockerfile is used, Pippin builds it locally and tags it by content hash.
The bundled default sandbox image includes socat, which Pippin uses for host_port_forwards. Custom images that use host_port_forwards must provide socat themselves.
Forward host services into the sandbox while keeping 127.0.0.1 working inside the container:
{
"sandboxes": {
"default": {
"root": "~/Developer",
"host_port_forwards": [
{ "host_port": 6379 },
{ "host_port": 3306 }
]
}
}
}Each entry forwards host.docker.internal:<host_port> to 127.0.0.1:<sandbox_port> inside the sandbox. If sandbox_port is omitted, it defaults to host_port.
Instead of manually wiring dotfiles, environment variables, and SSH/GPG forwarding, declare the tools you use and Pippin handles the rest.
{
"sandboxes": {
"default": {
"root": "~/Developer",
"tools": ["git", "gh", "aws", "npm", "ssh", "codex", "copilot"]
}
}
}Built-in recipes:
| Tool | What it does |
|---|---|
git |
Mounts Git config and forwards the host GPG agent. Enables SSH agent. |
gh |
Mounts ~/.config/gh/config.yml and resolves GH_TOKEN. |
aws |
Mounts ~/.aws/config and resolves temporary credentials. |
snowflake |
Mounts Snowflake config and extracts cached auth. |
npm |
Mounts ~/.npmrc and forwards npm env. |
bun |
Mounts ~/.npmrc and forwards bun/npm env. |
pnpm |
Mounts ~/.npmrc, mounts the pnpm store, and sets PNPM_STORE_DIR. |
ssh |
Mounts SSH config and known_hosts. Enables SSH agent. |
codex |
Mounts Codex config and forwards OPENAI_API_KEY. |
copilot |
Mounts Copilot config and resolves GitHub auth. |
Some commands can be configured to run on the host instead of inside the sandbox:
{
"sandboxes": {
"default": {
"root": "~/Developer",
"host_commands": ["git", "ssh", "docker"]
}
}
}Matching is by the first token of the command.
Host commands bypass sandbox isolation entirely.
Enable SSH agent forwarding per sandbox:
{
"sandboxes": {
"default": {
"root": "~/Developer",
"ssh_agent": true
}
}
}When enabled, Pippin mounts Docker Desktop's SSH agent socket and sets SSH_AUTH_SOCK inside the container.
Forward specific environment variables into a sandbox:
{
"sandboxes": {
"default": {
"root": "~/Developer",
"environment": ["NPM_TOKEN", "AWS_PROFILE", "GITHUB_TOKEN"]
}
}
}Values are resolved from your login shell environment when the sandbox starts.
Pippin fingerprints the active sandbox configuration, including image, mounts, environment forwarding, tools, and agent forwarding. If the config changes, the sandbox is restarted automatically before the next command runs.
Host machine Container (Docker)
───────────────── ──────────────────
pippin CLI
│
├─ selects named sandbox
├─ starts container via docker ─▶ pippin-server (HTTP + WebSocket)
├─ polls /health │
└─ WebSocket /exec?cmd=... ◀──────────▶ spawns process, streams I/O
The server binary is bundled with the CLI and copied into the container at startup.
bun run test
bun run typecheck
bun run buildMIT
