Skip to content

Instantly share code, notes, and snippets.

@wedancedalot
Last active June 10, 2026 18:33
Show Gist options
  • Select an option

  • Save wedancedalot/c47c3d5f638d1a8818994bb28825836f to your computer and use it in GitHub Desktop.

Select an option

Save wedancedalot/c47c3d5f638d1a8818994bb28825836f to your computer and use it in GitHub Desktop.
Runs Claude Code inside a sandboxed Docker container, mounting the current directory as `/workspace`. Builds the image (`local/claude-code`) on first run, reuses it after.
#!/usr/bin/env bash
set -euo pipefail
IMAGE="local/claude-code"
VOLUME="claude-auth"
SSH_DIR="${HOME}/.ssh"
CLAUDE_DIR="${HOME}/.claude"
SETUP_MARKER=".claude-docker-setup-done"
# ── Build image if missing ──────────────────────────────────
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
echo "Building Docker image ${IMAGE} ..."
docker build -t "$IMAGE" -<<'DOCKERFILE'
FROM node:20-bookworm
RUN apt-get update && apt-get install -y \
git curl ripgrep less vim openssh-client \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code
RUN useradd -m -s /bin/bash claude
USER claude
WORKDIR /workspace
CMD ["bash"]
DOCKERFILE
fi
docker volume create "$VOLUME" > /dev/null
# ── Helper: exec a command inside a disposable container with the volume ──
vol_run() {
docker run --rm -v "${VOLUME}:/home/claude" "$@"
}
# ── SSH key selection ───────────────────────────────────────
setup_ssh() {
if [[ ! -d "$SSH_DIR" ]]; then
echo "No ~/.ssh directory found — skipping SSH setup."
return
fi
echo ""
echo "═══ SSH Keys ═══"
echo "Found in ${SSH_DIR}:"
echo ""
local files=()
for f in "$SSH_DIR"/*; do
[[ -f "$f" ]] || continue
local name
name=$(basename "$f")
# skip noise
case "$name" in
.DS_Store|*.old) continue ;;
esac
# skip sockets (e.g. agent)
[[ -S "$f" ]] && continue
files+=("$name")
done
if [[ ${#files[@]} -eq 0 ]]; then
echo " (no key files found)"
return
fi
local i=1
for name in "${files[@]}"; do
printf " %2d) %s\n" "$i" "$name"
((i++))
done
echo ""
echo " a) All"
echo " n) None"
echo ""
read -rp "Select files to copy (comma-separated numbers, 'a' for all, 'n' to skip): " selection
[[ "$selection" == "n" ]] && { echo "Skipping SSH keys."; return; }
local selected=()
if [[ "$selection" == "a" ]]; then
selected=("${files[@]}")
else
IFS=',' read -ra indices <<< "$selection"
for idx in "${indices[@]}"; do
idx="${idx// /}"
if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx >= 1 && idx <= ${#files[@]} )); then
selected+=("${files[$((idx-1))]}")
fi
done
fi
if [[ ${#selected[@]} -eq 0 ]]; then
echo "No valid selection — skipping SSH keys."
return
fi
echo ""
echo "Copying ${#selected[@]} SSH file(s) into container volume..."
local tmpdir
tmpdir=$(mktemp -d)
for name in "${selected[@]}"; do
cp -L "${SSH_DIR}/${name}" "${tmpdir}/${name}"
done
vol_run \
-v "${tmpdir}:/tmp/ssh-import:ro" \
"$IMAGE" bash -c '
mkdir -p /home/claude/.ssh
chmod 700 /home/claude/.ssh
cp /tmp/ssh-import/* /home/claude/.ssh/
for f in /home/claude/.ssh/*; do
case "$(basename "$f")" in
*.pub|config|known_hosts) chmod 644 "$f" ;;
*) chmod 600 "$f" ;;
esac
done
'
rm -rf "$tmpdir"
echo "Done — ${#selected[@]} SSH file(s) copied."
}
# ── Claude skills selection ─────────────────────────────────
setup_skills() {
local skills_dir="${CLAUDE_DIR}/skills"
if [[ ! -d "$skills_dir" ]]; then
echo "No ~/.claude/skills directory found — skipping skills setup."
return
fi
echo ""
echo "═══ Claude Skills ═══"
echo "Found in ${skills_dir}:"
echo ""
local skills=()
for d in "$skills_dir"/*/; do
[[ -d "$d" ]] || continue
skills+=("$(basename "$d")")
done
if [[ ${#skills[@]} -eq 0 ]]; then
echo " (no skills found)"
return
fi
local i=1
for s in "${skills[@]}"; do
printf " %2d) %s\n" "$i" "$s"
((i++))
done
echo ""
echo " a) All"
echo " n) None"
echo ""
read -rp "Select skills to copy (comma-separated numbers, 'a' for all, 'n' to skip): " selection
[[ "$selection" == "n" ]] && { echo "Skipping skills."; return; }
local selected=()
if [[ "$selection" == "a" ]]; then
selected=("${skills[@]}")
else
IFS=',' read -ra indices <<< "$selection"
for idx in "${indices[@]}"; do
idx="${idx// /}"
if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx >= 1 && idx <= ${#skills[@]} )); then
selected+=("${skills[$((idx-1))]}")
fi
done
fi
if [[ ${#selected[@]} -eq 0 ]]; then
echo "No valid selection — skipping skills."
return
fi
echo ""
echo "Copying ${#selected[@]} skill(s) into container volume..."
local tmpdir
tmpdir=$(mktemp -d)
for name in "${selected[@]}"; do
# -R recursive, -L resolve symlinks (skills are often symlinked)
cp -RL "${skills_dir}/${name}" "${tmpdir}/${name}"
done
vol_run \
-v "${tmpdir}:/tmp/skills-import:ro" \
"$IMAGE" bash -c '
mkdir -p /home/claude/.claude/skills
cp -r /tmp/skills-import/* /home/claude/.claude/skills/
'
rm -rf "$tmpdir"
echo "Done — ${#selected[@]} skill(s) copied."
}
# ── Claude settings ─────────────────────────────────────────
setup_settings() {
local settings="${CLAUDE_DIR}/settings.json"
[[ -f "$settings" ]] || return
echo ""
echo "═══ Claude Settings ═══"
read -rp "Copy ~/.claude/settings.json into container? [Y/n] " answer
answer="${answer:-Y}"
if [[ "$answer" =~ ^[Yy]$ ]]; then
local tmpdir
tmpdir=$(mktemp -d)
cp "$settings" "${tmpdir}/settings.json"
vol_run \
-v "${tmpdir}:/tmp/settings-import:ro" \
"$IMAGE" bash -c '
mkdir -p /home/claude/.claude
cp /tmp/settings-import/settings.json /home/claude/.claude/settings.json
'
rm -rf "$tmpdir"
echo "Done — settings.json copied."
fi
}
# ── Detect whether setup has already run ────────────────────
volume_needs_setup() {
local output
output=$(docker run --rm -v "${VOLUME}:/home/claude" "$IMAGE" \
bash -c "test -f /home/claude/${SETUP_MARKER} && echo exists || echo missing" 2>/dev/null)
[[ "$output" == "missing" ]]
}
mark_setup_done() {
vol_run "$IMAGE" bash -c "touch /home/claude/${SETUP_MARKER}"
}
# ── Run interactive setup ───────────────────────────────────
run_setup() {
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ Claude Docker — Initial Setup ║"
echo "╚══════════════════════════════════════╝"
setup_ssh
setup_skills
setup_settings
mark_setup_done
echo ""
echo "Setup complete. Run with --setup to reconfigure."
echo ""
}
# ── Parse args ──────────────────────────────────────────────
FORCE_SETUP=false
for arg in "$@"; do
case "$arg" in
--setup|-s) FORCE_SETUP=true ;;
esac
done
if $FORCE_SETUP || volume_needs_setup; then
run_setup
fi
# ── Launch container ────────────────────────────────────────
docker run --rm -it \
--security-opt=no-new-privileges:true \
--cap-drop=ALL \
-v "${VOLUME}:/home/claude" \
-v "$(pwd):/workspace:rw" \
-w /workspace \
"$IMAGE" claude

01-claude-docker.sh

Runs Claude Code inside a sandboxed Docker container, mounting the current directory as /workspace. Builds the image (local/claude-code) on first run, reuses it after.

The container drops all Linux capabilities, sets no-new-privileges, and isolates auth state in a named volume (claude-auth) — useful when you want Claude to operate on a repo without giving it host-level access.

Requirements

  • Docker

Usage

# first run — auto-prompts to copy SSH keys & Claude skills
./scripts/01-claude-docker.sh

# re-run setup any time
./scripts/01-claude-docker.sh --setup

What --setup configures

Item Source Destination in volume
SSH keys ~/.ssh/* /home/claude/.ssh/
Claude skills ~/.claude/skills/* /home/claude/.claude/skills/
Claude settings ~/.claude/settings.json /home/claude/.claude/settings.json

Setup runs automatically on first use (fresh volume). Pass --setup or -s to reconfigure at any time. Symlinked skills are resolved and copied as real files.

Installing as claude-docker

Pick one of the options below so you can run the script as claude-docker from any directory.

mkdir -p ~/.local/bin
ln -s "$(pwd)/01-claude-docker.sh" ~/.local/bin/claude-docker
# add to PATH if it isn't already
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc   # or ~/.bashrc

Verify

which claude-docker
claude-docker
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment