Invisible dependency isolation for local development and coding agents.
safe-install routes risky package-manager operations through a disposable Docker-compatible container. On macOS, OrbStack is the intended low-friction runtime. The goal is simple: keep normal pnpm install, npm ci, uv sync, and pip install workflows feeling ordinary while host secrets stay out of reach.
By default, install commands run with:
- only the current project mounted at
/work - container
HOME=/tmp/safe-home - no host home directory
- no SSH keys, browser profiles, npm tokens, PyPI tokens, GitHub tokens, or agent settings
- dropped Linux capabilities
no-new-privileges- npm/pnpm lifecycle scripts disabled unless explicitly allowed
- Python
pip installcommands install into a project.venvand thepython/python3shims auto-use that environment from inside the project
Claude Code can load safe-install as a plugin. When enabled, the plugin's bin/ wrappers are available to Claude's Bash tool, so package-manager installs run through safe-install without shell rc setup.
For local testing from this checkout:
claude --plugin-dir .To install the local development marketplace:
claude plugin marketplace add /path/to/safe-install --scope user
claude plugin marketplace update safe-install-dev
claude plugin install safe-install@safe-install-dev --scope userIn Claude Code, verify:
safe-install doctor
SAFE_INSTALL_DRY_RUN=1 pnpm installThe intended marketplace flow is:
/plugin install safe-install@<marketplace>
/reload-pluginsFrom the project you want to protect, run:
curl -fsSL https://raw.githubusercontent.com/cachetronaut/safe-install/main/install.sh | bashThen restart Terminal and Claude Code.
For the current terminal only:
source ~/.local/share/safe-install/activate.sh
safe-install doctorExpected:
pnpm: protected
npm: protected
npx: protected
bun: protected
uv: protected
pip3: protected
python3: protectedIf you already cloned the repo locally, run:
./bin/safe-install init --repo /path/to/your/projectinit does four visible things:
- adds shell activation to your shell rc file
- installs a repo-local pre-commit guard when the current directory is a git repo
- configures Claude Code when
~/.claude/settings.jsonexists - starts OrbStack/Docker when possible
Open a new terminal after init, or run source /path/to/safe-install/activate.sh once in the current terminal.
Restart Claude Code after init, then run safe-install doctor inside Claude.
For an already-open Codex or coding-agent shell, run eval "$($HOME/.local/share/safe-install/bin/safe-install reload)" so the current process prepends the installed wrappers without a restart.
Use your usual commands:
pnpm install
npm ci
bun install
uv sync --locked
uv pip install pytest
pip install -r requirements.txtThe wrappers intercept install-like operations and run them in the container. Other commands pass through to the real host package manager.
Python is the exception for local usability: bare pip install ... creates or reuses .venv in the current project, installs there with the host Python, and subsequent python or python3 commands automatically use that .venv through the safe-install shims. This avoids disappearing container-only installs and macOS externally-managed Python failures.
uv uses the official Debian uv image rather than a Python-minor-specific image, so project Python constraints such as Python 3.14 remain valid. The wrapper hides a host .venv behind a container-local volume during uv commands because virtual environments are platform-specific. It also moves uv's downloaded Python interpreters and cache out of /tmp/safe-home so uv run can execute them inside the container.
For packages that expose command-line tools, use the ecosystem launchers instead of guessing host paths. pnpm dlx/npx/bunx fetch-and-run remote packages and are sandboxed; pnpm exec/pnpm run/bun run run already-installed local code on the host:
pnpm exec <command> # local, runs on host
pnpm dlx <package> # remote, sandboxed
npx <package> # remote, sandboxed
bunx <package> # remote, sandboxed
bun run <command> # local, runs on host
uv run <command>
uv tool run <tool>safe-install --pm pnpm -- install
safe-install --pm npm -- ci
safe-install --pm bun -- install
safe-install --pm uv -- sync --locked
safe-install --pm pip -- install -r requirements.txtPreview without running Docker:
SAFE_INSTALL_DRY_RUN=1 pnpm install
SAFE_INSTALL_DRY_RUN=1 pnpm dlx vite --version
SAFE_INSTALL_DRY_RUN=1 npx prettier --version
SAFE_INSTALL_DRY_RUN=1 bun install
SAFE_INSTALL_DRY_RUN=1 bunx cowsay hi
SAFE_INSTALL_DRY_RUN=1 uv run python --version
safe-install --pm pnpm --dry-run -- install
SAFE_INSTALL_DRY_RUN=1 pip install pdfplumberRun without network for already-downloaded material:
safe-install --pm pnpm --no-network --readonly -- installAllow npm/pnpm/bun lifecycle scripts only when you trust the dependency tree:
safe-install --pm pnpm --allow-scripts -- install
safe-install --pm bun --allow-scripts -- installClaude Code plugin sessions are protected when the plugin's bin/ directory is on Claude's Bash PATH. Shell and other coding-agent sessions are protected when their shell environment resolves package managers through safe-install/bin first.
Check any terminal or agent session:
safe-install doctorStart the local container runtime when needed:
safe-install startReal install commands also try to start OrbStack or Docker Desktop automatically on macOS when Docker is not reachable. Set SAFE_INSTALL_AUTO_START=0 to disable that behavior.
If startup fails, wait until docker info succeeds in the same shell before retrying the install. Colima and other Docker-compatible runtimes are supported too, but they must expose a working Docker CLI/socket before safe-install can run the protected container.
For pnpm TypeScript/Node repos that commit lockfile changes from the protected Linux container, native optional dependency entries can differ from the macOS host unless the repo declares both platforms. Add a pnpm supportedArchitectures block when platform-specific optional packages matter:
{
"pnpm": {
"supportedArchitectures": {
"os": ["current", "linux"],
"cpu": ["current", "x64", "arm64"],
"libc": ["current", "glibc"]
}
}
}Install a repo-local pre-commit guard:
safe-install install-git-hook --repo /path/to/repoThe guard blocks commits that stage dependency files when safe-install is not active on PATH.
For Claude Code, prefer the plugin route. The shell installer still configures ~/.claude/settings.json automatically when that file exists. If Claude Code was already open, restart it and check:
safe-install doctorManual Claude Code setup is still available:
node ./scripts/install-claude-env.jsFor Codex CLI sessions launched from your shell, shell activation is enough. For desktop apps that do not inherit your login shell, start them from an activated terminal or configure their launcher environment with:
PATH=/path/to/safe-install/bin:$PATHFor an already-open Codex worktree, run this inside that session after installing or updating:
eval "$($HOME/.local/share/safe-install/bin/safe-install reload)"
safe-install doctorpnpm:install,i,add,update,up,import,dlx,createpnpx: package execution throughpnpm dlxnpm:install,i,ci,update,up,exec,x,initnpx: package execution throughnpm execbun:install,i,add,remove,rm,update,up,x,create,initbunx: package execution throughbun xuv:sync,add,pip,tool,python,runpipandpip3:install,wheel,downloadpythonandpython3: auto-dispatch to a project.venvwhen one exists, otherwise pass through
pnpm exec, pnpm run, and bun run are intentionally not wrapped. They invoke already-installed, local, trusted code, so they run on the host. Sandboxing them would run the tool as root inside the container against the bind-mounted repo and corrupt git-aware tooling (for example, a lint-staged pre-commit hook whose stash/restore would rewrite the host index). Remote-fetching execution still goes through the container via pnpm dlx/pnpm create, npx/npm exec, and bunx/bun x/bun create.
Bypass is explicit:
SAFE_INSTALL_BYPASS=1 pnpm installThe default container budget is intentionally small for everyday local development:
- memory:
2g - CPUs:
2 - tmpfs:
512m
Override per command or session when a dependency tree needs more room:
SAFE_INSTALL_MEMORY=4g SAFE_INSTALL_CPUS=4 pnpm install
SAFE_INSTALL_TMPFS_SIZE=1g uv syncThe first backend targets Docker-compatible runtimes. That covers OrbStack on macOS, Docker Desktop, Colima, Podman-compatible Docker sockets, and Linux Docker engines.
Future backends should preserve the same policy contract:
- project-only mount
- disposable home
- no ambient secrets
- optional network isolation
- package-manager-specific lifecycle-script controls
safe-install doctorvisibility
Python virtualenv management is intentionally host-side because Linux container virtualenvs and compiled wheels are not generally executable on macOS hosts. Use SAFE_INSTALL_PYTHON_CONTAINER=1 pip install ... only when you explicitly want the previous disposable-container behavior.
See CONTRIBUTING.md.
This reduces risk from dependency installs. It does not make arbitrary untrusted code safe.
The mounted project directory is still writable by default. With network enabled, code inside the container can still reach the internet. Keep secrets out of project folders and use --no-network --readonly when inspecting suspicious material.
See SECURITY.md.