
Tailscale
- 12 installs
- 610 repo stars
- Updated June 26, 2026
- alsk1992/cloddsbot
tailscale is a skill that shares local services over a Tailscale tailnet with Serve and exposes them publicly with Funnel.
About
This skill wraps Tailscale to share local services over a private tailnet using Serve and to expose ports publicly using Funnel. It also reports network status, lists and pings peers, and transfers files between machines. A developer uses it to reach a locally running bot or dev server remotely, or to make a webhook endpoint publicly accessible.
- Shares local services over a Tailscale tailnet with Serve (private)
- Exposes local ports to the public internet with Funnel
- Peer status, ping, and file transfer over the tailnet
Tailscale by the numbers
- 12 all-time installs (skills.sh)
- Ranked #836 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
tailscale capabilities & compatibility
Requires a Tailscale account and optional TAILSCALE_AUTHKEY; Tailscale has a free tier.
- Capabilities
- port sharing · public tunnel · peer networking · file transfer
- Use cases
- devops
- Runs
- Runs locally
- Pricing
- Bring your own API key
What tailscale says it does
Share local services via Tailscale Serve (private) and Funnel (public internet access).
Add authentication** — Funnel bypasses Tailscale auth
npx skills add https://github.com/alsk1992/cloddsbot --skill tailscaleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 610 |
| Last updated | June 26, 2026 |
| Repository | alsk1992/cloddsbot ↗ |
What it does
Share a local service privately over a tailnet or publicly via Tailscale Funnel.
Who is it for?
Reaching a locally running bot or dev server remotely, or exposing a webhook endpoint.
Skip if: Managing cloud provider infrastructure or container orchestration.
When should I use this skill?
You need to share a local port privately over a tailnet or publicly to the internet.
What you get
The service is reachable over the tailnet (Serve) or the public internet (Funnel).
- Private tailnet URL (Serve)
- Public internet URL (Funnel)
By the numbers
- Serve vs Funnel comparison across 4 attributes
- 3 use-case examples
Files
Tailscale - Complete API Reference
Share local services via Tailscale Serve (private) and Funnel (public internet access).
---
Chat Commands
Share Local Services (Private)
/tailscale serve 3000 Share port on tailnet
/tailscale serve 3000 --path /api Share at specific path
/tailscale serve stop 3000 Stop sharing port
/tailscale serve status View active sharesPublic Access (Funnel)
/tailscale funnel 3000 Expose to internet
/tailscale funnel 3000 --https Force HTTPS
/tailscale funnel stop 3000 Stop public access
/tailscale funnel status View funnelsNetwork Status
/tailscale status Network status
/tailscale ip Show Tailscale IP
/tailscale peers List connected peers
/tailscale ping <peer> Ping a peerFile Transfer
/tailscale send <file> <peer> Send file to peer
/tailscale receive Receive incoming files---
TypeScript API Reference
Create Tailscale Client
import { createTailscaleClient } from 'clodds/tailscale';
const tailscale = createTailscaleClient({
// Auth (optional if already logged in)
authKey: process.env.TAILSCALE_AUTHKEY,
// Socket path
socketPath: '/var/run/tailscale/tailscaled.sock',
});Serve (Private Sharing)
// Share local port on tailnet
await tailscale.serve({
port: 3000,
protocol: 'https', // 'http' | 'https'
});
console.log(`Shared at: https://${tailscale.hostname}:3000`);
// Share at specific path
await tailscale.serve({
port: 8080,
path: '/api',
protocol: 'https',
});
// Share with custom hostname
await tailscale.serve({
port: 3000,
hostname: 'clodds', // clodds.tailnet-name.ts.net
});
// Stop sharing
await tailscale.serveStop(3000);
// Get serve status
const serves = await tailscale.serveStatus();
for (const serve of serves) {
console.log(`Port ${serve.port} → ${serve.url}`);
}Funnel (Public Internet)
// Expose to public internet
await tailscale.funnel({
port: 3000,
protocol: 'https',
});
console.log(`Public URL: https://${tailscale.hostname}.ts.net`);
// With custom domain (if configured)
await tailscale.funnel({
port: 3000,
hostname: 'api.example.com',
});
// Stop funnel
await tailscale.funnelStop(3000);
// Get funnel status
const funnels = await tailscale.funnelStatus();
for (const funnel of funnels) {
console.log(`Port ${funnel.port} → ${funnel.publicUrl}`);
}Network Status
// Get status
const status = await tailscale.status();
console.log(`Hostname: ${status.hostname}`);
console.log(`IP: ${status.ip}`);
console.log(`Tailnet: ${status.tailnet}`);
console.log(`Online: ${status.online}`);
// List peers
const peers = await tailscale.peers();
for (const peer of peers) {
console.log(`${peer.hostname} (${peer.ip})`);
console.log(` OS: ${peer.os}`);
console.log(` Online: ${peer.online}`);
console.log(` Last seen: ${peer.lastSeen}`);
}
// Ping peer
const ping = await tailscale.ping('other-machine');
console.log(`Latency: ${ping.latencyMs}ms`);File Transfer
// Send file to peer
await tailscale.sendFile({
file: '/path/to/file.zip',
peer: 'other-machine',
});
// Receive files (returns when file received)
const received = await tailscale.receiveFile({
savePath: '/downloads',
timeout: 60000,
});
console.log(`Received: ${received.filename}`);
console.log(`From: ${received.sender}`);
console.log(`Size: ${received.size} bytes`);Get Tailscale IP
const ip = await tailscale.getIP();
console.log(`Tailscale IP: ${ip}`); // 100.x.x.x---
Serve vs Funnel
| Feature | Serve | Funnel |
|---|---|---|
| Access | Tailnet only | Public internet |
| Auth | Tailscale identity | None (public) |
| URL | machine.tailnet.ts.net | machine.ts.net |
| Use case | Internal tools | Public APIs |
---
URL Formats
| Type | Format |
|---|---|
| Serve | https://machine.tailnet-name.ts.net:port |
| Funnel | https://machine.ts.net |
| Custom domain | https://your-domain.com |
---
Use Cases
Share Dev Server
// Share local dev server with team
await tailscale.serve({ port: 3000 });
// Team can access at https://your-machine.tailnet.ts.net:3000Expose Webhook Endpoint
// Make webhook publicly accessible
await tailscale.funnel({ port: 3000, path: '/webhooks' });
// External services can POST to https://your-machine.ts.net/webhooksShare Bot with Phone
// Access bot from phone while away from desk
await tailscale.serve({ port: 18789 });
// Open https://your-machine.tailnet.ts.net:18789/webchat on phone---
Requirements
- Tailscale installed and running
- Logged into a Tailnet
- For Funnel: Funnel enabled in Tailscale admin
---
Best Practices
1. Use Serve for internal — Keep private services private 2. Use Funnel sparingly — Only for truly public endpoints 3. Add authentication — Funnel bypasses Tailscale auth 4. Monitor access — Check who's connecting 5. Stop when done — Don't leave services exposed
/**
* Tailscale CLI Skill
*
* Commands:
* /tailscale status - Tailscale status
* /tailscale serve <port> - Expose port via Tailscale Serve
* /tailscale funnel <port> - Expose port via Tailscale Funnel
* /tailscale stop - Stop serving
* /tailscale list - List peers on tailnet
*/
async function execute(args: string): Promise<string> {
const parts = args.trim().split(/\s+/);
const cmd = parts[0]?.toLowerCase() || 'status';
try {
const { TailscaleClient, ServeManager } = await import('../../../tailscale/index');
const client = new TailscaleClient();
switch (cmd) {
case 'status': {
const installed = await client.isInstalled();
if (!installed) {
return '**Tailscale Status**\n\nTailscale is not installed. Install from https://tailscale.com/download';
}
const running = await client.isRunning();
if (!running) {
return '**Tailscale Status**\n\nTailscale is installed but not running. Start with `tailscale up`.';
}
const status = await client.getStatus();
const onlinePeers = status.peers.filter(p => p.online);
const ip = status.tailscaleIPs?.[0] || 'unknown';
return `**Tailscale Status**\n\n` +
`State: ${status.backendState}\n` +
`Version: ${status.version}\n` +
`IP: ${ip}\n` +
`Hostname: ${status.self.hostName}\n` +
`DNS: ${status.self.dnsName}\n` +
`Tailnet: ${status.currentTailnet || 'unknown'}\n` +
`MagicDNS: ${status.magicDnsSuffix || 'n/a'}\n` +
`Peers: ${onlinePeers.length} online / ${status.peers.length} total`;
}
case 'serve': {
if (!parts[1]) return 'Usage: /tailscale serve <port> [--path /prefix]';
const port = parseInt(parts[1], 10);
if (isNaN(port)) return 'Invalid port number.';
const pathIdx = parts.indexOf('--path');
const path = pathIdx >= 0 ? parts[pathIdx + 1] : undefined;
const manager = new ServeManager(client);
const url = await manager.serve(port, { path });
return `**Tailscale Serve Started**\n\n` +
`Port: ${port}\n` +
`URL: ${url}\n` +
`Access: Private (tailnet only)\n\n` +
`Stop with: /tailscale stop`;
}
case 'funnel': {
if (!parts[1]) return 'Usage: /tailscale funnel <port> [--path /prefix]';
const port = parseInt(parts[1], 10);
if (isNaN(port)) return 'Invalid port number.';
const available = await client.funnelAvailable();
if (!available) {
return '**Tailscale Funnel**\n\nFunnel is not available for this account. Check your Tailscale plan and ACL settings.';
}
const pathIdx = parts.indexOf('--path');
const path = pathIdx >= 0 ? parts[pathIdx + 1] : undefined;
const manager = new ServeManager(client);
const url = await manager.funnel(port, { path });
return `**Tailscale Funnel Started**\n\n` +
`Port: ${port}\n` +
`URL: ${url}\n` +
`Access: **PUBLIC** (internet accessible)\n\n` +
`Stop with: /tailscale stop`;
}
case 'stop': {
const manager = new ServeManager(client);
await manager.stopAll();
return 'Tailscale Serve and Funnel stopped.';
}
case 'list':
case 'peers': {
const installed = await client.isInstalled();
if (!installed) return 'Tailscale is not installed.';
const running = await client.isRunning();
if (!running) return 'Tailscale is not running.';
const peers = await client.listPeers();
if (peers.length === 0) {
return '**Tailnet Peers**\n\nNo peers found.';
}
const lines = peers.map(p => {
const status = p.online ? 'online' : 'offline';
const ip = p.tailscaleIPs?.[0] || 'no IP';
return `- **${p.hostName}** (${p.os}) [${status}] ${ip}`;
});
return `**Tailnet Peers (${peers.length})**\n\n${lines.join('\n')}`;
}
case 'ping': {
if (!parts[1]) return 'Usage: /tailscale ping <hostname>';
const result = await client.ping(parts[1], 3);
if (!result.online) {
return `**Ping ${parts[1]}**: offline / unreachable`;
}
return `**Ping ${parts[1]}**: ${result.latency.toFixed(1)}ms`;
}
case 'ip': {
const ip = await client.getIP();
return ip ? `Tailscale IP: ${ip}` : 'Could not get Tailscale IP.';
}
default:
return helpText();
}
} catch (error) {
return `Error: ${error instanceof Error ? error.message : String(error)}`;
}
}
function helpText(): string {
return `**Tailscale Commands**
/tailscale status - Connection status
/tailscale serve <port> - Expose via Serve (private)
/tailscale funnel <port> - Expose via Funnel (public)
/tailscale stop - Stop serving
/tailscale list - List tailnet peers
/tailscale ping <host> - Ping a peer
/tailscale ip - Show Tailscale IP`;
}
export default {
name: 'tailscale',
description: 'Tailscale VPN sharing, Serve, and Funnel for remote access',
commands: ['/tailscale', '/ts'],
handle: execute,
};
Related skills
FAQ
What is the difference between Serve and Funnel?
Serve shares a port on the tailnet only (Tailscale identity auth); Funnel exposes it to the public internet with no auth.
What is required to use Funnel?
Tailscale installed and running, logged into a tailnet, and Funnel enabled in the Tailscale admin.