Skip to content

Instantly share code, notes, and snippets.

@jacobparis
Created January 17, 2026 21:30
Show Gist options
  • Select an option

  • Save jacobparis/55853947d1f6c2a310b5c7d368a99493 to your computer and use it in GitHub Desktop.

Select an option

Save jacobparis/55853947d1f6c2a310b5c7d368a99493 to your computer and use it in GitHub Desktop.
run claude in a vercel sandbox overnight

sandbox.sh

Run Claude in an isolated Vercel Sandbox with full git access.

It uses the sandbox ID (sbx_asdfasdfasdf) as the branch name, so it's safe to push and then you can get your changes by pulling them from that branch

Examples

Test permissions are set up properly

./sandbox.sh bash -c '
  claude -p "who are you?" > who.txt
  git add who.txt
  git commit -m "Add who.txt from Claude"
'

Ralph loop

Create a ./spec.md file that contains your project plan

> claude "Lets plan a tool that tracks what I spend time on, interview me for details and write them to ./spec.md"

Then when you're happy, copy that spec into the sandbox and run claude in a loop

./sandbox.sh --copy spec.md bash -c '
  for i in $(seq 1 25); do
    echo "=== Iteration $i/25 ==="
    claude --dangerously-skip-permissions "
      Study ./spec.md and pick the most important thing to do.

      Do not assert anything is correct that has not been verified
      When the task is complete
      - Update the spec with what has been done, what was learned, and what was a bad idea.
      - Be as brief as possible and then even more brief.
      - commit the changes. you are running in a special branch just for this task
    "
  done
'
#!/usr/bin/env bash
#
# Creates a Vercel Sandbox with Claude and GitHub CLI installed
# Clones your current project and creates a new branch in the sandbox
# Runs the command in the sandbox and pushes the branch back to the original repository
# Usage:
# ./sandbox.sh [--copy <file>...] <command> [args...]
#
set -e
# Parse --copy flags
FILES_TO_COPY=()
while [[ $# -gt 0 ]]; do
case $1 in
--copy)
FILES_TO_COPY+=("$2")
shift 2
;;
*)
break
;;
esac
done
if [ $# -eq 0 ]; then
echo "Usage: sandbox.sh [--copy <file>...] <command> [args...]"
echo "Example:
sandbox.sh --copy spec.md bash -c '
claude -p \"Study spec.md and do the thing\"
'"
exit 1
fi
# Check for AI Gateway API key in keychain
# This is better than passing it in as an environment variable because it's not logged to the console.
# To set it, run:
# security add-generic-password -a "$USER" -s "AI_GATEWAY_API_KEY" -w
#
ANTHROPIC_TOKEN=$(security find-generic-password -a "$USER" -s "AI_GATEWAY_API_KEY" -w 2>/dev/null || true)
if [ -z "$ANTHROPIC_TOKEN" ]; then
echo "Error: No AI Gateway API key found in keychain."
echo ""
echo "To add your token, run:"
echo " security add-generic-password -a \"\$USER\" -s \"AI_GATEWAY_API_KEY\" -w"
echo ""
echo "Or get a token from: https://console.anthropic.com/settings/keys"
exit 1
fi
# Get git info from local
REPO_URL=$(git config --get remote.origin.url)
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main")
GIT_USER_NAME=$(git config --get user.name || echo "Sandbox User")
GIT_USER_EMAIL=$(git config --get user.email || echo "sandbox@example.com")
# Extract owner/repo from URL
REPO_PATH=$(echo "$REPO_URL" | sed -E 's|.*github\.com[:/](.+)(\.git)?$|\1|' | sed 's/\.git$//')
echo "┌──────────────────────────────────────────────────"
echo "│ Repository: $REPO_PATH"
echo "│ Branch: $BRANCH"
if [ ${#FILES_TO_COPY[@]} -gt 0 ]; then
echo "│ Copy: ${FILES_TO_COPY[*]}"
fi
echo "│ Command: $*"
echo "└──────────────────────────────────────────────────"
echo
# Create sandbox
printf "Creating sandbox... "
SANDBOX_ID=$(sandbox create --timeout 10m 2>&1 | grep -o 'sbx_[a-zA-Z0-9]*')
echo "$SANDBOX_ID"
# Install Claude Code CLI (silent)
printf "Installing Claude CLI... "
sandbox exec "$SANDBOX_ID" npm install -g @anthropic-ai/claude-code @antfu/ni > /dev/null 2>&1
echo ""
# Create Claude config with token from local keychain (using sandbox copy to avoid logging)
CLAUDE_CONFIG_TMP=$(mktemp)
cat > "$CLAUDE_CONFIG_TMP" << EOF
{"env":{"ANTHROPIC_BASE_URL":"https://ai-gateway.vercel.sh","ANTHROPIC_API_KEY":"$ANTHROPIC_TOKEN"}}
EOF
sandbox exec "$SANDBOX_ID" mkdir -p .claude > /dev/null 2>&1
sandbox copy "$CLAUDE_CONFIG_TMP" "$SANDBOX_ID":.claude/settings.json > /dev/null 2>&1
rm "$CLAUDE_CONFIG_TMP"
# Copy Claude skills and commands from local
printf "Copying Claude skills... "
if [ -d "$HOME/.claude/skills" ]; then
sandbox exec "$SANDBOX_ID" mkdir -p .claude/skills > /dev/null 2>&1
for skill in "$HOME/.claude/skills"/*; do
[ -e "$skill" ] && sandbox copy "$skill" "$SANDBOX_ID":.claude/skills/ > /dev/null 2>&1
done
fi
if [ -d "$HOME/.claude/commands" ]; then
sandbox exec "$SANDBOX_ID" mkdir -p .claude/commands > /dev/null 2>&1
for cmd in "$HOME/.claude/commands"/*; do
[ -e "$cmd" ] && sandbox copy "$cmd" "$SANDBOX_ID":.claude/commands/ > /dev/null 2>&1
done
fi
echo ""
# Install GitHub CLI (silent)
printf "Installing GitHub CLI... "
sandbox exec "$SANDBOX_ID" -- bash -c "curl -fsSL https://github.com/cli/cli/releases/download/v2.67.0/gh_2.67.0_linux_amd64.tar.gz | tar xz && sudo mv gh_2.67.0_linux_amd64/bin/gh /usr/local/bin/ && rm -rf gh_2.67.0_linux_amd64" > /dev/null 2>&1
echo ""
# Interactive GitHub login
echo ""
echo "GitHub authentication required:"
sandbox exec --interactive --tty "$SANDBOX_ID" gh auth login
echo ""
# Clone repository (silent)
printf "Cloning repository... "
sandbox exec "$SANDBOX_ID" -- bash -c "gh repo clone '$REPO_PATH' /tmp/repo -- --branch '$BRANCH' --single-branch && shopt -s dotglob && mv /tmp/repo/* . && rm -rf /tmp/repo" > /dev/null 2>&1
echo ""
# Configure git identity (silent)
sandbox exec "$SANDBOX_ID" git config user.email "$GIT_USER_EMAIL" > /dev/null 2>&1
sandbox exec "$SANDBOX_ID" git config user.name "$GIT_USER_NAME" > /dev/null 2>&1
sandbox exec "$SANDBOX_ID" git config push.autoSetupRemote true > /dev/null 2>&1
# Install project dependencies
printf "Installing dependencies... "
sandbox exec "$SANDBOX_ID" ni install --silent > /dev/null 2>&1
echo ""
# Create branch (silent)
printf "Creating branch... "
sandbox exec "$SANDBOX_ID" git checkout -b "$SANDBOX_ID" > /dev/null 2>&1
echo ""
# Copy specified files from working tree
if [ ${#FILES_TO_COPY[@]} -gt 0 ]; then
printf "Copying files... "
for file in "${FILES_TO_COPY[@]}"; do
if [ -e "$file" ]; then
# Create parent directory if needed
dir=$(dirname "$file")
if [ "$dir" != "." ]; then
sandbox exec "$SANDBOX_ID" mkdir -p "$dir" > /dev/null 2>&1
fi
sandbox copy "$file" "$SANDBOX_ID":"$file" > /dev/null 2>&1
else
echo ""
echo "Warning: File not found: $file"
fi
done
echo ""
fi
# Run the command
echo ""
echo "┌──────────────────────────────────────────────────"
echo "│ Running: $*"
echo "└──────────────────────────────────────────────────"
echo ""
sandbox exec "$SANDBOX_ID" "$@" > /dev/null 2>&1
EXIT_CODE=$?
echo ""
# Push the branch (silent)
printf "Pushing branch... "
sandbox exec "$SANDBOX_ID" git push > /dev/null 2>&1
echo ""
echo ""
echo "┌──────────────────────────────────────────────────"
echo "│ ✓ Complete"
echo "│ Branch: $SANDBOX_ID"
echo "│ PR: https://github.com/$REPO_PATH/pull/new/$SANDBOX_ID"
echo "└──────────────────────────────────────────────────"
exit $EXIT_CODE
@robkebab
Copy link

@jacobparis this rocks! Thank you 🙏

@robkebab
Copy link

For me, I found it useful to add the --scope and --timeout flags that get passed to the sandbox cli. I threw it in this fork if you're interested.

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