Plan mode: If you are planning work, this entire skill is ONE plan step: "Invoke /vibes:exe". Do not decompose the steps below into separate plan tasks.
Deploy to exe.dev
Deploy your Vibes app to exe.dev, a VM hosting platform with persistent storage and HTTPS by default.
Prerequisites
-
SSH key in ~/.ssh/ (id_ed25519, id_rsa, or id_ecdsa)
-
exe.dev account - run ssh exe.dev once to create your account and verify email
-
Generated Vibes app - an index.html file ready to deploy
Gather Config Upfront
Use AskUserQuestion to collect deployment config before running the deploy script.
Use the AskUserQuestion tool with these questions:
Question 1: "What VM name should we use? (becomes yourname.exe.xyz)" Header: "VM Name" Options: Suggest based on app name from context + user enters via "Other"
Question 2: "Which file should we deploy?" Header: "File" Options: ["index.html (default)", "Other path"]
Question 3: "Does this app need AI features?" Header: "AI" Options: ["No", "Yes - I have an OpenRouter key"]
Question 4: "Is this a SaaS app with subdomain claiming?" Header: "Registry" Options: ["No - simple static deploy", "Yes - need Clerk keys for registry"]
After Receiving Answers
-
If AI enabled, ask for the OpenRouter API key
-
If Registry enabled, ask for Clerk PEM public key and webhook secret
-
Proceed immediately to deploy - no more questions
Quick Deploy
cd "${CLAUDE_PLUGIN_ROOT}/scripts" && [ -d node_modules ] || npm install node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html
What It Does
-
Creates VM on exe.dev via SSH CLI
-
Starts nginx (pre-installed on exeuntu image)
-
Uploads your index.html to /var/www/html/
-
Generates HANDOFF.md - context document for remote Claude
-
For Connect-enabled deployments, includes full Docker setup documentation (commands, troubleshooting, service endpoints)
-
Makes VM public via ssh exe.dev share set-public <vmname>
-
Verifies public access at https://myapp.exe.xyz
AI-Enabled Apps
For apps using the useAI hook, deploy with the --ai-key flag:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html --ai-key "sk-or-v1-..."
This sets up a secure AI proxy:
-
Installs Bun runtime
-
Creates /home/exedev/proxy.js
-
proxies /api/ai/* to OpenRouter
-
Configures systemd service for the proxy
-
Adds nginx reverse proxy from port 80/443 to Bun (port 3001)
IMPORTANT: Do not manually set up AI proxying. Manual nginx config changes can overwrite SSL settings and miss the Bun/proxy.js service. Always use the deploy script with --ai-key .
Multi-Tenant AI Apps
For SaaS apps with per-tenant AI:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html --ai-key "sk-or-v1-..." --multi-tenant
Registry Server
For SaaS apps using subdomain claiming (from /vibes:sell ), deploy with Clerk credentials:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --file index.html
--clerk-key "cat clerk-public-key.pem"
--clerk-webhook-secret "whsec_xxx"
--reserved "admin,api,billing"
This sets up a subdomain registry server:
-
Installs Bun runtime to /usr/local/bin/bun
-
Creates /var/www/registry-server.ts with Clerk JWT verification
-
Configures systemd service (port 3002)
-
Adds nginx proxy for /registry.json , /check/* , /claim , /webhook
Registry Endpoints
Endpoint Method Auth Description
/registry.json
GET None Public read of all claims
/check/{subdomain}
GET None Check availability
/claim
POST Bearer JWT Claim subdomain for user
/webhook
POST Svix sig Clerk subscription events
Getting the Clerk Public Key
The registry server needs Clerk's PEM public key to verify JWTs for the /claim endpoint.
Option 1: From Clerk Dashboard
-
Go to Clerk Dashboard → API Keys
-
Scroll to "PEM Public Key" or click "Show JWT Public Key"
-
Copy the full key (starts with -----BEGIN PUBLIC KEY----- )
Option 2: From JWKS endpoint
Get your Clerk frontend API domain from dashboard
curl https://YOUR_CLERK_DOMAIN/.well-known/jwks.json
Then convert the JWK to PEM format using an online tool or jose CLI.
Passing to deploy script:
From a file
node deploy-exe.js --clerk-key "cat clerk-public-key.pem" ...
Inline (escape newlines)
node deploy-exe.js --clerk-key "-----BEGIN PUBLIC KEY-----\nMIIB...\n-----END PUBLIC KEY-----" ...
Manual configuration on server:
ssh myapp.exe.dev sudo nano /etc/registry.env
Add: CLERK_PEM_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
sudo systemctl restart vibes-registry
Continue Development on the VM
Claude is pre-installed on exe.dev VMs. After deployment, you can continue development remotely:
ssh myapp.exe.dev -t "cd /var/www/html && claude"
The HANDOFF.md file provides context about what was built, so Claude can continue meaningfully.
Manual Public Access
If the deploy script doesn't make the VM public automatically, run:
ssh exe.dev share set-public myapp
Multi-Tenant Apps
For apps that need tenant isolation (e.g., alice.myapp.com , bob.myapp.com ):
Client-Side Isolation
The same index.html serves all subdomains. JavaScript reads the hostname and uses the subdomain as a Fireproof database prefix:
// In your app:
const hostname = window.location.hostname;
const subdomain = hostname.split('.')[0];
const dbName = myapp-${subdomain};
// Each subdomain gets its own Fireproof database const { database } = useFireproof(dbName);
Custom Domain Setup
Add --domain flag:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp --domain myapp.com
Configure wildcard DNS at your DNS provider:
*.myapp.com CNAME myapp.exe.xyz myapp.com ALIAS exe.xyz
Set up wildcard SSL on the VM:
ssh myapp.exe.dev
sudo apt install certbot
sudo certbot certonly --manual --preferred-challenges dns
-d "myapp.com" -d "*.myapp.com"
CLI Options
Option Description
--name <vm>
VM name (required)
--file <path>
HTML file to deploy (default: index.html)
--domain <domain>
Custom domain for wildcard setup
--ai-key <key>
OpenRouter API key for AI features
--multi-tenant
Enable subdomain-based multi-tenancy
--tenant-limit <$>
Credit limit per tenant in dollars (default: 5)
--clerk-key <pem>
Clerk PEM public key for JWT verification
--clerk-webhook-secret <secret>
Clerk webhook signing secret
--reserved <list>
Comma-separated reserved subdomain names
--preallocated <list>
Pre-claimed subdomains (format: sub:user_id )
--dry-run
Show commands without executing
--skip-verify
Skip deployment verification
Redeployment
After making changes, redeploy with:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name myapp
SSH Access
Access your VM directly:
ssh myapp.exe.dev
Architecture
exe.dev VM (exeuntu image) ├── nginx (serves all subdomains via server_name _) ├── claude (pre-installed CLI) ├── /usr/local/bin/bun ← Bun runtime (system-wide) ├── /var/www/html/ │ ├── index.html ← Your Vibes app │ └── HANDOFF.md ← Context for remote Claude ├── (with --ai-key) │ ├── /opt/vibes/proxy.js ← AI proxy service (port 3001) │ └── vibes-proxy.service ← systemd unit └── (with --clerk-key) ├── /var/www/registry-server.ts ← Registry service (port 3002) ├── /var/www/html/registry.json ← Subdomain claims data └── vibes-registry.service ← systemd unit
Port Assignments
Service Port Purpose
AI Proxy 3001 OpenRouter proxy for useAI hook
Registry 3002 Subdomain claim/check API
-
No server-side logic - pure static hosting (unless using AI proxy)
-
Persistent disk - survives restarts
-
HTTPS by default - exe.dev handles SSL for *.exe.xyz
-
Claude pre-installed - continue development on the VM
Post-Deploy Debugging
After deployment, always work with local files - they are the source of truth. SSHing to read deployed files is slow and wastes tokens.
Task Use Local Use SSH
Editing/debugging code ✅ Always ❌ Never
Checking console errors ✅ Local file ❌ No need
Verifying deploy ❌ ✅ curl https://vm.exe.xyz
Server-specific issues ❌ ✅ Only if local works but remote doesn't
To redeploy after local fixes:
node "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-exe.js" --name <vmname> --file index.html
SSL Configuration
The deploy script preserves existing SSL by using include files for AI proxy config. When manually editing nginx:
-
Never replace the entire config - only add/modify specific blocks
-
Check for SSL first: Look for listen 443 ssl in the config
-
Use includes: Put new configs in /etc/nginx/conf.d/ or separate files
-
Test before reload: sudo nginx -t
Manual File Transfer to VMs
When manually transferring files (outside the deploy script), use the two-stage pattern.
Key distinction:
-
exe.dev = orchestrator (for VM management: ssh exe.dev new , ssh exe.dev share )
-
<app>.exe.xyz = actual VM (for file operations, SSH access)
Reliable pattern:
Upload: SCP to server temp → sudo move to /var/www/html/
scp index.html myapp.exe.xyz:/tmp/ ssh myapp.exe.xyz "sudo cp /tmp/index.html /var/www/html/"
Download: Direct SCP works fine
scp myapp.exe.xyz:/var/www/html/index.html ./downloaded.html
Why server-side temp?
-
Direct SCP to /var/www/html/ fails (permission denied - owned by www-data)
-
Server /tmp/ is world-writable, so SCP succeeds
-
sudo cp moves file with correct ownership
Common mistakes:
Mistake Error Fix
ssh exe.dev cat /var/www/...
"No VMs found" Use ssh <app>.exe.xyz
scp file vm:/var/www/html/
Permission denied Use temp + sudo pattern
Forgetting sudo for /var/www Permission denied Always sudo cp for www-data dirs
Quick reference commands:
Connect to VM
ssh <app>.exe.xyz
Read file
ssh <app>.exe.xyz "cat /var/www/html/index.html"
Upload file (two-stage)
scp index.html <app>.exe.xyz:/tmp/ ssh <app>.exe.xyz "sudo cp /tmp/index.html /var/www/html/"
Download file
scp <app>.exe.xyz:/var/www/html/index.html ./downloaded.html
Verify
ssh <app>.exe.xyz "head -20 /var/www/html/index.html"
What's Next?
After successful deployment, present these options using AskUserQuestion:
Question: "Your app is live at https://${name}.exe.xyz! What's next?" Header: "Next" Options:
-
Label: "Share my URL" Description: "Get the shareable link for your app. I'll confirm the public URL and you can send it to anyone - they'll see your app immediately with full functionality."
-
Label: "Make changes and redeploy" Description: "Continue iterating locally. Edit your files here, then run deploy again to push updates. The VM keeps running so there's zero downtime during updates."
-
Label: "Continue development on VM" Description: "Work directly on the server. SSH in and use the pre-installed Claude to make changes live. Great for server-specific debugging or when you want changes to persist immediately."
-
Label: "I'm done for now" Description: "Wrap up this session. Your app stays live at the URL - it runs 24/7 on exe.dev's persistent VMs. Come back anytime to make updates."
After user responds:
-
"Share URL" → Confirm "Your app is live at https://${name}.exe.xyz - share this link!"
-
"Make changes" → Acknowledge, stay ready for local edits
-
"Continue on VM" → Provide: ssh ${name}.exe.dev -t "cd /var/www/html && claude"
-
"I'm done" → Confirm app stays live, wish them well