
Remote
- 13 installs
- 610 repo stars
- Updated June 26, 2026
- alsk1992/cloddsbot
remote is a Claude skill that manages SSH, ngrok, Cloudflare, and localtunnel tunnels to expose and control access to a local instance.
About
This skill manages remote access to a local instance through SSH tunnels, ngrok, Cloudflare tunnels, and localtunnel. Developers use /remote commands or a TypeScript API to create tunnels, list and check them, get public URLs, forward ports, and close tunnels. It supports auto-reconnect and reports uptime and bytes transferred. It is used to expose a local server for webhooks or remote bot access.
- Creates ngrok, Cloudflare, SSH, and localtunnel tunnels to a local instance
- Lists tunnels, checks health, and gets public URLs via /remote commands
- Supports port forwarding and auto-reconnect on disconnect
Remote by the numbers
- 13 all-time installs (skills.sh)
- Ranked #966 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
remote capabilities & compatibility
Free tiers exist; ngrok/Cloudflare need auth tokens and paid plans unlock subdomains
- Capabilities
- tunnel management · port forwarding · remote access
- Works with
- cloudflare
- Use cases
- devops
- Pricing
- Freemium
What remote says it does
Manage SSH tunnels, ngrok exposure, and remote access to your local Clodds instance.
SSH tunnels, ngrok, and remote access management
npx skills add https://github.com/alsk1992/cloddsbot --skill remoteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 610 |
| Last updated | June 26, 2026 |
| Repository | alsk1992/cloddsbot ↗ |
What it does
Expose a local server via SSH, ngrok, or Cloudflare tunnels and manage remote access.
Who is it for?
Exposing a local server via a tunnel for webhook testing or remote bot access.
When should I use this skill?
A user needs a public URL for a local port or wants to manage SSH/ngrok tunnels.
By the numbers
- 4 tunnel types (ngrok, Cloudflare, SSH, localtunnel)
Files
Remote - Complete API Reference
Manage SSH tunnels, ngrok exposure, and remote access to your local Clodds instance.
---
Chat Commands
Create Tunnels
/remote tunnel ngrok 3000 Expose port via ngrok
/remote tunnel cloudflare 3000 Expose via Cloudflare
/remote tunnel ssh 3000 user@server SSH tunnel
/remote tunnel localtunnel 3000 Expose via localtunnelManage Tunnels
/remote list List active tunnels
/remote status <id> Check tunnel health
/remote url <id> Get tunnel URL
/remote close <id> Close tunnel
/remote close-all Close all tunnelsPort Forwarding
/remote forward 8080:localhost:3000 Local port forward
/remote forward remote 3000:server:80 Remote port forward---
TypeScript API Reference
Create Remote Manager
import { createRemoteManager } from 'clodds/remote';
const remote = createRemoteManager({
// ngrok auth
ngrokAuthToken: process.env.NGROK_AUTH_TOKEN,
// Cloudflare tunnel
cloudflareToken: process.env.CLOUDFLARE_TUNNEL_TOKEN,
// SSH key
sshKeyPath: '~/.ssh/id_rsa',
// Auto-reconnect
autoReconnect: true,
reconnectDelayMs: 5000,
});Create ngrok Tunnel
const tunnel = await remote.createNgrokTunnel({
port: 3000,
protocol: 'http', // 'http' | 'tcp' | 'tls'
// Optional
subdomain: 'my-clodds', // Requires paid plan
authToken: process.env.NGROK_AUTH_TOKEN,
});
console.log(`Public URL: ${tunnel.url}`);
console.log(`Tunnel ID: ${tunnel.id}`);Create Cloudflare Tunnel
const tunnel = await remote.createCloudflareTunnel({
port: 3000,
hostname: 'clodds.example.com',
token: process.env.CLOUDFLARE_TUNNEL_TOKEN,
});
console.log(`URL: ${tunnel.url}`);Create SSH Tunnel
const tunnel = await remote.createSshTunnel({
localPort: 3000,
remoteHost: 'server.example.com',
remotePort: 80,
username: 'deploy',
privateKey: fs.readFileSync('~/.ssh/id_rsa'),
});
console.log(`Tunnel established`);
console.log(`Access via: ssh -L 3000:localhost:80 deploy@server.example.com`);List Tunnels
const tunnels = remote.listTunnels();
for (const tunnel of tunnels) {
console.log(`${tunnel.id}: ${tunnel.type}`);
console.log(` URL: ${tunnel.url}`);
console.log(` Port: ${tunnel.port}`);
console.log(` Status: ${tunnel.status}`);
console.log(` Created: ${tunnel.createdAt}`);
}Check Status
const status = await remote.getStatus(tunnelId);
console.log(`Status: ${status.status}`); // 'connected' | 'reconnecting' | 'disconnected'
console.log(`Uptime: ${status.uptimeMs}ms`);
console.log(`Bytes in: ${status.bytesIn}`);
console.log(`Bytes out: ${status.bytesOut}`);Close Tunnel
// Close single tunnel
await remote.closeTunnel(tunnelId);
// Close all tunnels
await remote.closeAll();Event Handlers
remote.on('connected', (tunnel) => {
console.log(`Tunnel connected: ${tunnel.url}`);
});
remote.on('disconnected', (tunnel) => {
console.log(`Tunnel disconnected: ${tunnel.id}`);
});
remote.on('error', (tunnel, error) => {
console.error(`Tunnel error: ${error.message}`);
});---
Tunnel Types
| Type | Best For | Requirements |
|---|---|---|
| ngrok | Quick testing | Free account |
| Cloudflare | Production | Cloudflare account |
| SSH | Secure access | SSH server |
| localtunnel | Free, temporary | None |
---
Use Cases
Expose Webhook Endpoint
// Expose local server for webhook testing
const tunnel = await remote.createNgrokTunnel({ port: 3000 });
console.log(`Set webhook URL to: ${tunnel.url}/webhooks/trading-signals`);Remote Bot Access
// Access bot from phone while at home
const tunnel = await remote.createCloudflareTunnel({
port: 3000,
hostname: 'clodds.mysite.com',
});
// Now access at https://clodds.mysite.com---
Best Practices
1. Use Cloudflare for production — More stable than ngrok 2. Secure with auth — Don't expose without protection 3. Monitor connections — Watch for disconnections 4. Close unused tunnels — Don't leave open indefinitely
/**
* Remote CLI Skill
*
* Commands:
* /remote status - Show active tunnels
* /remote ssh <host> - Create SSH tunnel
* /remote ngrok <port> - Start ngrok tunnel
* /remote cloudflare <port> - Start Cloudflare tunnel
* /remote stop <id> - Stop tunnel
* /remote stop-all - Stop all tunnels
*/
async function execute(args: string): Promise<string> {
const parts = args.trim().split(/\s+/);
const cmd = parts[0]?.toLowerCase() || 'help';
try {
const { tunnels } = await import('../../../remote/index');
switch (cmd) {
case 'status':
case 'list':
case 'ls': {
const allTunnels = tunnels.list();
if (allTunnels.length === 0) {
return '**Active Tunnels**\n\nNo active tunnels.';
}
let output = '**Active Tunnels**\n\n| ID | Type | Local Port | Public URL | Status | Started |\n|-----|------|------------|------------|--------|----------|\n';
for (const t of allTunnels) {
const elapsed = Math.round((Date.now() - t.startedAt.getTime()) / 1000);
const duration = elapsed < 60 ? `${elapsed}s` : `${Math.round(elapsed / 60)}m`;
output += `| ${t.id} | ${t.type} | ${t.localPort} | ${t.publicUrl || 'N/A'} | ${t.status} | ${duration} ago |\n`;
}
const active = tunnels.getActive();
output += `\n**${active.length}** connected, **${allTunnels.length}** total`;
return output;
}
case 'ssh': {
if (!parts[1]) {
return 'Usage: /remote ssh <sshHost> --local <port> --remote-host <host> --remote-port <port>\n\nExample: /remote ssh user@server --local 8080 --remote-host localhost --remote-port 3000';
}
const sshHost = parts[1];
const localPort = parseInt(getFlag(parts, '--local') || getFlag(parts, '--port') || '8080', 10);
if (isNaN(localPort)) return 'Local port must be a number.';
const remoteHost = getFlag(parts, '--remote-host') || 'localhost';
const remotePort = parseInt(getFlag(parts, '--remote-port') || '80', 10);
if (isNaN(remotePort)) return 'Remote port must be a number.';
const sshUser = getFlag(parts, '--user');
const sshKey = getFlag(parts, '--key');
const tunnel = await tunnels.createSshTunnel({
localPort,
remoteHost,
remotePort,
sshHost,
sshUser: sshUser || undefined,
sshKey: sshKey || undefined,
});
return `**SSH Tunnel Created**\n\nID: ${tunnel.id}\nLocal: localhost:${tunnel.localPort}\nRemote: ${remoteHost}:${remotePort}\nSSH Host: ${sshHost}\nStatus: ${tunnel.status}\nPublic URL: ${tunnel.publicUrl || 'N/A'}`;
}
case 'ngrok': {
if (!parts[1]) {
return 'Usage: /remote ngrok <port> [--subdomain <name>] [--region <region>]\n\nExample: /remote ngrok 3000 --subdomain myapp';
}
const localPort = parseInt(parts[1], 10);
if (isNaN(localPort)) {
return 'Port must be a number. Usage: /remote ngrok <port>';
}
const subdomain = getFlag(parts, '--subdomain');
const region = getFlag(parts, '--region');
const authToken = getFlag(parts, '--token');
const tunnel = await tunnels.createNgrokTunnel({
localPort,
authToken: authToken || undefined,
subdomain: subdomain || undefined,
region: region || undefined,
});
return `**ngrok Tunnel Created**\n\nID: ${tunnel.id}\nLocal: localhost:${localPort}\nPublic URL: ${tunnel.publicUrl || 'pending...'}\nStatus: ${tunnel.status}`;
}
case 'cloudflare':
case 'cf': {
if (!parts[1]) {
return 'Usage: /remote cloudflare <port> [--hostname <domain>]\n\nExample: /remote cloudflare 3000';
}
const localPort = parseInt(parts[1], 10);
if (isNaN(localPort)) {
return 'Port must be a number. Usage: /remote cloudflare <port>';
}
const hostname = getFlag(parts, '--hostname');
const tunnel = await tunnels.createCloudflareTunnel({
localPort,
hostname: hostname || undefined,
});
return `**Cloudflare Tunnel Created**\n\nID: ${tunnel.id}\nLocal: localhost:${localPort}\nPublic URL: ${tunnel.publicUrl || 'pending...'}\nStatus: ${tunnel.status}`;
}
case 'stop':
case 'close': {
if (!parts[1]) {
return 'Usage: /remote stop <tunnel-id>\n\nUse `/remote status` to see tunnel IDs.';
}
const id = parts[1];
const tunnel = tunnels.get(id);
if (!tunnel) {
return `Tunnel \`${id}\` not found. Use \`/remote status\` to see active tunnels.`;
}
tunnels.close(id);
return `Tunnel \`${id}\` (${tunnel.type}, port ${tunnel.localPort}) stopped.`;
}
case 'stop-all':
case 'close-all': {
const count = tunnels.list().length;
tunnels.closeAll();
return count > 0
? `Closed **${count}** tunnel(s).`
: 'No active tunnels to close.';
}
default:
return helpText();
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
// If it's a known operational error (not an import failure), show it
if (msg.includes('not installed') || msg.includes('failed to start')) {
return `**Error:** ${msg}`;
}
return helpText();
}
}
/** Extract --flag value from args array */
function getFlag(parts: string[], flag: string): string | null {
const idx = parts.indexOf(flag);
if (idx !== -1 && idx + 1 < parts.length) {
return parts[idx + 1];
}
return null;
}
function helpText(): string {
return `**Remote Access Commands**
/remote status - Show active tunnels
/remote ssh <host> [--local <port>] - Create SSH tunnel
/remote ngrok <port> - Start ngrok tunnel
/remote cloudflare <port> - Start Cloudflare tunnel
/remote stop <id> - Stop a tunnel
/remote stop-all - Stop all tunnels
**SSH example:** /remote ssh user@server --local 8080 --remote-host localhost --remote-port 3000
**ngrok example:** /remote ngrok 3000 --subdomain myapp`;
}
export default {
name: 'remote',
description: 'SSH tunnels, ngrok, and remote access management',
commands: ['/remote', '/tunnel'],
handle: execute,
};
Related skills
FAQ
Which tunnel types are supported?
ngrok, Cloudflare, SSH, and localtunnel, each suited to different testing or production needs.
Does it reconnect automatically?
Yes, it supports autoReconnect with a configurable reconnect delay.