Hydrogen Desktop (macOS / Lima)
Public Made by Adomby adom
The macOS platform layer for Hydrogen Desktop.
name: hd-port-watcher
description: >
Dynamic port forwarding for Hydrogen Desktop. A daemon inside the
Adom-Workspace machine watches for new TCP listeners (OAuth callbacks from VS
Code extensions like Codex, Copilot, GitLens) and notifies HD to create
host-side TCP proxies. Covers the cases macOS/Lima port exposure can't:
127.0.0.1-only listeners and exposure gaps. Makes localhost:{port}
on the Mac reach the machine. Extensions just work — no manual setup.
Trigger words: port forward, dynamic port, OAuth callback, Codex auth,
extension auth, port mapping, localhost port, port watcher, port proxy.
HD Port Watcher — Dynamic Port Forwarding
This is the machine-runtime version (default). For the legacy Docker container runtime (
HD_RUNTIME=docker) see the docker/ bucket.
Why the daemon is still here on the machine
When a service inside the Adom-Workspace machine binds 0.0.0.0:<port>, Lima's
vz networking makes it reachable from the Mac at localhost:<sameport> — so you
might think a port watcher is unnecessary. It isn't. The daemon is RETAINED
because that 0.0.0.0 exposure has two gaps the watcher closes:
127.0.0.1-only listeners. vz does NOT mirror loopback, so a loopback-only listener inside the machine is never exposed on the Mac. VS Code extensions (Codex, Copilot, GitLens) start OAuth callback servers that frequently bind127.0.0.1— the browser redirects tolocalhost:{port}/auth/callback?code=...but the Mac never had that port, so it doesn't exist.- Exposure lag / wedge. macOS/Lima port exposure can lag or break
even for
0.0.0.0listeners (a real HD-logged failure mode: "code-server alive in the machine but the Mac can't reach 127.0.0.1:7380"). The watcher gives HD a reliable host-side proxy that doesn't depend on Lima's automatic exposure.
The Solution
A lightweight daemon inside the machine monitors /proc/net/tcp for new
listening ports. When one appears (that wasn't there at boot and isn't a known
service), it notifies HD's control API. HD creates a host-side TCP proxy on
that same port, forwarding through code-server's /proxy/{port} to reach the
machine. The browser redirect just works — even for loopback-only callback
servers Lima wouldn't have exposed.
Architecture
Adom-Workspace machine: macOS Host:
┌─────────────────────┐ ┌──────────────────────┐
│ Codex creates server │ │ HD Control API │
│ on 127.0.0.1:1455 │ │ (:47084) │
│ (loopback, no expose)│ │ │
│ port-watcher.sh │──POST──→ │ /port-forward │
│ detects port 1455 │ open │ creates TCP proxy │
│ │ │ on localhost:1455 │
│ code-server │ │ │
│ /proxy/1455/ ←───────┼───────── │ proxy forwards here │
└─────────────────────┘ └──────────────────────┘
↑
Browser redirects to
localhost:1455/callback
What Gets Forwarded (and what doesn't)
FORWARDED (temporary, appeared after boot):
- OAuth callback ports from extensions (Codex, Copilot, etc.) — especially
127.0.0.1-only ones Lima cannot expose - Any new TCP listener that wasn't running when the machine started
- Auto-expires after 5 minutes (OAuth callbacks are fast)
NOT FORWARDED (known services, stay behind /proxy/{port} or 0.0.0.0 exposure):
- code-server (8080 inside the machine; exposed as 7380 mapping/
0.0.0.0) - relay WS/HTTP (8765, 8766)
- adom-vscode (8821)
- shotlog (8820)
- Any port in the KNOWN_PORTS list
- Any port that was listening at machine boot time
- Any port ≤ 1023
This keeps the local workspace machine behaving like cloud Hydrogen — known
services use /proxy/{port} (or macOS/Lima exposure for 0.0.0.0 binds), and
only short-lived / loopback-only OAuth ports get a dedicated host proxy.
API Endpoints
POST /port-forward
Create or remove a host-side TCP proxy. Called by the machine's watcher.
{"port": 1455, "action": "open"} // Create proxy
{"port": 1455, "action": "close"} // Remove proxy
GET /port-forward
List currently forwarded ports.
{"ok": true, "forwarded_ports": [1455], "count": 1}
The Watcher Daemon
scripts/port-watcher.sh runs inside the machine:
- Polls
/proc/net/tcpevery 2 seconds - Diffs against previous scan
- New port (not in KNOWN_PORTS whitelist) → POST
/port-forwardopen - Port gone → POST
/port-forwardclose - Port open > 5 minutes → auto-expire (likely a service, not OAuth)
No boot snapshot — the whitelist is the sole source of truth. Services like shotlog can start at any time (days/weeks later) and will never get a host proxy as long as their port is in the whitelist.
The watcher reaches HD at the adom-host gateway plus the control port
(<adom-host>:<control>/port-forward). vz does NOT mirror loopback, so the
machine cannot reach HD at its own 127.0.0.1; instead the machine's
/etc/hosts maps adom-host to the gateway, and that's where HD's host
listener lives. The control port is dynamic per launch; the live control URL
(already pointing at the gateway host) is in ~/.adom/hd-control-url.
Installation
The watcher is a bash script (port-watcher.sh) deployed into the machine
during setup steps. Installation uses limactl shell hd-builder + nsenter
into the machine, NOT docker cp/docker exec:
# Stage the script into the Lima VM, then install it into the machine via nsenter.
# (No \\wsl$ share / wsl cat exists on macOS — copy into the VM, then nsenter.)
limactl copy port-watcher.sh hd-builder:/tmp/port-watcher.sh
limactl shell hd-builder -- sudo nsenter -t <leader> -a -- bash -lc \
'cp /tmp/port-watcher.sh /usr/local/bin/port-watcher.sh && chmod +x /usr/local/bin/port-watcher.sh'
# Launch detached inside the machine (as the adom user)
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- bash -lc \
'nohup bash /usr/local/bin/port-watcher.sh >/tmp/port-watcher.log 2>&1 &'
The script parses /proc/net/tcp directly in awk — it filters listener rows
(state 0A), splits the hex local_address, and converts the port with
strtonum("0x" …). No Python is involved.
UI: Toast + Port Mappings Dialog
Toast notification: When a port is forwarded, HD shows an info toast:
"Port 1455 forwarded to workspace". When removed: "Port 1455 forwarding stopped".
Powered by the port-forward-changed Tauri event.
Port Mappings dialog: Accessible from the Adom menu → Admin → "Port Mappings".
Shows exposed 0.0.0.0 ports AND dynamic watcher-created proxies. Dynamic
ports have a "Stop" button. Matches the API Explorer visual style.
Adding New Known Services
When a new service is added to the machine that runs a persistent TCP server,
add its port to the KNOWN_PORTS whitelist string in port-watcher.sh
(scripts/port-watcher.sh:16):
KNOWN_PORTS="8080 8765 8766 8821 8820 36035 47084 47083 47085 1420 7380 8770 9000"
This prevents it from getting a dedicated host proxy. Services should bind
0.0.0.0 (exposed by Lima) or use /proxy/{port} instead, consistent with web
Hydrogen.
The forwarding layers
There are distinct forwarding mechanisms. The /proc/net/tcp watcher above
is Layer 2 — and on macOS it is the primary path. Layer 1 (the
navigation-interception localhost proxy) is Windows-only and does NOT run on
macOS — see the note below.
Layer 1 — the navigation-interception proxy (Windows-only; NOT active on macOS)
On Windows, HD intercepts top-level WebView2 navigations via
setup_frame_navigation_interception() (hd-app/src/lib.rs, #[cfg(windows)]).
On every navigation it runs extract_oauth_callback_port(), which
triple-URL-decodes the target URL (urlencoding::decode x3) and scans each
decode layer — plus the raw and the percent-encoded
redirect_uri=http%3A%2F%2Flocalhost%3A… form — for
redirect_uri=http://localhost:<port>. When it finds one it calls
start_oauth_callback_proxy(port), which pre-binds 127.0.0.1:<port> on the
host and waits up to ~5 min for one inbound request, then forwards the full path
into the machine via code-server (http://localhost:<code_server_port>/proxy/<port>/<path>)
and streams the response back to the browser.
This interceptor does NOT exist on macOS. It depends on WebView2's
ICoreWebView2 navigation events; HD's macOS webview is WKWebView, which
exposes no equivalent navigation-interception (the same CDP gap that makes
/eval fire-and-forget on macOS). So on macOS the automatic zero-latency
pre-bind doesn't happen — the port watcher (Layer 2) is the primary path,
with the manual POST /start-oauth-proxy endpoint (Layer 3) as the explicit
fallback. The /start-oauth-proxy + /oauth/callback endpoints themselves are
cross-platform and work the same on macOS.
Control / callback endpoints (HD control API):
POST /start-oauth-proxy— manually pre-bind a callback port (on macOS this is the explicit way to get the Layer-1 effect, since the navigation interceptor doesn't run).GET /oauth/callback— HD's own built-in callback receiver (used by the Claude Code PKCE flow); do not call directly.
Failure mode the AI may see: if a proxy binds but code-server can't be
reached, HD returns a styled 502 Bad Gateway page (dark #0d1117
background, teal #00b8b0 "OAuth callback proxy" heading) reading roughly "HD
tried to forward this OAuth callback to the workspace via code-server proxy
(port …/proxy/…), but it failed. The extension that started this OAuth flow may
need its callback port (…) mapped to the host." Seeing that page in a browser
means the proxy bound but the machine-side proxy hop failed — check the HD log
for [oauth-proxy] lines and that code-server is alive.
Layer 2 — the port watcher (/proc/net/tcp daemon — the primary path on macOS)
The bash daemon documented above. On macOS it is the workhorse (Layer 1's
navigation interceptor is Windows-only): it surfaces new callback ports —
including 127.0.0.1-only callback servers Lima silently won't expose — and asks
HD to proxy them. Note: when an app has already registered its port via the
adom-port-hints protocol, the watcher skips it (it only owns the fallback for
third-party extensions that don't know HD exists).
Layer 3 — manual
POST /start-oauth-proxy or POST /port-forward (or
adom-cli port-forward register) for cases the watcher misses (on macOS, where
the Windows-only navigation interceptor doesn't run, this is the explicit
pre-bind path).
Port Conflicts
If the host port is already taken by another Mac app, the forwarding fails silently. The OAuth callback will break. Mitigation: extensions typically pick a different random port on retry. HD logs the conflict.
How OAuth Callbacks Work End-to-End
- Extension (Codex) starts HTTP server on
127.0.0.1:1455inside the machine - port-watcher.sh detects port 1455 (not in boot snapshot, not known) — note Lima would NOT have exposed this loopback-only listener
- Watcher POSTs
{"port":1455,"action":"open"}to HD - HD creates TCP proxy:
localhost:1455→ code-server/proxy/1455/ - Extension opens browser with
redirect_uri=http://localhost:1455/callback - User authenticates, browser redirects to
localhost:1455/callback?code=... - HD's proxy receives the request, forwards into the machine via code-server
- Extension receives callback, completes auth
- Extension closes its server, port 1455 disappears
- Watcher POSTs
{"port":1455,"action":"close"}, HD removes proxy
Debugging
All debugging runs inside the machine via limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- ..., NEVER docker exec:
# Check what ports the watcher sees
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- bash -lc "cat /proc/net/tcp | awk '\$4 == \"0A\" { split(\$2, a, \":\"); printf \"%d\\n\", strtonum(\"0x\" a[2]) }' | sort -n"
# Check the watcher's own log
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- cat /tmp/port-watcher.log
# Check what HD has forwarded (from the Mac host)
curl -sf http://127.0.0.1:47084/port-forward
# Manually forward a port (for testing, from the Mac host)
curl -sf -X POST http://127.0.0.1:47084/port-forward \
-H Content-Type:application/json \
-d '{"port":1455,"action":"open"}'
# Check HD log for port-fwd events
# Look for [port-fwd] entries
Related skills
hd-networking—0.0.0.0exposure, reaching the host at theadom-hostgateway (vz does NOT mirror loopback), NEVER do thesehd-ports— bind0.0.0.0first; fall back to the port-forward registry for127.0.0.1-only services or unreliable exposure
---
name: hd-port-watcher
description: >
Dynamic port forwarding for Hydrogen Desktop. A daemon inside the
Adom-Workspace machine watches for new TCP listeners (OAuth callbacks from VS
Code extensions like Codex, Copilot, GitLens) and notifies HD to create
host-side TCP proxies. Covers the cases macOS/Lima port exposure can't:
`127.0.0.1`-only listeners and exposure gaps. Makes localhost:{port}
on the Mac reach the machine. Extensions just work — no manual setup.
Trigger words: port forward, dynamic port, OAuth callback, Codex auth,
extension auth, port mapping, localhost port, port watcher, port proxy.
---
# HD Port Watcher — Dynamic Port Forwarding
> This is the machine-runtime version (default). For the legacy Docker container
> runtime (`HD_RUNTIME=docker`) see the docker/ bucket.
## Why the daemon is still here on the machine
When a service inside the Adom-Workspace machine binds `0.0.0.0:<port>`, Lima's
vz networking makes it reachable from the Mac at `localhost:<sameport>` — so you
might think a port watcher is unnecessary. It isn't. The daemon is **RETAINED**
because that `0.0.0.0` exposure has two gaps the watcher closes:
1. **`127.0.0.1`-only listeners.** vz does NOT mirror loopback, so a
loopback-only listener inside the machine is never exposed on the Mac. VS Code
extensions (Codex, Copilot, GitLens) start OAuth callback servers that
frequently bind `127.0.0.1` — the browser redirects to
`localhost:{port}/auth/callback?code=...` but the Mac never had that port,
so it doesn't exist.
2. **Exposure lag / wedge.** macOS/Lima port exposure can lag or break
even for `0.0.0.0` listeners (a real HD-logged failure mode:
"code-server alive in the machine but the Mac can't reach 127.0.0.1:7380"). The
watcher gives HD a reliable host-side proxy that doesn't depend on Lima's
automatic exposure.
## The Solution
A lightweight daemon inside the machine monitors `/proc/net/tcp` for new
listening ports. When one appears (that wasn't there at boot and isn't a known
service), it notifies HD's control API. HD creates a host-side TCP proxy on
that same port, forwarding through code-server's `/proxy/{port}` to reach the
machine. The browser redirect just works — even for loopback-only callback
servers Lima wouldn't have exposed.
## Architecture
```
Adom-Workspace machine: macOS Host:
┌─────────────────────┐ ┌──────────────────────┐
│ Codex creates server │ │ HD Control API │
│ on 127.0.0.1:1455 │ │ (:47084) │
│ (loopback, no expose)│ │ │
│ port-watcher.sh │──POST──→ │ /port-forward │
│ detects port 1455 │ open │ creates TCP proxy │
│ │ │ on localhost:1455 │
│ code-server │ │ │
│ /proxy/1455/ ←───────┼───────── │ proxy forwards here │
└─────────────────────┘ └──────────────────────┘
↑
Browser redirects to
localhost:1455/callback
```
## What Gets Forwarded (and what doesn't)
**FORWARDED (temporary, appeared after boot):**
- OAuth callback ports from extensions (Codex, Copilot, etc.) — especially
`127.0.0.1`-only ones Lima cannot expose
- Any new TCP listener that wasn't running when the machine started
- Auto-expires after 5 minutes (OAuth callbacks are fast)
**NOT FORWARDED (known services, stay behind /proxy/{port} or 0.0.0.0 exposure):**
- code-server (8080 inside the machine; exposed as 7380 mapping/`0.0.0.0`)
- relay WS/HTTP (8765, 8766)
- adom-vscode (8821)
- shotlog (8820)
- Any port in the KNOWN_PORTS list
- Any port that was listening at machine boot time
- Any port ≤ 1023
This keeps the local workspace machine behaving like cloud Hydrogen — known
services use `/proxy/{port}` (or macOS/Lima exposure for `0.0.0.0` binds), and
only short-lived / loopback-only OAuth ports get a dedicated host proxy.
## API Endpoints
### POST /port-forward
Create or remove a host-side TCP proxy. Called by the machine's watcher.
```json
{"port": 1455, "action": "open"} // Create proxy
{"port": 1455, "action": "close"} // Remove proxy
```
### GET /port-forward
List currently forwarded ports.
```json
{"ok": true, "forwarded_ports": [1455], "count": 1}
```
## The Watcher Daemon
`scripts/port-watcher.sh` runs inside the machine:
1. Polls `/proc/net/tcp` every 2 seconds
2. Diffs against previous scan
3. New port (not in KNOWN_PORTS whitelist) → POST `/port-forward` open
4. Port gone → POST `/port-forward` close
5. Port open > 5 minutes → auto-expire (likely a service, not OAuth)
No boot snapshot — the whitelist is the sole source of truth. Services
like shotlog can start at any time (days/weeks later) and will never
get a host proxy as long as their port is in the whitelist.
The watcher reaches HD at the `adom-host` gateway plus the control port
(`<adom-host>:<control>/port-forward`). vz does NOT mirror loopback, so the
machine cannot reach HD at its own `127.0.0.1`; instead the machine's
`/etc/hosts` maps `adom-host` to the gateway, and that's where HD's host
listener lives. The control port is dynamic per launch; the live control URL
(already pointing at the gateway host) is in `~/.adom/hd-control-url`.
## Installation
The watcher is a bash script (`port-watcher.sh`) deployed into the machine
during setup steps. Installation uses `limactl shell hd-builder` + `nsenter`
into the machine, NOT `docker cp`/`docker exec`:
```bash
# Stage the script into the Lima VM, then install it into the machine via nsenter.
# (No \\wsl$ share / wsl cat exists on macOS — copy into the VM, then nsenter.)
limactl copy port-watcher.sh hd-builder:/tmp/port-watcher.sh
limactl shell hd-builder -- sudo nsenter -t <leader> -a -- bash -lc \
'cp /tmp/port-watcher.sh /usr/local/bin/port-watcher.sh && chmod +x /usr/local/bin/port-watcher.sh'
# Launch detached inside the machine (as the adom user)
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- bash -lc \
'nohup bash /usr/local/bin/port-watcher.sh >/tmp/port-watcher.log 2>&1 &'
```
The script parses `/proc/net/tcp` directly in `awk` — it filters listener rows
(state `0A`), splits the hex `local_address`, and converts the port with
`strtonum("0x" …)`. No Python is involved.
## UI: Toast + Port Mappings Dialog
**Toast notification:** When a port is forwarded, HD shows an info toast:
"Port 1455 forwarded to workspace". When removed: "Port 1455 forwarding stopped".
Powered by the `port-forward-changed` Tauri event.
**Port Mappings dialog:** Accessible from the Adom menu → Admin → "Port Mappings".
Shows exposed `0.0.0.0` ports AND dynamic watcher-created proxies. Dynamic
ports have a "Stop" button. Matches the API Explorer visual style.
## Adding New Known Services
When a new service is added to the machine that runs a persistent TCP server,
add its port to the `KNOWN_PORTS` whitelist string in `port-watcher.sh`
(`scripts/port-watcher.sh:16`):
```bash
KNOWN_PORTS="8080 8765 8766 8821 8820 36035 47084 47083 47085 1420 7380 8770 9000"
```
This prevents it from getting a dedicated host proxy. Services should bind
`0.0.0.0` (exposed by Lima) or use `/proxy/{port}` instead, consistent with web
Hydrogen.
## The forwarding layers
There are distinct forwarding mechanisms. The `/proc/net/tcp` watcher above
is **Layer 2** — and on macOS it is the **primary** path. Layer 1 (the
navigation-interception localhost proxy) is **Windows-only** and does NOT run on
macOS — see the note below.
### Layer 1 — the navigation-interception proxy (Windows-only; NOT active on macOS)
On Windows, HD intercepts top-level WebView2 navigations via
`setup_frame_navigation_interception()` (`hd-app/src/lib.rs`, `#[cfg(windows)]`).
On every navigation it runs `extract_oauth_callback_port()`, which
**triple-URL-decodes** the target URL (`urlencoding::decode` x3) and scans each
decode layer — plus the raw and the percent-encoded
`redirect_uri=http%3A%2F%2Flocalhost%3A…` form — for
`redirect_uri=http://localhost:<port>`. When it finds one it calls
`start_oauth_callback_proxy(port)`, which **pre-binds** `127.0.0.1:<port>` on the
host and waits up to ~5 min for one inbound request, then forwards the full path
into the machine via code-server (`http://localhost:<code_server_port>/proxy/<port>/<path>`)
and streams the response back to the browser.
**This interceptor does NOT exist on macOS.** It depends on WebView2's
`ICoreWebView2` navigation events; HD's macOS webview is **WKWebView**, which
exposes no equivalent navigation-interception (the same CDP gap that makes
`/eval` fire-and-forget on macOS). So on macOS the *automatic* zero-latency
pre-bind doesn't happen — the **port watcher (Layer 2) is the primary path**,
with the manual `POST /start-oauth-proxy` endpoint (Layer 3) as the explicit
fallback. The `/start-oauth-proxy` + `/oauth/callback` endpoints themselves are
cross-platform and work the same on macOS.
**Control / callback endpoints** (HD control API):
- `POST /start-oauth-proxy` — manually pre-bind a callback port (on macOS this is
the explicit way to get the Layer-1 effect, since the navigation interceptor
doesn't run).
- `GET /oauth/callback` — HD's own built-in callback receiver (used by the
Claude Code PKCE flow); do not call directly.
**Failure mode the AI may see:** if a proxy binds but code-server can't be
reached, HD returns a styled **502 Bad Gateway** page (dark `#0d1117`
background, teal `#00b8b0` "OAuth callback proxy" heading) reading roughly *"HD
tried to forward this OAuth callback to the workspace via code-server proxy
(port …/proxy/…), but it failed. The extension that started this OAuth flow may
need its callback port (…) mapped to the host."* Seeing that page in a browser
means the proxy bound but the machine-side proxy hop failed — check the HD log
for `[oauth-proxy]` lines and that code-server is alive.
### Layer 2 — the port watcher (`/proc/net/tcp` daemon — the primary path on macOS)
The bash daemon documented above. On macOS it is the workhorse (Layer 1's
navigation interceptor is Windows-only): it surfaces new callback ports —
including `127.0.0.1`-only callback servers Lima silently won't expose — and asks
HD to proxy them. Note: when an app has already registered its port via the
adom-port-hints protocol, the watcher skips it (it only owns the fallback for
third-party extensions that don't know HD exists).
### Layer 3 — manual
`POST /start-oauth-proxy` or `POST /port-forward` (or
`adom-cli port-forward register`) for cases the watcher misses (on macOS, where
the Windows-only navigation interceptor doesn't run, this is the explicit
pre-bind path).
## Port Conflicts
If the host port is already taken by another Mac app, the forwarding
fails silently. The OAuth callback will break. Mitigation: extensions
typically pick a different random port on retry. HD logs the conflict.
## How OAuth Callbacks Work End-to-End
1. Extension (Codex) starts HTTP server on `127.0.0.1:1455` inside the machine
2. port-watcher.sh detects port 1455 (not in boot snapshot, not known) —
note Lima would NOT have exposed this loopback-only listener
3. Watcher POSTs `{"port":1455,"action":"open"}` to HD
4. HD creates TCP proxy: `localhost:1455` → code-server `/proxy/1455/`
5. Extension opens browser with `redirect_uri=http://localhost:1455/callback`
6. User authenticates, browser redirects to `localhost:1455/callback?code=...`
7. HD's proxy receives the request, forwards into the machine via code-server
8. Extension receives callback, completes auth
9. Extension closes its server, port 1455 disappears
10. Watcher POSTs `{"port":1455,"action":"close"}`, HD removes proxy
## Debugging
All debugging runs inside the machine via `limactl shell hd-builder -- sudo
nsenter -t <leader> -a -S 1001 -G 1001 -- ...`, NEVER `docker exec`:
```bash
# Check what ports the watcher sees
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- bash -lc "cat /proc/net/tcp | awk '\$4 == \"0A\" { split(\$2, a, \":\"); printf \"%d\\n\", strtonum(\"0x\" a[2]) }' | sort -n"
# Check the watcher's own log
limactl shell hd-builder -- sudo nsenter -t <leader> -a -S 1001 -G 1001 -- cat /tmp/port-watcher.log
# Check what HD has forwarded (from the Mac host)
curl -sf http://127.0.0.1:47084/port-forward
# Manually forward a port (for testing, from the Mac host)
curl -sf -X POST http://127.0.0.1:47084/port-forward \
-H Content-Type:application/json \
-d '{"port":1455,"action":"open"}'
# Check HD log for port-fwd events
# Look for [port-fwd] entries
```
## Related skills
- `hd-networking` — `0.0.0.0` exposure, reaching the host at the `adom-host`
gateway (vz does NOT mirror loopback), NEVER do these
- `hd-ports` — bind `0.0.0.0` first; fall back to the port-forward registry for
`127.0.0.1`-only services or unreliable exposure