expose
Expose a local service on Tailscale with a unique path
Best use case
expose is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Expose a local service on Tailscale with a unique path
Teams using expose should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/expose/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How expose Compares
| Feature / Agent | expose | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Expose a local service on Tailscale with a unique path
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# /expose - Expose Local Services via Tailscale
Expose a local service on Tailscale with a unique path, preserving the root path for the main application.
## Usage
```
/expose <port> [service-name]
```
## Examples
```
/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
```
## Implementation
When called, this command will:
1. **Auto-Detect Dev Server (if no port specified)**
- Search for common dev server ports: 3000, 3001, 4200, 5173, 5174, 8000, 8080, 8081
- Check for running processes with common dev commands: `npm run dev`, `yarn dev`, `vite`, `next dev`
- Use `lsof` to find the actual port being used
- If multiple found, prompt user to choose
2. **Check Tailscale Status**
- Verify Tailscale is running
- Get current Tailscale IP and hostname
3. **Generate Unique Path**
- If service-name provided: use `/service-name`
- Otherwise: generate random path like `/svc-<random-8-chars>`
- Ensure path doesn't conflict with existing services
3. **Preserve Root Path**
- Never override the root path `/`
- Keep track of which service owns root (if any)
4. **Expose Service**
```bash
tailscale serve --set-path /<unique-path> --bg http://localhost:<port>
```
5. **Store Service Mapping**
- Save to `.claude/tailscale-services.json`:
```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"
}
]
}
```
6. **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
```
## Command Logic
```bash
#!/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="$HOME/{{TOOL_DIR}}/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 "$HOME/{{TOOL_DIR}}/tailscale-services.json" ]; then
echo ""
echo "📋 Service Registry:"
jq -r '.services[] | " \(.name): \(.direct) (path: \(.path))"' "$HOME/{{TOOL_DIR}}/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" "$HOME/{{TOOL_DIR}}/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\"))" "$HOME/{{TOOL_DIR}}/tailscale-services.json" > "${SERVICES_FILE}.tmp" && mv "${SERVICES_FILE}.tmp" "$SERVICES_FILE"
echo "✅ Service removed from ${PATH_NAME}"
}
```
## Features
- **Auto-generates unique paths** to avoid conflicts
- **Preserves root path** for main application
- **Tracks all exposed services** in JSON file
- **Provides both HTTPS and direct access URLs**
- **Handles SPAs correctly** by recommending direct port access
- **Supports custom service names** or auto-generation
- **Lists all exposed services** with `/expose list`
- **Removes services** with `/expose remove <port|path>`
## Related Commands
- `/expose list` - Show all exposed services
- `/expose remove <port>` - Unexpose a service
- `/expose clear` - Remove all exposed services (except root)Related Skills
workflow
Guide through structured delivery workflow with plan, implement, validate phases
webapp-testing
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
validate
Verify implementation against specifications
ui-ux-pro-max
UI/UX design intelligence. 67 styles, 96 palettes, 57 font pairings, 25 charts, 13 stacks (React, Next.js, Vue, Svelte, Astro, Nuxt, SwiftUI, React Native, Flutter, Tailwind, shadcn/ui, Jetpack Compose). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient.
tui-style-guide
TUI style guide for consistent terminal interface design
token-usage
Show Claude Code token usage across sessions — daily, weekly, per-project, and per-session breakdowns. Parses {{HOME_TOOL_DIR}}/projects/**/*.jsonl for consumption data. Use when the user asks about token usage, costs, how many tokens were used, session statistics, or wants a usage report.
tmux-status
Show status of all tmux sessions including dev environments, spawned agents, and running processes
tmux-monitor
Monitor and report status of all tmux sessions including dev environments, spawned agents, and running processes. Uses tmuxwatch for enhanced visibility.
tmux-message
Reliable peer-to-peer message delivery to other Claude Code instances via tmux send-keys. Use as a fallback when claude-peers MCP send_message fails to surface in the receiver's inbox (delivered server-side but receiver never picks it up — observed behaviour). Also use when sending a directive to a known Claude Code TUI session by tmux session name or fuzzy hint, or when injecting a multi-line directive into a peer's prompt and submitting it. Trigger phrases — "claude-peers fallback", "tmux send-keys", "send to peer via tmux", "inject directive", "deliver to nanoclaw/hermes peer", "peer message". Tmux-only — won't reach peers running outside tmux.
test-driven-development
Use when implementing any feature or bugfix, before writing implementation code. Enforces RED-GREEN-REFACTOR cycle with test-first approach.
test-ainb
Run tests for the ainb (agents-in-a-box) Rust workspace via a 5-layer strategy — unit, insta snapshot, mock-plugin compositing, real-plugin spawn, vhs recording. Wraps cargo + insta + vhs into one CLI. Use when Stevie says "/test-ainb", "test ainb", "run ainb tests", "snapshot <component>", "regenerate vhs tapes", or any phrasing about validating ainb test layers. The skill autodetects which ainb-tui worktree the cwd sits in and dispatches to scripts/run.sh.
sync-learnings
Sync user-level agent config changes back to toolkit repository (works for Claude, Codex, Copilot)