Skip to content

Latest commit

 

History

History
71 lines (48 loc) · 4.96 KB

File metadata and controls

71 lines (48 loc) · 4.96 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

worm is a Bash tool that opens SSH tunnels to remote Docker Compose services and exposes them locally as *.localhost hostnames via a Caddy reverse proxy running in Docker. No /etc/hosts editing — *.localhost auto-resolves to 127.0.0.1. SSH tunnels bind to Unix domain sockets under ~/.cache/worm/sockets/, not TCP ports — this prevents direct localhost:<port> access bypassing Caddy. Three scripts, no build system, no tests.

Inspired by kobemertens/worm. README is the canonical user-facing doc.

Commands

# Start Caddy (Linux)
docker compose up -d

# Start Caddy (macOS — REQUIRED override, see "macOS" below)
docker compose -f docker-compose.yml -f docker-compose.macos.yml up -d

# Open a tunnel (interactive fzf picker)
./tunnel-app.sh
./tunnel-app.sh <search-term>          # filter projects
./tunnel-app.sh -o                     # then pick a specific service (not just virtuoso)
./tunnel-app.sh -v ...                 # verbose / debug
./tunnel-app.sh --refresh              # re-query remote hosts for project list

# Inspect / clean up
./list-tunnels                         # active tunnels; also prunes stale state files
docker compose logs -f caddy
docker exec worm-caddy caddy config    # dump live Caddy config

State lives in ~/.cache/worm/:

  • <host>.txt — cached project list per SSH host
  • tunnels/<domain>.json — one file per active tunnel
  • sockets/<domain>.sock — Unix socket each SSH -L binds to; mounted into Caddy as /sockets
  • ssh-control/ — SSH ControlMaster sockets (connection multiplexing)

The sockets directory must be created by the script before Caddy starts: if docker compose up runs first, Docker creates the bind-mount target as root and the user can no longer write to it. Always start tunnels via ./tunnel-app.sh (which mkdirs first), not docker compose up -d directly.

Architecture

Three executables, all Bash:

  • tunnel-app.sh — entry point. Reads ~/.ssh/config for hosts, filters via ignore-hosts.txt (exact match, one host per line), queries docker compose ls on each remote, fzf-picks a project/service, then delegates to container-tunnel-caddy.
  • container-tunnel-caddy — does the actual work: opens an ssh -L <socket>:<container_ip>:<port> tunnel bound to a Unix socket (with StreamLocalBindUnlink=yes so a stale socket from a prior run is replaced), writes a state file, regenerates Caddyfile from all current state files, hits Caddy's admin API (localhost:2019) to reload. Cleans up sockets and state on Ctrl+C.
  • list-tunnels — reads ~/.cache/worm/tunnels/*.json, prunes entries whose tunnel is dead.

Caddyfile is generated, not hand-edited. auto_https off and admin API on 2019 are set globally. Each tunnel becomes one <domain>.localhost:80 { reverse_proxy unix//sockets/<domain>.sock } block. The Caddyfile is regenerated by cp (preserves inode) so the bind-mount inside the running Caddy container sees the new content without restart — don't switch to atomic-rename writes here.

Discovery relies on Docker Compose labels (com.docker.compose.project, com.docker.compose.service) on the remote — projects not started via Compose won't appear.

macOS

macOS support is recent and load-bearing. Anything platform-sensitive in container-tunnel-caddy must keep both branches working:

  • is_macos() (checks $OSTYPE) gates platform branches.
  • docker-compose.macos.yml is a required override on macOS (adds network_mode: bridge, ports: 80/443). Don't fold its contents into the base compose file — the base uses network_mode: host for Linux. The socket volume mount lives in the base compose file and is inherited by both platforms.
  • The socket bind mount (${HOME}/.cache/worm/sockets:/sockets) needs Docker Desktop to share ~/.cache (under /Users/$USER by default — already shared on macOS). VirtioFS is required for Unix sockets to propagate; gRPC-FUSE doesn't work. Recent Docker Desktop defaults to VirtioFS.
  • BSD vs GNU: sed -i '' (macOS) vs sed -i (Linux); stat flags also differ. Match the existing pattern when adding new calls.

Conventions / hard contracts

  • Container name worm-caddy is referenced from multiple scripts — don't rename without updating call sites.
  • Caddy reloads via docker exec worm-caddy caddy reload --config /etc/caddy/Caddyfile, which talks to its own admin API (localhost:2019). Don't switch the file write to a temp+rename pattern — that breaks the bind-mount inode and Caddy reloads stale content.
  • ignore-hosts.txt is exact-match only (no globs, no regex). One host per line.
  • Default remote port is 8890 (Virtuoso); the Virtuoso service path automatically appends /sparql to the printed URL.

Dependencies

Runtime only — fzf, openssh-client, docker (with Compose v2). No package manager / lockfile.