Claude Sandbox Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Create a fully sandboxed Docker environment for autonomous Claude Code sessions with isolated postgres, localstack, and nREPL.

Architecture: Monolith container (Claude + Clojure + nREPL) with sidecar postgres/localstack. Git worktrees for code isolation. Host mounts for auth and dependency caching.

Tech Stack: Docker, docker-compose, Babashka tasks, Clojure nREPL with cider middleware


Task 1: Create sandbox directory structure

Files:

Step 1: Create directory

mkdir -p sandbox
touch sandbox/.gitkeep

Step 2: Commit

git add sandbox/.gitkeep
git commit -m "chore: add sandbox directory"

Task 2: Create Dockerfile

Files:

Step 1: Write Dockerfile

FROM eclipse-temurin:21-jdk-alpine

# System dependencies
RUN apk add --no-cache \
    nodejs npm \
    bash curl git \
    netcat-openbsd \
    rlwrap

# Install Clojure CLI
RUN curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh \
    && chmod +x linux-install.sh \
    && ./linux-install.sh \
    && rm linux-install.sh

# Install Babashka
RUN curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install \
    && chmod +x install \
    && ./install --dir /usr/local/bin \
    && rm install

# Install bbin
RUN curl -o- -L https://raw.githubusercontent.com/babashka/bbin/v0.2.4/bbin > /usr/local/bin/bbin \
    && chmod +x /usr/local/bin/bbin

# Install clj-nrepl-eval via bbin
ENV BBIN_HOME=/root/.local/share/bbin
ENV PATH="${BBIN_HOME}/bin:${PATH}"
RUN bbin install https://github.com/bhauman/clojure-mcp-light.git \
    --as clj-nrepl-eval \
    --main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'

# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code

WORKDIR /workspace

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

Step 2: Commit

git add sandbox/Dockerfile
git commit -m "feat(sandbox): add Dockerfile with Claude + Clojure + Babashka"

Task 3: Create entrypoint script

Files:

Step 1: Write entrypoint

#!/bin/bash
set -e

NREPL_PORT="${NREPL_PORT:-7888}"

echo "Starting nREPL on port $NREPL_PORT..."
cd /workspace

# Start nREPL with cider middleware in background
clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.3.1"}
                        cider/cider-nrepl {:mvn/version "0.56.0"}
                        refactor-nrepl/refactor-nrepl {:mvn/version "3.11.0"}}
                 :aliases {:cider/nrepl {:main-opts ["-m" "nrepl.cmdline"
                           "--middleware" "[refactor-nrepl.middleware/wrap-refactor,cider.nrepl/cider-middleware]"]}}}' \
    -M:dev:test:cider/nrepl \
    -p "$NREPL_PORT" \
    -b 0.0.0.0 &

NREPL_PID=$!

# Wait for nREPL to be ready
echo "Waiting for nREPL..."
until nc -z localhost "$NREPL_PORT" 2>/dev/null; do
    sleep 0.5
done
echo "nREPL ready on port $NREPL_PORT"

# Run Claude Code with dangerous permissions (safe in sandbox)
exec claude --dangerously-skip-permissions "$@"

Step 2: Commit

git add sandbox/entrypoint.sh
git commit -m "feat(sandbox): add entrypoint script for nREPL + Claude"

Task 4: Create docker-compose.yml

Files:

Step 1: Write docker-compose.yml

name: orcha-sandbox-${FEATURE_NAME:-default}

services:
  postgres:
    image: postgres:18.1
    environment:
      POSTGRES_DB: orcha
      POSTGRES_HOST_AUTH_METHOD: trust
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d orcha"]
      interval: 2s
      timeout: 5s
      retries: 10
      start_period: 5s

  localstack:
    image: localstack/localstack:latest
    environment:
      - SERVICES=s3,sqs,secretsmanager,ssm
      - DEFAULT_REGION=eu-central-1
      - PERSISTENCE=1
    volumes:
      - localstack-data:/var/lib/localstack
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 2s
      timeout: 5s
      retries: 10
      start_period: 10s

  claude:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      postgres:
        condition: service_healthy
      localstack:
        condition: service_healthy
    environment:
      - ENVIRON_NAME=sandbox
      - NREPL_PORT=7888
    volumes:
      - ${WORKTREE_PATH:-.}:/workspace
      - ${HOME}/.claude:/root/.claude:ro
      - ${WORKTREE_PATH:-.}/.claude-sandbox/projects:/root/.claude/projects
      - ${HOME}/.m2:/root/.m2
      - ${HOME}/.gitlibs:/root/.gitlibs
      - ${HOME}/.clojure/.cpcache:/root/.clojure/.cpcache
    ports:
      - "${NREPL_HOST_PORT:-17888}:7888"
    stdin_open: true
    tty: true

volumes:
  postgres-data:
  localstack-data:

Step 2: Commit

git add sandbox/docker-compose.yml
git commit -m "feat(sandbox): add docker-compose with postgres, localstack, claude"

Task 5: Add :sandbox profile to config.edn

Files:

Step 1: Add sandbox endpoint for AWS

Find the :com.getorcha/aws section and add :sandbox profile to the endpoint:

:com.getorcha/aws
{:config     {:region   "eu-central-1"
              :endpoint #profile {:local-dev "http://localhost:4566"
                                  :sandbox   "http://localstack:4566"
                                  :default   nil}}

Step 2: Add sandbox profile for db-credentials-json

Find :com.getorcha/db-credentials-json and add :sandbox profile:

:com.getorcha/db-credentials-json
#profile {:local-dev "{\"host\":\"localhost\",\"port\":5432,\"dbname\":\"orcha\",\"username\":\"postgres\"}"
          :sandbox   "{\"host\":\"postgres\",\"port\":5432,\"dbname\":\"orcha\",\"username\":\"postgres\"}"
          :default   #orcha/param "/v1-orcha/db-credentials"}

Step 3: Commit

git add resources/com/getorcha/config.edn
git commit -m "feat(sandbox): add :sandbox profile for Docker networking"

Task 6: Create sandbox bb tasks

Files:

Step 1: Write sandbox.clj

(ns sandbox
  (:require [babashka.fs :as fs]
            [babashka.process :as p]
            [clojure.string :as str]))

(def worktree-base ".worktrees/feature")
(def port-range-start 17000)
(def port-range-size 1000)


(defn feature->port
  "Hash feature name to port in range 17000-17999"
  [feature-name]
  (+ port-range-start
     (mod (Math/abs (hash feature-name)) port-range-size)))


(defn parse-args
  "Parse --port flag from args, return {:port n :feature name}"
  [args]
  (loop [args args
         result {}]
    (if (empty? args)
      result
      (let [[a b & rest] args]
        (cond
          (= a "--port")
          (recur rest (assoc result :port (parse-long b)))

          (= a "--worktree")
          (recur (cons b rest) (assoc result :worktree true))

          (nil? (:feature result))
          (recur (cons b rest) (assoc result :feature a))

          :else
          (recur (cons b rest) result))))))


(defn start
  "Start sandbox for feature"
  [args]
  (let [{:keys [feature port]} (parse-args args)]
    (when-not feature
      (println "Usage: bb sandbox:start <feature-name> [--port PORT]")
      (System/exit 1))

    (let [port (or port (feature->port feature))
          worktree-path (str worktree-base "/" feature)
          abs-worktree-path (str (fs/absolutize worktree-path))
          branch-name (str "feature/" feature)]

      ;; Create worktree if not exists
      (when-not (fs/exists? worktree-path)
        (println (str "Creating worktree at " worktree-path " from main..."))
        (p/shell "git" "worktree" "add" "-b" branch-name worktree-path "main"))

      ;; Create .claude-sandbox/projects for conversation persistence
      (fs/create-dirs (str worktree-path "/.claude-sandbox/projects"))

      ;; Write port file
      (spit (str worktree-path "/.nrepl-port") (str port))

      (println (str "nREPL will be exposed on port " port))
      (println "Starting sandbox containers...")

      ;; Start docker-compose
      (p/shell {:dir "sandbox"
                :extra-env {"WORKTREE_PATH" abs-worktree-path
                            "NREPL_HOST_PORT" (str port)
                            "FEATURE_NAME" feature
                            "HOME" (System/getenv "HOME")}}
               "docker-compose" "run" "--rm" "--service-ports" "claude"))))


(defn stop
  "Stop sandbox for feature"
  [args]
  (let [{:keys [feature]} (parse-args args)]
    (when-not feature
      (println "Usage: bb sandbox:stop <feature-name>")
      (System/exit 1))

    (println (str "Stopping sandbox for " feature "..."))
    (p/shell {:dir "sandbox"
              :extra-env {"FEATURE_NAME" feature}}
             "docker-compose" "down")))


(defn list-sandboxes
  "List running sandboxes"
  [_args]
  (let [result (p/shell {:out :string}
                        "docker" "ps" "--filter" "name=orcha-sandbox" "--format" "{{.Names}}")]
    (if (str/blank? (:out result))
      (println "No running sandboxes")
      (do
        (println "Running sandboxes:")
        (doseq [name (str/split-lines (:out result))]
          (println (str "  " name)))))))


(defn clean
  "Clean up sandbox containers and optionally worktree"
  [args]
  (let [{:keys [feature worktree]} (parse-args args)]
    (when-not feature
      (println "Usage: bb sandbox:clean <feature-name> [--worktree]")
      (System/exit 1))

    (let [worktree-path (str worktree-base "/" feature)
          branch-name (str "feature/" feature)]

      ;; Stop and remove containers/volumes
      (println (str "Removing containers and volumes for " feature "..."))
      (p/shell {:dir "sandbox"
                :extra-env {"FEATURE_NAME" feature}
                :continue true}
               "docker-compose" "down" "-v")

      ;; Remove worktree if requested
      (when worktree
        (when (fs/exists? worktree-path)
          (println (str "Removing worktree " worktree-path "..."))
          (p/shell "git" "worktree" "remove" "--force" worktree-path))

        ;; Delete branch
        (println (str "Deleting branch " branch-name "..."))
        (p/shell {:continue true} "git" "branch" "-D" branch-name))

      (println "Done."))))

Step 2: Add tasks to bb.edn

Add the following to the :tasks map in bb.edn:

;; Sandbox tasks
sandbox:start
{:doc      "Start sandboxed Claude session: bb sandbox:start <feature-name> [--port PORT]"
 :requires ([sandbox])
 :task     (sandbox/start *command-line-args*)}

sandbox:stop
{:doc      "Stop sandbox: bb sandbox:stop <feature-name>"
 :requires ([sandbox])
 :task     (sandbox/stop *command-line-args*)}

sandbox:list
{:doc      "List running sandboxes"
 :requires ([sandbox])
 :task     (sandbox/list-sandboxes *command-line-args*)}

sandbox:clean
{:doc      "Clean up sandbox: bb sandbox:clean <feature-name> [--worktree]"
 :requires ([sandbox])
 :task     (sandbox/clean *command-line-args*)}

Step 3: Commit

git add scripts/sandbox.clj bb.edn
git commit -m "feat(sandbox): add bb tasks for sandbox management"

Task 7: Update .gitignore

Files:

Step 1: Add sandbox-related ignores

Add to .gitignore:

# Sandbox
.claude-sandbox/
.nrepl-port

Step 2: Commit

git add .gitignore
git commit -m "chore: ignore sandbox conversation state"

Task 8: Build and test Docker image

Step 1: Build the image

cd sandbox && docker-compose build claude

Expected: Image builds successfully

Step 2: Test basic startup

cd sandbox && WORKTREE_PATH=$(pwd)/.. docker-compose run --rm claude echo "Container works"

Expected: Prints "Container works" after nREPL starts

Step 3: Commit if any fixes needed


Task 9: End-to-end test

Step 1: Create test sandbox

bb sandbox:start test-sandbox

Expected:

Step 2: Test nREPL connection from host

In another terminal:

PORT=$(cat .worktrees/feature/test-sandbox/.nrepl-port)
clj-nrepl-eval -p $PORT "(+ 1 2 3)"

Expected: 6

Step 3: Exit Claude and clean up

Exit Claude with /exit or Ctrl+D, then:

bb sandbox:clean test-sandbox --worktree

Expected: Containers, volumes, worktree, and branch removed


Task 10: Documentation

Files:

Step 1: Add sandbox documentation to CLAUDE.md

Add a section:

## Sandbox Development

Run Claude Code in an isolated Docker environment:

```bash
bb sandbox:start <feature-name>     # Start sandbox with worktree
bb sandbox:stop <feature-name>      # Stop containers
bb sandbox:list                     # List running sandboxes
bb sandbox:clean <name> [--worktree] # Clean up

Connect editor to nREPL:

cat .worktrees/feature/<name>/.nrepl-port
# Connect to localhost:<port>

Inside sandbox, start Integrant system with (go) in nREPL.


**Step 2: Commit**

```bash
git add CLAUDE.md
git commit -m "docs: add sandbox usage instructions"