toolkit/packages/skills/expose/SKILL.md
Expose a local service on Tailscale with a unique path
npx skillsauth add stevengonsalvez/agents-in-a-box exposeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expose a local service on Tailscale with a unique path, preserving the root path for the main application.
/expose <port> [service-name]
/expose # Auto-detects running dev server and exposes it
/expose 3000 # Exposes localhost:3000 with auto-generated path
/expose 8080 api # Exposes localhost:8080 as /api
/expose 5173 vite-app # Exposes localhost:5173 as /vite-app
When called, this command will:
Auto-Detect Dev Server (if no port specified)
npm run dev, yarn dev, vite, next devlsof to find the actual port being usedCheck Tailscale Status
Generate Unique Path
/service-name/svc-<random-8-chars>Preserve Root Path
/Expose Service
tailscale serve --set-path /<unique-path> --bg http://localhost:<port>
Store Service Mapping
.claude/tailscale-services.json:{
"root_service": {
"port": 6802,
"name": "lipi",
"protected": true
},
"services": [
{
"port": 3000,
"path": "/svc-a8f3d2c1",
"name": "frontend",
"url": "https://example-hostname.example.ts.net/svc-a8f3d2c1",
"direct": "http://100.64.1.2:3000",
"created": "2024-01-15T10:30:00Z"
}
]
}
Display Access Info
✅ Service exposed successfully!
📍 Service: frontend (port 3000)
🔗 HTTPS Path: https://example-hostname.example.ts.net/svc-a8f3d2c1
🔗 Direct Access: http://100.64.1.2:3000
🔗 Hostname Access: http://example-hostname:3000
💡 For SPAs, use direct access URLs to avoid routing issues
#!/bin/bash
# Auto-detect running dev server
auto_detect_dev_server() {
# Common dev server ports
COMMON_PORTS=(3000 3001 3002 4200 4321 5173 5174 8000 8080 8081 6802 4013)
echo "🔍 Searching for running dev servers..."
FOUND_SERVICES=()
for port in "${COMMON_PORTS[@]}"; do
if lsof -i :$port >/dev/null 2>&1; then
# Get process info
PROCESS=$(lsof -i :$port | grep LISTEN | awk '{print $1}' | head -1)
FOUND_SERVICES+=("$port:$PROCESS")
echo " Found: $PROCESS on port $port"
fi
done
# Also check for common dev processes
DEV_PROCESSES=("next" "vite" "webpack" "parcel" "snowpack" "turbopack")
for proc in "${DEV_PROCESSES[@]}"; do
PORTS=$(lsof -i -P | grep -i "$proc" | grep LISTEN | awk '{print $9}' | cut -d: -f2 | sort -u)
for port in $PORTS; do
if [[ ! " ${FOUND_SERVICES[@]} " =~ " $port:" ]]; then
FOUND_SERVICES+=("$port:$proc")
echo " Found: $proc on port $port"
fi
done
done
if [ ${#FOUND_SERVICES[@]} -eq 0 ]; then
echo "❌ No running dev servers found"
echo " Start your dev server first, then run /expose"
return 1
elif [ ${#FOUND_SERVICES[@]} -eq 1 ]; then
# Only one found, use it
PORT=$(echo "${FOUND_SERVICES[0]}" | cut -d: -f1)
PROCESS=$(echo "${FOUND_SERVICES[0]}" | cut -d: -f2)
echo "✅ Auto-detected: $PROCESS on port $PORT"
return $PORT
else
# Multiple found, let user choose
echo ""
echo "Multiple dev servers found. Which one to expose?"
for i in "${!FOUND_SERVICES[@]}"; do
PORT=$(echo "${FOUND_SERVICES[$i]}" | cut -d: -f1)
PROCESS=$(echo "${FOUND_SERVICES[$i]}" | cut -d: -f2)
echo " $((i+1)). $PROCESS on port $PORT"
done
read -p "Enter number (1-${#FOUND_SERVICES[@]}): " choice
if [[ $choice -ge 1 && $choice -le ${#FOUND_SERVICES[@]} ]]; then
PORT=$(echo "${FOUND_SERVICES[$((choice-1))]}" | cut -d: -f1)
return $PORT
else
echo "❌ Invalid choice"
return 1
fi
fi
}
expose_service() {
local PORT=$1
local SERVICE_NAME=$2
# If no port specified, auto-detect
if [ -z "$PORT" ]; then
auto_detect_dev_server
PORT=$?
if [ $PORT -eq 1 ]; then
return 1
fi
# Auto-generate service name based on detected process
if [ -z "$SERVICE_NAME" ]; then
SERVICE_NAME="dev-$PORT"
fi
fi
# Get Tailscale info
TAILSCALE_IP=$(tailscale ip -4)
TAILSCALE_HOSTNAME=$(tailscale status --self --peers=false | awk '{print $2}')
TAILSCALE_DOMAIN="${TAILSCALE_HOSTNAME}.example.ts.net"
# Generate path
if [ -z "$SERVICE_NAME" ]; then
RANDOM_ID=$(openssl rand -hex 4)
PATH_NAME="/svc-${RANDOM_ID}"
SERVICE_NAME="service-${PORT}"
else
PATH_NAME="/${SERVICE_NAME}"
fi
# Check if port is already exposed
EXISTING=$(tailscale serve status --json | jq -r ".Web[\"${TAILSCALE_DOMAIN}:443\"].Handlers[\"${PATH_NAME}\"].Proxy")
if [ "$EXISTING" != "null" ]; then
echo "⚠️ Path ${PATH_NAME} is already in use"
echo " Current proxy: ${EXISTING}"
echo " Generate new path? (y/n)"
read -r response
if [ "$response" = "y" ]; then
RANDOM_ID=$(openssl rand -hex 4)
PATH_NAME="/svc-${RANDOM_ID}"
else
return 1
fi
fi
# Expose the service
tailscale serve --set-path ${PATH_NAME} --bg http://localhost:${PORT}
# Save to tracking file
SERVICES_FILE="/.claude/tailscale-services.json"
# Create file if doesn't exist
if [ ! -f "$SERVICES_FILE" ]; then
echo '{"root_service": null, "services": []}' > "$SERVICES_FILE"
fi
# Add service to tracking
NEW_SERVICE=$(jq -n \
--arg port "$PORT" \
--arg path "$PATH_NAME" \
--arg name "$SERVICE_NAME" \
--arg url "https://${TAILSCALE_DOMAIN}${PATH_NAME}" \
--arg direct "http://${TAILSCALE_IP}:${PORT}" \
--arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{port: $port, path: $path, name: $name, url: $url, direct: $direct, created: $created}')
jq ".services += [$NEW_SERVICE]" "$SERVICES_FILE" > "${SERVICES_FILE}.tmp" && mv "${SERVICES_FILE}.tmp" "$SERVICES_FILE"
# Display results
echo "✅ Service exposed successfully!"
echo ""
echo "📍 Service: ${SERVICE_NAME} (port ${PORT})"
echo "🔗 HTTPS Path: https://${TAILSCALE_DOMAIN}${PATH_NAME}"
echo "🔗 Direct Access: http://${TAILSCALE_IP}:${PORT}"
echo "🔗 Hostname Access: http://${TAILSCALE_HOSTNAME}:${PORT}"
echo ""
echo "💡 For SPAs, use direct access URLs to avoid routing issues"
}
# List exposed services
list_services() {
echo "🌐 Exposed Services on Tailscale"
echo "================================"
# Show Tailscale serve status
tailscale serve status
# Show tracked services
if [ -f "/.claude/tailscale-services.json" ]; then
echo ""
echo "📋 Service Registry:"
jq -r '.services[] | " \(.name): \(.direct) (path: \(.path))"' "/.claude/tailscale-services.json"
fi
}
# Remove service
unexpose_service() {
local PATH_OR_PORT=$1
if [[ "$PATH_OR_PORT" =~ ^[0-9]+$ ]]; then
# It's a port, find the path
PATH_NAME=$(jq -r ".services[] | select(.port == \"$PATH_OR_PORT\") | .path" "/.claude/tailscale-services.json")
else
PATH_NAME="$PATH_OR_PORT"
fi
if [ -z "$PATH_NAME" ]; then
echo "❌ Service not found"
return 1
fi
# Remove from Tailscale
tailscale serve clear ${PATH_NAME}
# Remove from tracking
jq "del(.services[] | select(.path == \"$PATH_NAME\"))" "/.claude/tailscale-services.json" > "${SERVICES_FILE}.tmp" && mv "${SERVICES_FILE}.tmp" "$SERVICES_FILE"
echo "✅ Service removed from ${PATH_NAME}"
}
/expose list/expose remove <port|path>/expose list - Show all exposed services/expose remove <port> - Unexpose a service/expose clear - Remove all exposed services (except root)documentation
Report reflect drain spend over a time window — tokens split by cached (cache_read), uncached writes (cache_creation), and io (input+output), with a $ estimate, grouped by day / outcome / model / transcript. Reads the drainer's cost log and surfaces outlier runs and cache-reuse health (the 41.5M-token failure mode = low cache reuse + high cache writes). Use to answer "what is reflection costing me" for the last day / week.
development
Show fleet status — every claude session running on the host, merged across ainb + claude-peers broker + background jobs. Use when you need to enumerate sessions before composing an action, see which sessions have a peer registered (broker-routable) vs tmux-only, check the `summary` of each session, or pipe the list into jq for filtering. Default output: text table. Pass --format json for LLM consumption.
testing
Ordered multi-step prompts to fleet targets, ack-gated between steps via JSONL assistant-turn-end detection. Use for cycles like disconnect→reconnect→verify, or any flow where step N+1 requires step N to have completed first. The skill BLOCKS until each target's transcript shows the next assistant turn finishing OR per-step timeout fires (default 300s).
development
Center control panel — enumerate every claude session that is blocked waiting on something: a user answer (AskUserQuestion fired), an API error retry, an idle assistant turn-end with no follow-up, or an explicit WAITING: marker. Returns rich JSON with signal kind + context per session. Use this when you've stepped away from the fleet and want one place to see everything that wants your attention and answer it.