Skip to content

mgild/safespace

Repository files navigation

SafeSpace

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.

The Problem

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. postinstall hooks, 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 install can 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.

Install

curl -fsSL https://raw.githubusercontent.com/mgild/safespace/main/install.sh | bash

Requires 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 shell

The installer handles this automatically, but if you installed Docker after SafeSpace you may need to do it manually.

Quick Start

space clone https://github.com/user/repo     # clone + isolated container + enter
space create myproject                        # empty container
space                                         # interactive dashboard

That'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.

What You Get

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.

How It Protects You

What a malicious dependency can do on your host

~/.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

What a malicious dependency can do in SafeSpace

~/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.

Security

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

Credential access is opt-in

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 container

All 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.

Commands

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 space

Status & Shell Defaults

space 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 image

When cloning a repo that contains a .safespace.yml, settings are applied automatically.

Recommended Workflow for AI Sessions

# 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 clean

For 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 test

Base Images & Snapshots

Create 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.

Configuration

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 spaces

Set 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.

Persistence

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.

Per-Repo Config

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 install

Settings are picked up automatically on space clone. Generate one from an existing space with space share > .safespace.yml.

Supply Chain Threat Reference

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.

IDE Integration (Cursor / VS Code)

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.

From a local machine (Docker on the same host)

space code myproject           # open in VS Code
space code myproject --cursor  # open in Cursor

Requires the cursor / code CLI in your PATH (Cursor/VS Code → Command PaletteShell Command: Install 'cursor' command in PATH).

From a remote server (SSH/mosh setup)

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-server

After 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 Hostmyproject → 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

Full IDE setup guide with diagrams, ProxyJump config, troubleshooting, and links to Cursor/VS Code docs

Docs

About

Containerized dev environments with Docker. Auto-enter on cd, persistent home volumes, full dev tooling + Claude CLI.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors