Hoody Tunnel
Section titled “Hoody Tunnel”The Hoody Tunnel kit exposes a multiplexed WebSocket control plane that brokers EXPOSE and PULL port bindings between local development processes and remote environments. These endpoints let you inspect active sessions, audit bindings, scrape Prometheus metrics, and administratively terminate sessions.
All endpoints are unauthenticated and intended for local kit control. Use them for debugging, dashboards, and operator tooling.
Health & Metrics
Section titled “Health & Metrics”GET /api/v1/tunnel/health
Section titled “GET /api/v1/tunnel/health”Returns the kit’s runtime health snapshot. No authentication required.
curl http://localhost:8080/api/v1/tunnel/healthconst health = await client.tunnel.health.check();Kit is healthy
{ "status": "ok", "service": "hoody-tunnel", "built": "2026-01-14T10:22:08Z", "started": "2026-01-14T12:00:01Z", "memory": { "rss": 48234496, "heap": 28602368 }, "fds": 312, "pid": 4821, "ip": "10.0.0.42", "userAgent": "hoody-cli/2.4.0"}| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | Liveness indicator (ok when the kit is serving) |
service | string | Yes | Kit service identifier |
built | string | null | No | Build timestamp of the running binary |
started | string | Yes | Process start timestamp (RFC 3339) |
memory | object | No | Process memory snapshot (heap, rss in bytes) |
fds | integer | null | No | Open file descriptor count |
pid | integer | Yes | Process ID of the kit |
ip | string | Yes | Local interface IP the kit is bound to |
userAgent | string | Yes | User-Agent of the most recent health probe |
GET /api/v1/tunnel/metrics
Section titled “GET /api/v1/tunnel/metrics”Returns Prometheus text-format metrics for the kit, including active sessions, active bindings, and FD permits.
curl http://localhost:8080/api/v1/tunnel/metricsconst metrics = await client.tunnel.getMetrics();Prometheus text format
# HELP hoody_tunnel_active_sessions Number of active tunnel sessions# TYPE hoody_tunnel_active_sessions gaugehoody_tunnel_active_sessions 3
# HELP hoody_tunnel_active_bindings Number of active bindings across all sessions# TYPE hoody_tunnel_active_bindings gaugehoody_tunnel_active_bindings 7
# HELP hoody_tunnel_fd_permits_available File descriptor permits remaining# TYPE hoody_tunnel_fd_permits_available gaugehoody_tunnel_fd_permits_available 493WebSocket Control Plane
Section titled “WebSocket Control Plane”GET /api/v1/tunnel/connect
Section titled “GET /api/v1/tunnel/connect”WebSocket upgrade endpoint for the multiplexed tunnel session. Clients MUST request subprotocol hoody-tunnel.v1 or hoody-tunnel.v2 and send a HELLO frame as the first binary message. See the kit README for the wire protocol.
This endpoint takes no parameters.
curl --include \ --no-buffer \ --header "Connection: Upgrade" \ --header "Upgrade: websocket" \ --header "Sec-WebSocket-Version: 13" \ --header "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ --header "Sec-WebSocket-Protocol: hoody-tunnel.v2" \ http://localhost:8080/api/v1/tunnel/connectconst socket = await client.tunnel.tunnelConnect();Switching Protocols — WebSocket upgrade accepted. The Sec-WebSocket-Protocol response header echoes the negotiated subprotocol.
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Protocol: hoody-tunnel.v2Invalid or missing subprotocol. The x-hoody-tunnel-versions response header lists the supported subprotocol versions (hoody-tunnel.v1, hoody-tunnel.v2).
{ "error": "unsupported subprotocol; expected one of: hoody-tunnel.v1, hoody-tunnel.v2"}Sessions & Bindings
Section titled “Sessions & Bindings”GET /api/v1/tunnel/sessions
Section titled “GET /api/v1/tunnel/sessions”Returns all active tunnel sessions with their bindings, stream counts, and protocol version.
This endpoint takes no parameters.
curl http://localhost:8080/api/v1/tunnel/sessionsconst result = await client.tunnel.listSessions();Active sessions
{ "total": 2, "sessions": [ { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", "peerAddr": "127.0.0.1:51422", "isV2": true, "connectionsGranted": 4, "activeStreams": 2, "maxStreams": 100, "bindings": [ { "bindId": 1, "containerPort": 3000, "kind": "expose", "mode": "http" }, { "bindId": 2, "containerPort": 5432, "kind": "pull", "mode": "tcp" } ] }, { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VWXYZ", "peerAddr": "127.0.0.1:51501", "isV2": false, "connectionsGranted": 1, "activeStreams": 0, "maxStreams": 100, "bindings": [ { "bindId": 1, "containerPort": 8080, "kind": "expose", "mode": "http" } ] } ]}GET /api/v1/tunnel/bindings
Section titled “GET /api/v1/tunnel/bindings”Returns all active EXPOSE and PULL bindings with their owning session, port, kind, mode, and bind ID.
This endpoint takes no parameters.
curl http://localhost:8080/api/v1/tunnel/bindingsconst result = await client.tunnel.listBindings();Active bindings
{ "total": 3, "bindings": [ { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", "port": 3000, "kind": "expose", "mode": "http", "bindId": 1 }, { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", "port": 5432, "kind": "pull", "mode": "tcp", "bindId": 2 }, { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VWXYZ", "port": 8080, "kind": "expose", "mode": "http", "bindId": 1 } ]}GET /api/v1/tunnel/tunnels
Section titled “GET /api/v1/tunnel/tunnels”Returns a unified view of all active tunnel sessions with their expose and pull bindings, stream counts, orphan count, and FD budget status.
This endpoint takes no parameters.
curl http://localhost:8080/api/v1/tunnel/tunnelsconst overview = await client.tunnel.listTunnels();Tunnel overview
{ "totalStreams": 2, "totalBindings": 3, "orphanedSessions": 0, "fdPermitsAvailable": 493, "sessions": [ { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", "peerAddr": "127.0.0.1:51422", "protocol": "hoody-tunnel.v2", "connectionsGranted": 4, "activeStreams": 2, "exposeBindings": [ { "bindId": 1, "containerPort": 3000 } ], "pullBindings": [ { "bindId": 2, "containerPort": 5432 } ] }, { "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VWXYZ", "peerAddr": "127.0.0.1:51501", "protocol": "hoody-tunnel.v1", "connectionsGranted": 1, "activeStreams": 0, "exposeBindings": [ { "bindId": 1, "containerPort": 8080 } ], "pullBindings": [] } ]}Session Management
Section titled “Session Management”DELETE /api/v1/tunnel/sessions/{session_id}
Section titled “DELETE /api/v1/tunnel/sessions/{session_id}”Signals the tunnel kit to close the named session. Live sessions receive a GOAWAY(0x0001, "closed by admin") sent directly on the WebSocket and are force-closed after grace_ms. Admin kills are non-resumable (orphan parking is skipped).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
session_id | path | string | Yes | Session ID as returned by GET /sessions |
grace_ms | query | integer | No | GOAWAY drain budget in ms (0–5000, default 50) |
curl -X DELETE \ "http://localhost:8080/api/v1/tunnel/sessions/sess_01HMZ3R5K4QX8YJ0N7T2VABCD?grace_ms=250"await client.tunnel.killSession({ session_id: "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", grace_ms: 250,});Close initiated
{ "sessionId": "sess_01HMZ3R5K4QX8YJ0N7T2VABCD", "status": "closing"}Invalid grace_ms (out of range 0–5000)
{ "error": "grace_ms must be between 0 and 5000"}Session not found
{ "error": "session not found"}