Stop running AI-generated code on your bare metal.
When you ask Claude, Cursor, or Copilot to build something, they pull in dependencies you've never audited. A single malicious postinstall script in an npm package, a compromised PyPI wheel, or a typosquatted crate can read your SSH keys, exfiltrate your API tokens, scan your filesystem, or install a persistent backdoor — and it all runs as you, with full access to everything you own.
SafeSpace gives every project an isolated container with full dev tooling, no Linux capabilities, a custom seccomp profile, and a validating proxy between it and Docker. Your code runs. Your machine stays clean.
AI coding assistants are powerful, but they routinely:
-
Install packages you didn't vet.
npm install,pip install,cargo add— each one can execute arbitrary code at install time. Supply chain attacks on npm, PyPI, and crates.io are not theoretical. Thousands of malicious packages are published every month. -
Run arbitrary build scripts.
postinstallhooks,setup.py,build.rs— these run with your full user permissions. A compromised dependency can read~/.ssh/id_ed25519,~/.aws/credentials,~/.claude/, browser cookies, or anything else your user can access. -
Pull in transitive dependencies you've never seen. A single
npm installcan bring in hundreds of packages. Any one of them can execute code. You are trusting the entire dependency tree every time. -
Generate code that fetches remote resources. Downloading tarballs, cloning repos, curling scripts — all running as your user with network access to your internal services.
Running this on your host machine means a single bad dependency can compromise your SSH keys, cloud credentials, API tokens, browser sessions, and every other project on your disk. It doesn't matter how careful you are — the attack surface is the union of every dependency your AI assistant decides to install.
SafeSpace is the fix. Each project gets its own container. If a dependency is malicious, it can trash the container but it can't touch your host, your keys, or your other projects.
curl -fsSL https://raw.githubusercontent.com/mgild/safespace/main/install.sh | bashRequires Docker, Git, and Rust (for building the CLI). fzf recommended.
Docker permissions: If you get "permission denied" errors, add yourself to the docker group:
sudo usermod -aG docker $USER
newgrp docker # activate in current shellThe installer handles this automatically, but if you installed Docker after SafeSpace you may need to do it manually.
space clone https://github.com/user/repo # clone + isolated container + enter
space create myproject # empty container
space # interactive dashboardThat's it. You're now in an isolated environment with full dev tooling. Multiple terminals share one container — each space enter opens a new shell in the same environment.
A full dev environment out of the box — no setup required:
Languages: Rust (rustup), Go, Node.js (nvm), Bun, Python 3 (uv), Solana CLI
Tools: Claude CLI, gh, lazygit, tmux, neovim, ripgrep, fzf, docker, git-delta, zoxide, just, watchexec, and more
Shell: zsh with oh-my-zsh, syntax highlighting, autosuggestions, history substring search
Your dotfiles (zshrc, gitconfig, tmux) can be forwarded with --with-dotfiles. Nothing is mounted by default — see Security.
~/.ssh/id_ed25519 readable your private keys
~/.aws/credentials readable your cloud access
~/.claude/ readable your API tokens
~/.config/gh/ readable your GitHub OAuth tokens
~/.gnupg/ readable your GPG keys
~/Documents, ~/Desktop, ... readable your personal files
other projects in ~/code/ readable every other repo you work on
~/.zsh_history readable your command history
browser profiles readable your cookies and sessions
~/spaces/thisproject/ readable only this project's workspace
~/ writable only this container's home volume (disposable)
nothing else
No SSH keys. No cloud creds. No API tokens. No access to other projects. No access to your home directory. If the container is compromised, space destroy it and start fresh.
Every container runs with:
- All capabilities dropped (
--cap-drop=ALL) - Custom seccomp profile blocking
ptrace,mount,bpf,io_uring,unshare,setns,open_by_handle_at, and 30+ other escape-relevant syscalls - Per-container Docker network — containers can't see each other's traffic
- Docker API proxy that blocks privileged mode, host namespaces, capability escalation, device access, socket remounting, and unauthorized bind mounts
- No credentials by default — SSH keys, Claude config, GitHub tokens are only mounted when you explicitly ask
space create myproject # no credentials mounted (default)
space create myproject --with-ssh # mount ~/.ssh (read-only)
space create myproject --with-ssh-agent # forward SSH agent (implies --with-ssh)
space create myproject --with-claude # mount ~/.claude (read-only)
space create myproject --with-gh # mount ~/.config/gh (read-only)
space create myproject --with-aws # mount ~/.aws (read-only)
space create myproject --with-gitconfig # mount ~/.gitconfig (read-only)
space create myproject --with-gcloud # mount ~/.config/gcloud (read-only)
space create myproject --with-gpg # mount ~/.gnupg (read-only)
space create myproject --with-dotfiles # mount host dotfiles (.zshrc, .gitconfig, .tmux.conf)
space create myproject --with-all # all of the above
space create myproject --no-docker # no Docker access inside containerAll credential mounts are enforced read-only — both by the CLI and by the Docker API proxy. Even if code inside the container tries to remount them read-write via the Docker API, the proxy blocks it.
Principle of least privilege: only grant what you need. If you're just building and testing code, use no flags. If you need to git push, add --with-ssh-agent. Don't mount credentials "just in case."
See SECURITY.md for the full threat model, seccomp syscall table, and known limitations.
space enter myproject # attach to a container
space cd myproject # cd into workspace on host
space stop myproject # stop without destroying
space destroy myproject # remove (workspace files kept)
space bind-port 8080:8080 # expose a port (auto-detects current space)
space list-ports # show all port mappings
space logs myproject -f # follow container logs
space exec myproject ls -la # run a one-off command
space snapshot # snapshot current space (auto-detect + timestamp)
space snapshot myproject v1 # snapshot with custom tag
space snapshot list # list all snapshots
space update # self-update
# Additional commands
space start myproject # start without attaching
space code myproject # open in VS Code (--cursor for Cursor)
space cp myproject:/path ./local # copy files in/out
space run myproject npm test # run a command inside an existing space
space expose 8080 # public HTTPS tunnel via bore/cloudflared
space share myproject # export config as .safespace.yml
space template create rust-web # create a reusable template
space template list # list available templates
space doctor # check setup, fix broken symlinks
space heal # restart stopped containers
space fork myproject myfork # snapshot + create new space from it
space rename old new # rename a spacespace status [name] prints a focused summary that includes the container state, runtime stats, port and service bindings, and the actual .safespace.yml (or fallback marker values) no matter whether you run it from the host or from inside a space. The interactive dashboard also now reports the Docker proxy container at the top of the list so it’s no longer hidden from view.
Because the CLI now detects whatever shell you are running (zsh/fish/bash/dash, etc.), every space enter session spawns that same shell inside the container—no extra config needed.
Shell integration still installs the space/ss wrappers plus the legacy ss-hook.zsh placeholder, but the chpwd auto-enter hook has been disabled. You must now launch a space with space enter <name> explicitly, which keeps the experience predictable while still sourcing .bashrc.safespace/.zshrc.safespace for the prompt.
Useful flags for create / clone:
--size small/medium/large/xl # resource limits (2/4/16/32 GB RAM)
--with postgres # attach sidecar services (postgres, redis, mysql, mongo, minio)
--template rust-web # create from a template
--base rust-nightly # use a custom base imageWhen cloning a repo that contains a .safespace.yml, settings are applied automatically.
# 1. Create an isolated space for the project
space create my-ai-project
# 2. Inside the space, let your AI assistant install whatever it wants
# npm install, pip install, cargo add — it's all sandboxed
npm init -y && npm install express # runs in the container, not on your host
# 3. If you need git push access, add it explicitly
space create my-ai-project --with-ssh-agent
# 4. When done, or if something feels wrong
space destroy my-ai-project --purge # nuke everything, start cleanFor projects you trust less (random GitHub repos, unfamiliar dependencies, AI-generated code you haven't reviewed):
# Maximum isolation — no credentials, no Docker access
space create sketchy-project --no-docker
# Non-interactive command execution inside a space
space run test-it npm testCreate custom base images with extra tooling, or snapshot a running container:
# Custom base images (Dockerfiles you maintain)
space base create rust-nightly # scaffold a Dockerfile
vim ~/.local/share/safespace/bases/rust-nightly/Dockerfile
space base build rust-nightly # build the image
space create myproject --base rust-nightly
# Snapshots (docker commit of current state)
space snapshot myproject # save as timestamped snapshot
space snapshot list # show all snapshots
space create newproject --image safespace-snapshot-myproject:20260307-153042--base and --image are mutually exclusive. Base images layer a Dockerfile on top of the root image. Snapshots capture the full container state as-is.
SafeSpace uses a YAML config at ~/.config/space/config.yml (env vars override YAML):
space init # interactive setup wizard
space config get # show all config
space config set with_ssh true # always mount SSH keys
space config set with_claude true # always mount Claude config
space config set default_base rust # default base for new spacesSet defaults so you don't need flags every time:
| Key | Default | Description |
|---|---|---|
with_ssh |
false |
Mount ~/.ssh read-only by default |
with_ssh_agent |
false |
Forward SSH agent by default |
with_claude |
false |
Mount ~/.claude read-only by default |
with_gh |
false |
Mount ~/.config/gh read-only by default |
with_aws |
false |
Mount ~/.aws read-only by default |
with_gitconfig |
false |
Mount ~/.gitconfig read-only by default |
with_gcloud |
false |
Mount ~/.config/gcloud read-only by default |
with_gpg |
false |
Mount ~/.gnupg read-only by default |
with_dotfiles |
false |
Mount host dotfiles (.zshrc, .gitconfig, .tmux.conf) by default |
no_docker |
false |
Disable Docker access by default |
default_base |
(none) | Base image to use when --base not specified |
CLI flags always override config defaults. See docs/REFERENCE.md for all config keys.
Home volume — a Docker named volume backs ~/ inside the container. cargo install, pip install, npm -g, shell history — all persist across restarts.
Workspace — bind-mounted from the host. These are your actual project files, accessible from both sides.
Declarative packages — add a .ss-packages file to auto-install apt packages on container start.
Custom images — add a Dockerfile.safespace to extend the base image per-project.
Drop a .safespace.yml in any repo to declare the environment:
size: medium
services:
- postgres
- redis
packages:
- protobuf-compiler
- cmake
ports:
- "3000:3000"
- "5432:5432"
with_ssh_agent: true
with_gh: true
on_create: |
npm installSettings are picked up automatically on space clone. Generate one from an existing space with space share > .safespace.yml.
If you're not convinced this matters, here are real supply chain attacks on package registries:
| Attack | Registry | Impact |
|---|---|---|
| event-stream | npm | Stole cryptocurrency wallet keys via postinstall |
| ua-parser-js | npm | Cryptominer + password stealer injected into popular package |
| colors + faker | npm | Maintainer sabotaged own packages, broke thousands of apps |
| Codecov | bash script | Modified uploader script to exfiltrate env vars and secrets |
| PyPI typosquatting | PyPI | Dozens of packages delivering credential stealers |
| crates.io typosquatting | crates.io | rustdecimal (typosquat of rust_decimal) exfiltrated env vars |
These attacks succeed because package install hooks run with your full user permissions. In SafeSpace, a compromised dependency is contained to that space's container — it cannot access your keys, tokens, or other projects.
AI assistants make this worse because they install packages without you reviewing every transitive dependency. You may ask for "a REST API with authentication" and get 200+ npm packages you've never heard of, each with the ability to execute arbitrary code on your machine. SafeSpace ensures that code runs in a box, not on your box.
SafeSpace containers are full remote development environments. Connect Cursor or VS Code so that extensions, language servers, terminals, and debugging all run inside the container — your laptop just renders the UI.
space code myproject # open in VS Code
space code myproject --cursor # open in CursorRequires the cursor / code CLI in your PATH (Cursor/VS Code → Command Palette → Shell Command: Install 'cursor' command in PATH).
If you're developing on a remote machine — or want Cursor on your laptop connected to a container on a server — use the SSH server flag:
# On the server
space create myproject --with-ssh-serverAfter creation, SafeSpace prints the connection info:
▸ SSH server: ssh -p 54321 youruser@localhost
Cursor: Add SSH target localhost:54321 then open ~/spaces/myproject
Then on your laptop, add to ~/.ssh/config:
Host myproject
HostName localhost
Port 54321
User youruser
ProxyJump youruser@your-server.com # hop through the server into the container
In Cursor: install the Remote - SSH extension → Remote-SSH: Connect to Host → myproject → open ~/spaces/myproject. Cursor downloads its agent into the container once; on reconnect it's instant.
Extensions run inside the container. The Cursor/VS Code server agent, all extensions, and all language servers install into the container's home volume and persist across reconnects.
To set the SSH server on for all new spaces:
space config set with_ssh_server true- docs/REFERENCE.md — all commands, configuration, architecture, customization
- docs/cursor-ssh.md — Cursor & VS Code remote development setup
- SECURITY.md — threat model, Docker API proxy, seccomp profile, known limitations
- SKILLS.md — agent reference for AI coding tools
- CONTRIBUTING.md — build, test, PR guidelines