claude-code-tailscale
v0.2.0
Published
Claude Code plugin that joins a Tailscale tailnet as an ephemeral device using libtailscale
Maintainers
Readme
Claude Code Tailscale Plugin
A Claude Code MCP server that joins a Tailscale tailnet as an ephemeral device, giving Claude access to internal services, APIs, and resources on your tailnet.
How It Works
Claude Code
└── MCP Server (TypeScript)
└── tshelper (Go binary using tsnet)
└── SOCKS5 proxy on localhost
└── routes through your tailnetOn first tool invocation, the MCP server spawns tshelper, which:
- Joins your tailnet as an ephemeral device using
tsnet - Starts a SOCKS5 proxy, a control API, and an agent listener
- Returns connection details to the MCP server
Claude can then use tailscale_fetch to access any service on your tailnet — internal APIs, dashboards, databases — without exposing them to the internet. Multiple Claude Code sessions on different machines can discover each other, exchange messages, and coordinate work through shared task lists.
When the session ends, the device explicitly logs out and is immediately removed from your tailnet.
Prerequisites
- Go 1.24+ (to build tshelper)
- Node.js 20+
- A Tailscale account with an auth key (ephemeral, reusable recommended) or an OAuth client
Setup
1. Configure Credentials
Set your Tailscale credentials (pick one):
# Option A: Auth key (simplest)
export TS_AUTHKEY="tskey-auth-..."
# Option B: OAuth client secret (preferred for automation/CI)
export TS_CLIENT_SECRET="tskey-client-..."
export TS_TAGS="tag:claude-code" # required for OAuth2. Add to Claude Code
The simplest way — install directly from npm:
claude mcp add tailscale -- npx -y claude-code-tailscaleOr add to your project's .mcp.json:
{
"mcpServers": {
"tailscale": {
"command": "npx",
"args": ["-y", "claude-code-tailscale"],
"env": {
"TS_AUTHKEY": "tskey-auth-..."
}
}
}
}With an OAuth client secret:
{
"mcpServers": {
"tailscale": {
"command": "npx",
"args": ["-y", "claude-code-tailscale"],
"env": {
"TS_CLIENT_SECRET": "tskey-client-...",
"TS_TAGS": "tag:claude-code"
}
}
}
}With team coordination enabled:
{
"mcpServers": {
"tailscale": {
"command": "npx",
"args": ["-y", "claude-code-tailscale"],
"env": {
"TS_AUTHKEY": "tskey-auth-...",
"TS_ENABLE_TEAMS": "1"
}
}
}
}Building from Source
If you prefer to build locally instead of using the npm package:
git clone https://github.com/rosskukulinski/claude-code-tailscale.git
cd claude-code-tailscale
# Build the Go helper
cd tshelper && go build -o ../bin/tshelper . && cd ..
# Install npm dependencies and build TypeScript
npm install && npm run build
# Add to Claude Code
claude mcp add tailscale -- node /path/to/claude-code-tailscale/dist/index.jsTools
Networking
| Tool | Description |
|------|-------------|
| tailscale_status | Show tailnet connection status, IPs, and hostname |
| tailscale_fetch | HTTP fetch through the tailnet (GET, POST, PUT, DELETE, PATCH, HEAD) |
| tailscale_connect_test | Test TCP connectivity to a tailnet host:port |
| tailscale_port_forward | Create, list, or close TCP port forwards to tailnet hosts |
Diagnostics
| Tool | Description |
|------|-------------|
| tailscale_dns_lookup | Resolve DNS names via MagicDNS |
| tailscale_whois | Look up info about a tailnet IP (owner, hostname, tags) |
| tailscale_devices | List all devices on the tailnet |
| tailscale_ping | Ping a tailnet peer (disco, TSMP, ICMP, peerapi) |
| tailscale_acl_check | Show ACL packet filter rules for this node |
| tailscale_netcheck | DERP relay regions and latency probes |
| tailscale_cert | Get TLS certificate info for a tailnet domain |
Inter-Agent Messaging
| Tool | Description |
|------|-------------|
| tailscale_agents | Discover other Claude Code agents (cc-* peers) on the tailnet |
| tailscale_send_message | Send a message to another agent (or "*" to broadcast) |
| tailscale_check_messages | Check inbox for messages from other agents |
Team Coordination (opt-in: TS_ENABLE_TEAMS=1)
| Tool | Description |
|------|-------------|
| tailscale_team_create | Create a named team with this agent as lead |
| tailscale_team_join | Join a team hosted by another agent |
| tailscale_team_info | Show team membership and roles |
| tailscale_task_create | Create a task on the shared task list |
| tailscale_task_list | List tasks (filter by status: pending, in_progress, completed) |
| tailscale_task_update | Claim, complete, update, or delete a task |
Example Usage
Once configured, Claude can access your tailnet services:
> Use tailscale_fetch to get the status of my internal API at http://api-server.tail1234.ts.net/health
> Check if my database server is reachable: tailscale_connect_test db.tail1234.ts.net port 5432
> What's my tailnet connection status?Multi-Agent Coordination
Multiple Claude Code sessions on different machines can communicate and coordinate work over the tailnet.
Messaging — any agent can send messages to other cc-* peers:
> Find other agents on the tailnet
→ tailscale_agents
> Send a message to the backend agent
→ tailscale_send_message(to: "cc-ross-backend-a7b2", subject: "API ready", body: "...")Team task coordination (requires TS_ENABLE_TEAMS=1) — a lead agent creates a team and shared task list. Teammates join and claim tasks:
Terminal 1 (Machine A — backend repo):
> "Create a team called api-migration"
→ tailscale_team_create(name: "api-migration")
> "Add tasks for the migration"
→ tailscale_task_create(subject: "Update /api/users response schema", ...)
→ tailscale_task_create(subject: "Update /api/auth token format", ...)
Terminal 2 (Machine B — frontend repo):
> "Join the api-migration team"
→ tailscale_team_join(lead_hostname: "cc-ross-backend-a7b2")
> "What tasks are available?"
→ tailscale_task_list(status: "pending")
> "Claim the users schema task"
→ tailscale_task_update(id: "task-1", action: "claim")
→ (works on it...)
→ tailscale_task_update(id: "task-1", action: "complete")The lead holds the canonical task list in memory. Teammates interact with it over HTTP via the tailnet. If the lead goes offline, task operations fail cleanly — teammates can keep working on already-claimed tasks.
Configuration
| Environment Variable | Required | Default | Description |
|---------------------|----------|---------|-------------|
| TS_AUTHKEY | One of TS_AUTHKEY or TS_CLIENT_SECRET | — | Tailscale auth key (ephemeral recommended) |
| TS_CLIENT_SECRET | One of TS_AUTHKEY or TS_CLIENT_SECRET | — | OAuth client secret (tskey-client-...). Preferred for automation/CI |
| TS_TAGS | Required for OAuth | — | Comma-separated ACL tags (e.g., tag:claude-code,tag:dev) |
| TS_HOSTNAME | No | cc-{user}-{context}-{rand} | Tailnet hostname override (random suffix always appended) |
| TS_STATE_DIR | No | ~/.cache/claude-code-tailscale/{hostname} | State directory |
| TS_CONTROL_URL | No | — | Custom control server (for Headscale) |
| TS_EPHEMERAL | No | true | Register as ephemeral node |
| TS_ENABLE_TEAMS | No | — | Set to 1 to enable team/task coordination tools |
| TS_VERBOSE | No | — | Set to 1 for full tsnet debug logging |
Hostname format: cc-{user}-{context}-{random4} where {context} is the git repo name (or machine hostname if not in a repo) and {random4} is 4 random hex chars for uniqueness across concurrent sessions.
Architecture
Machine A (Lead) Machine B (Teammate)
+------------------------------------------+ +------------------------------------------+
| Claude Code Session A | | Claude Code Session B |
| [Claude LLM] <-> [MCP Server (TS)] | | [Claude LLM] <-> [MCP Server (TS)] |
| | | | | |
| [tshelper (Go)] | | [tshelper (Go)] |
| | | | | | | | | | | |
| SOCKS5 API Agent Team+Tasks | | SOCKS5 API Agent (proxied) |
| :9321 (local) | | :9321 |
+------------------------------------------+ +------------------------------------------+
| |
+--------- Tailscale Tailnet --------------+This plugin follows the same pattern as kong-tailscale-plugin:
- Kong plugin: LuaJIT FFI → libtailscale →
tailscale_loopback()→ SOCKS5 → cosocket - This plugin: Go tsnet binary → SOCKS5 proxy → Node.js
http.requestwithsocks-proxy-agent
The Go helper (tshelper) uses tsnet.Server directly instead of libtailscale's C API, since we're already in Go. It runs three listeners:
- SOCKS5 proxy (localhost, random port) — routes HTTP traffic through the tailnet
- Control API (localhost, random port) — serves MCP tool requests (DNS, ping, devices, etc.)
- Agent listener (tailnet, port 9321) — inter-agent messaging, team join/leave, shared tasks
Key design decisions:
- Lazy connection: Tailnet joins on first tool invocation to avoid wasting nodes
- Ephemeral by default: Device auto-removes when the session ends, with explicit logout for instant cleanup
- Unique hostnames: Each session gets a unique hostname (
cc-{user}-{repo}-{rand}) to avoid collisions - OAuth + auth key support: Works with both
TS_CLIENT_SECRET(OAuth) andTS_AUTHKEY(plain auth keys) - No system Tailscale required: tsnet embeds a full Tailscale node in the Go binary
- Centralized task list: The team lead holds the canonical task store in memory; teammates proxy via HTTP. No distributed consensus needed
- Feature-flagged teams: Team/task tools only register when
TS_ENABLE_TEAMS=1, keeping the default tool set lean
License
Apache 2.0
