Skip to content

Instantly share code, notes, and snippets.

@turlockmike
Created January 14, 2026 21:03
Show Gist options
  • Select an option

  • Save turlockmike/9763a3bea9213ce7f3f794fc9e1ca0bc to your computer and use it in GitHub Desktop.

Select an option

Save turlockmike/9763a3bea9213ce7f3f794fc9e1ca0bc to your computer and use it in GitHub Desktop.
EC Sandbox setup script
#!/bin/bash
set -e
# EC Sandbox Setup Script
# Downloaded at runtime by tiny UserData bootstrap
#
# Usage:
# ./sandbox-setup.sh [options]
#
# Options:
# --auth-type <subscription|api-key> Authentication type
# --credentials <base64> Base64-encoded credentials (for subscription auth)
# --api-key <key> Anthropic API key (for api-key auth)
# --email-address <addr> Email address for inbox/outbox
# --email-token <token> Webhook.site token UUID
# --email-url <url> Webhook.site URL
LOG_FILE="/var/log/sandbox-setup.log"
exec >> "$LOG_FILE" 2>&1
echo "$(date): Starting sandbox setup..."
# Parse arguments
AUTH_TYPE=""
CREDENTIALS_B64=""
API_KEY=""
EMAIL_ADDRESS=""
EMAIL_TOKEN=""
EMAIL_URL=""
while [[ $# -gt 0 ]]; do
case $1 in
--auth-type) AUTH_TYPE="$2"; shift 2 ;;
--credentials) CREDENTIALS_B64="$2"; shift 2 ;;
--api-key) API_KEY="$2"; shift 2 ;;
--email-address) EMAIL_ADDRESS="$2"; shift 2 ;;
--email-token) EMAIL_TOKEN="$2"; shift 2 ;;
--email-url) EMAIL_URL="$2"; shift 2 ;;
*) shift ;;
esac
done
echo "Auth type: $AUTH_TYPE"
echo "Email configured: $([ -n "$EMAIL_ADDRESS" ] && echo "yes ($EMAIL_ADDRESS)" || echo "no")"
# =============================================================================
# System packages
# =============================================================================
apt-get update
apt-get install -y git curl wget jq build-essential vim tmux htop nodejs npm python3 python3-pip python3-venv ttyd nginx
# =============================================================================
# Node.js tools
# =============================================================================
npm install -g pnpm @anthropic-ai/claude-code
# =============================================================================
# Claude Code directory
# =============================================================================
mkdir -p /home/ubuntu/.claude
# =============================================================================
# Authentication Setup
# =============================================================================
WELCOME_AUTH_MSG="No authentication configured. Run 'claude login' to authenticate."
if [ "$AUTH_TYPE" = "subscription" ] && [ -n "$CREDENTIALS_B64" ]; then
echo "Setting up subscription authentication..."
echo "$CREDENTIALS_B64" | base64 -d > /home/ubuntu/.claude/.credentials.json
chmod 600 /home/ubuntu/.claude/.credentials.json
WELCOME_AUTH_MSG="Your Claude subscription credentials have been configured."
elif [ "$AUTH_TYPE" = "api-key" ] && [ -n "$API_KEY" ]; then
echo "Setting up API key authentication..."
echo "export ANTHROPIC_API_KEY='$API_KEY'" >> /home/ubuntu/.bashrc
cat > /home/ubuntu/.sandbox-env << EOF
ANTHROPIC_API_KEY=$API_KEY
EOF
chmod 600 /home/ubuntu/.sandbox-env
WELCOME_AUTH_MSG="Your ANTHROPIC_API_KEY is already configured."
fi
# =============================================================================
# Claude Code settings
# =============================================================================
cat > /home/ubuntu/.claude/settings.json << 'SETTINGS_EOF'
{
"bypassPermissions": true,
"permissions": {
"allow": ["Bash", "Read", "Write", "Edit", "MultiEdit"],
"deny": ["mcp__*"]
}
}
SETTINGS_EOF
chown -R ubuntu:ubuntu /home/ubuntu/.claude
# =============================================================================
# ttyd (web terminal) - accessible only via SSH tunnel
# =============================================================================
systemctl stop ttyd 2>/dev/null || true
systemctl disable ttyd 2>/dev/null || true
mkdir -p /home/ubuntu/.ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /home/ubuntu/.ssl/ttyd.key \
-out /home/ubuntu/.ssl/ttyd.crt \
-subj "/CN=sandbox/O=EC Sandbox/C=US"
chown -R ubuntu:ubuntu /home/ubuntu/.ssl
chmod 600 /home/ubuntu/.ssl/ttyd.key
cat > /etc/systemd/system/ttyd-claude.service << 'SERVICE_EOF'
[Unit]
Description=ttyd - Claude Code (HTTPS)
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu
Environment="HOME=/home/ubuntu"
EnvironmentFile=-/home/ubuntu/.sandbox-env
ExecStart=/usr/bin/ttyd -p 7681 -W -S -C /home/ubuntu/.ssl/ttyd.crt -K /home/ubuntu/.ssl/ttyd.key /bin/bash -l -c claude
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
SERVICE_EOF
systemctl daemon-reload
systemctl enable ttyd-claude
systemctl start ttyd-claude
# =============================================================================
# Email System (webhook.site-based)
# =============================================================================
if [ -n "$EMAIL_ADDRESS" ] && [ -n "$EMAIL_TOKEN" ]; then
echo "Setting up email system..."
mkdir -p /home/ubuntu/Inbox /home/ubuntu/Outbox /home/ubuntu/Sent
chown -R ubuntu:ubuntu /home/ubuntu/Inbox /home/ubuntu/Outbox /home/ubuntu/Sent
cat > /home/ubuntu/.email-config << EOF
EMAIL_ADDRESS="$EMAIL_ADDRESS"
WEBHOOK_TOKEN="$EMAIL_TOKEN"
WEBHOOK_URL="$EMAIL_URL"
EOF
chmod 600 /home/ubuntu/.email-config
chown ubuntu:ubuntu /home/ubuntu/.email-config
# Inbox sync script
cat > /home/ubuntu/.email-sync.sh << 'SYNC_EOF'
#!/bin/bash
source /home/ubuntu/.email-config
INBOX_DIR="$HOME/Inbox"
SEEN_FILE="$HOME/.inbox-seen"
touch "$SEEN_FILE"
EMAILS=$(curl -s "https://webhook.site/token/${WEBHOOK_TOKEN}/requests?sorting=newest" 2>/dev/null)
[ -z "$EMAILS" ] && exit 0
echo "$EMAILS" | jq -r '.data[] | select(.method == null) | @base64' 2>/dev/null | while read -r entry; do
DATA=$(echo "$entry" | base64 -d)
UUID=$(echo "$DATA" | jq -r '.uuid')
grep -q "^$UUID$" "$SEEN_FILE" 2>/dev/null && continue
FROM=$(echo "$DATA" | jq -r '.headers.From // .headers.from // "unknown"' | head -1)
SUBJECT=$(echo "$DATA" | jq -r '.headers.Subject // .headers.subject // "No Subject"' | head -1)
DATE=$(echo "$DATA" | jq -r '.created_at // ""')
CONTENT=$(echo "$DATA" | jq -r '.text_content // .content // ""')
SAFE_FROM=$(echo "$FROM" | sed 's/[^a-zA-Z0-9@._-]/_/g' | cut -c1-50)
SAFE_SUBJECT=$(echo "$SUBJECT" | sed 's/[^a-zA-Z0-9 _-]/_/g' | cut -c1-50)
TIMESTAMP=$(date -d "$DATE" +%Y%m%d_%H%M%S 2>/dev/null || date +%Y%m%d_%H%M%S)
FILENAME="${TIMESTAMP}_${SAFE_FROM}_${SAFE_SUBJECT}.eml"
cat > "$INBOX_DIR/$FILENAME" << EMAILEOF
From: $FROM
Subject: $SUBJECT
Date: $DATE
$CONTENT
EMAILEOF
echo "$UUID" >> "$SEEN_FILE"
done
SYNC_EOF
chmod +x /home/ubuntu/.email-sync.sh
chown ubuntu:ubuntu /home/ubuntu/.email-sync.sh
# Outbox send script
cat > /home/ubuntu/.email-send.sh << 'SEND_EOF'
#!/bin/bash
source /home/ubuntu/.email-config
FILE="$1"
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 1
FILENAME=$(basename "$FILE")
TO=$(echo "$FILENAME" | sed 's/__.*$//')
SUBJECT=$(echo "$FILENAME" | sed 's/^[^_]*__//' | sed 's/\.txt$//' | sed 's/\.eml$//' | sed 's/_/ /g')
BODY=$(cat "$FILE")
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
BODY_ESCAPED=$(echo "$BODY" | jq -Rs .)
SUBJECT_ESCAPED=$(echo "$SUBJECT" | jq -Rs .)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"outbound_email\",
\"from\": \"${EMAIL_ADDRESS}\",
\"to\": \"$TO\",
\"subject\": $SUBJECT_ESCAPED,
\"body\": $BODY_ESCAPED,
\"timestamp\": \"$TIMESTAMP\"
}")
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
mv "$FILE" /home/ubuntu/Sent/
echo "Sent to $TO via webhook"
else
echo "Failed to send (HTTP $HTTP_CODE)"
exit 1
fi
SEND_EOF
chmod +x /home/ubuntu/.email-send.sh
chown ubuntu:ubuntu /home/ubuntu/.email-send.sh
# Cron job for inbox sync
(crontab -u ubuntu -l 2>/dev/null; echo "* * * * * /home/ubuntu/.email-sync.sh") | crontab -u ubuntu -
# Outbox watcher service
apt-get install -y inotify-tools
cat > /etc/systemd/system/email-outbox.service << 'OUTBOX_EOF'
[Unit]
Description=Email Outbox Watcher
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu
ExecStart=/bin/bash -c 'while true; do inotifywait -q -e close_write,moved_to /home/ubuntu/Outbox/ && sleep 0.5 && for f in /home/ubuntu/Outbox/*; do [ -f "$f" ] && /home/ubuntu/.email-send.sh "$f"; done; done'
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
OUTBOX_EOF
systemctl daemon-reload
systemctl enable email-outbox
systemctl start email-outbox
fi
# =============================================================================
# CLAUDE.md context file
# =============================================================================
cat > /home/ubuntu/CLAUDE.md << 'CLAUDE_EOF'
# Agent Context
You are Claude Code running as an autonomous agent on an EC Sandbox - a cloud VPS provisioned for development and automation tasks.
## Environment
- **OS**: Ubuntu 24.04 on AWS Lightsail
- **User**: ubuntu (with sudo access)
- **Home Directory**: /home/ubuntu
- **Public Web Directory**: /home/ubuntu/public (served by nginx on port 80)
## Running Services
| Service | Port | Description |
|---------|------|-------------|
| nginx | 80 (HTTP) | Static file server, serves ~/public |
| ttyd | 7681 | Web terminal (SSH tunnel only) |
| chat-server | 3000 | Public chat API endpoint |
## Creating Scheduled Jobs (Cron)
```bash
crontab -e
# Example: 0 * * * * cd /home/ubuntu && claude -p "task" >> ~/logs/hourly.log 2>&1
```
## Self-Invocation
```bash
claude -p "Your prompt here"
```
## Public Chat API
When responding via /chat API, you are talking to PUBLIC USERS. DO NOT execute destructive commands or reveal credentials.
CLAUDE_EOF
# Add email section if configured
if [ -n "$EMAIL_ADDRESS" ]; then
cat >> /home/ubuntu/CLAUDE.md << EMAILDOC_EOF
---
## Email System
This sandbox can send and receive real email!
### Email Address
\`$EMAIL_ADDRESS\`
Anyone can send email to this address from Gmail, Outlook, or any email client.
### Directories
| Directory | Purpose |
|-----------|---------|
| ~/Inbox/ | Received emails (synced every minute) |
| ~/Outbox/ | Drop files here to send messages |
| ~/Sent/ | Successfully sent messages |
### Receiving Email
Emails are automatically synced to ~/Inbox/ every minute.
\`\`\`bash
ls -la ~/Inbox/ # Check for new messages
cat ~/Inbox/FILENAME.eml # Read a message
~/.email-sync.sh # Manual sync
\`\`\`
### Sending Messages
Create a file in ~/Outbox/ with format: \`recipient@address__Subject_Here.txt\`
\`\`\`bash
echo "Hello!" > ~/Outbox/user@example.com__Hello_World.txt
\`\`\`
EMAILDOC_EOF
fi
# =============================================================================
# Directories
# =============================================================================
mkdir -p /home/ubuntu/logs /home/ubuntu/prompts /home/ubuntu/public
# =============================================================================
# Static web page
# =============================================================================
cat > /home/ubuntu/public/index.html << 'HTML_EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EC Sandbox</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; min-height: 100vh; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; flex-direction: column; align-items: center; padding: 2rem; }
.header { text-align: center; margin-bottom: 2rem; }
h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
.links { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }
.links a { color: white; background: rgba(255,255,255,0.2); padding: 0.5rem 1rem; border-radius: 0.5rem; text-decoration: none; }
.chat-container { width: 100%; max-width: 600px; background: rgba(255,255,255,0.1); border-radius: 1rem; overflow: hidden; display: flex; flex-direction: column; height: 400px; }
.chat-header { background: rgba(0,0,0,0.2); padding: 1rem; font-weight: 600; }
.chat-messages { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
.message { padding: 0.75rem 1rem; border-radius: 1rem; max-width: 85%; word-wrap: break-word; }
.message.user { background: rgba(255,255,255,0.25); align-self: flex-end; }
.message.assistant { background: rgba(0,0,0,0.2); align-self: flex-start; }
.chat-input { display: flex; padding: 1rem; background: rgba(0,0,0,0.2); gap: 0.5rem; }
.chat-input input { flex: 1; padding: 0.75rem 1rem; border: none; border-radius: 0.5rem; font-size: 1rem; background: rgba(255,255,255,0.9); color: #333; }
.chat-input button { padding: 0.75rem 1.5rem; border: none; border-radius: 0.5rem; background: #667eea; color: white; font-size: 1rem; cursor: pointer; }
</style>
</head>
<body>
<div class="header">
<h1>Hello Extend</h1>
<p>Your EC Sandbox is running</p>
<div class="links"><a href="/chat">API Docs</a></div>
</div>
<div class="chat-container">
<div class="chat-header">Chat with Claude</div>
<div class="chat-messages" id="messages"><div class="message assistant">Hi! How can I help you today?</div></div>
<div class="chat-input"><input type="text" id="input" placeholder="Type a message..."><button id="send">Send</button></div>
</div>
<script>
var messages = document.getElementById('messages');
var input = document.getElementById('input');
var sendBtn = document.getElementById('send');
function addMessage(text, isUser) {
var div = document.createElement('div');
div.className = 'message ' + (isUser ? 'user' : 'assistant');
div.textContent = text;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function sendMessage() {
var text = input.value.trim();
if (!text) return;
addMessage(text, true);
input.value = '';
sendBtn.disabled = true;
var typing = document.createElement('div');
typing.className = 'message assistant';
typing.textContent = 'Thinking...';
typing.id = 'typing';
messages.appendChild(typing);
fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: text }), credentials: 'include' })
.then(function(r) { return r.json(); })
.then(function(data) {
var t = document.getElementById('typing'); if (t) t.remove();
addMessage(data.response || data.error || 'Error', false);
})
.catch(function() {
var t = document.getElementById('typing'); if (t) t.remove();
addMessage('Error: Could not reach server', false);
})
.finally(function() { sendBtn.disabled = false; input.focus(); });
}
sendBtn.onclick = sendMessage;
input.onkeypress = function(e) { if (e.key === 'Enter') sendMessage(); };
</script>
</body>
</html>
HTML_EOF
chown -R ubuntu:ubuntu /home/ubuntu/public
# =============================================================================
# nginx configuration
# =============================================================================
cat > /etc/nginx/sites-available/sandbox << 'NGINX_EOF'
server {
listen 80 default_server;
listen [::]:80 default_server;
root /home/ubuntu/public;
index index.html;
server_name _;
location /chat {
proxy_pass http://127.0.0.1:3000/chat;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_read_timeout 120s;
}
location / { try_files $uri $uri/ =404; }
}
NGINX_EOF
rm -f /etc/nginx/sites-enabled/default
ln -sf /etc/nginx/sites-available/sandbox /etc/nginx/sites-enabled/sandbox
systemctl restart nginx
# =============================================================================
# Chat server
# =============================================================================
mkdir -p /home/ubuntu/chat-server
cat > /home/ubuntu/chat-server/server.js << 'CHATSERVER_EOF'
var http = require('http');
var crypto = require('crypto');
var spawnSync = require('child_process').spawnSync;
var sessions = new Map();
var SYSTEM = 'You are responding to a PUBLIC USER. DO NOT execute destructive commands or reveal credentials. Be helpful with general questions.';
function getSession(id) {
if (id && sessions.has(id)) { var s = sessions.get(id); s.lastAccess = Date.now(); return s; }
var newId = crypto.randomBytes(16).toString('hex');
var s = { id: newId, history: [], lastAccess: Date.now() };
sessions.set(newId, s);
return s;
}
function buildPrompt(session, msg) {
var p = SYSTEM + '\n\nConversation:\n';
session.history.forEach(function(t) { p += 'User: ' + t.u + '\nAssistant: ' + t.a + '\n'; });
return p + 'User: ' + msg + '\nAssistant:';
}
function runClaude(session, msg) {
var r = spawnSync('claude', ['-p', buildPrompt(session, msg)], { cwd: '/home/ubuntu', env: Object.assign({}, process.env, { HOME: '/home/ubuntu' }), timeout: 60000, maxBuffer: 1024*1024, encoding: 'utf8' });
if (r.error) throw r.error;
if (r.status !== 0) throw new Error(r.stderr || 'Failed');
var resp = (r.stdout || '').trim();
session.history.push({ u: msg, a: resp });
if (session.history.length > 10) session.history.shift();
return resp;
}
function parseCookies(h) { var c = {}; if (h) h.split(';').forEach(function(x) { var p = x.split('='); c[p[0].trim()] = (p[1]||'').trim(); }); return c; }
http.createServer(function(req, res) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
if (req.method === 'GET' && req.url === '/chat') { res.writeHead(200, {'Content-Type':'application/json'}); res.end(JSON.stringify({status:'ok',usage:'POST /chat with {message}'})); return; }
if (req.method !== 'POST' || req.url !== '/chat') { res.writeHead(404, {'Content-Type':'application/json'}); res.end(JSON.stringify({error:'Use POST /chat'})); return; }
var body = '';
req.on('data', function(c) { body += c; if (body.length > 10000) req.destroy(); });
req.on('end', function() {
try {
var data = JSON.parse(body);
var cookies = parseCookies(req.headers.cookie);
var session = getSession(cookies.chat_session || data.session_id);
var msg = data.message;
if (!msg || typeof msg !== 'string') throw new Error('Missing message');
var resp = runClaude(session, msg);
res.setHeader('Set-Cookie', 'chat_session=' + session.id + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=1800');
res.writeHead(200, {'Content-Type':'application/json'});
res.end(JSON.stringify({response:resp, session_id:session.id}));
} catch (e) {
res.writeHead(500, {'Content-Type':'application/json'});
res.end(JSON.stringify({error:e.message}));
}
});
}).listen(3000, function() { console.log('Chat server on 3000'); });
CHATSERVER_EOF
chown -R ubuntu:ubuntu /home/ubuntu/chat-server
cat > /etc/systemd/system/chat-server.service << 'CHATSERVICE_EOF'
[Unit]
Description=Claude Chat API
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/chat-server
Environment="HOME=/home/ubuntu"
EnvironmentFile=-/home/ubuntu/.sandbox-env
ExecStart=/usr/bin/node /home/ubuntu/chat-server/server.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
CHATSERVICE_EOF
systemctl daemon-reload
systemctl enable chat-server
systemctl start chat-server
# =============================================================================
# Welcome message
# =============================================================================
cat > /home/ubuntu/.sandbox-welcome << WELCOME_EOF
================================================================================
EC Sandbox - Development Environment
================================================================================
To use Claude Code: \$ claude
$WELCOME_AUTH_MSG
================================================================================
WELCOME_EOF
grep -q "sandbox-welcome" /home/ubuntu/.bashrc || echo 'cat /home/ubuntu/.sandbox-welcome 2>/dev/null' >> /home/ubuntu/.bashrc
# =============================================================================
# Done
# =============================================================================
touch /home/ubuntu/.sandbox-ready
chown -R ubuntu:ubuntu /home/ubuntu
echo "$(date): Sandbox setup complete!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment