diff --git a/docs/content/1.guide/14.security.md b/docs/content/1.guide/14.security.md
index cf9973e9..b17bfc34 100644
--- a/docs/content/1.guide/14.security.md
+++ b/docs/content/1.guide/14.security.md
@@ -75,6 +75,7 @@ For your own auth UI, disable built-in handling with `otpParam: false`, then cal
- **Stay on loopback.** Bind to a routable address only intentionally, and require authentication when you do.
- **Keep `auth: false` local.** The hosted bridges (`devframeViteBridge`, `@devframes/next`'s handler) gate their side-car by default; opt out with an explicit `auth: false` only when the host framework owns the trust boundary another way.
+- **Gate the socket in a listener you own.** The `server` option binds its `upgrade` listener after async setup, so a guard that wraps the listeners present at startup misses it. To run your own check first, such as the peer address from `req.socket.remoteAddress` or a session cookie, leave `server` unset and own the listener. It sees every upgrade on the server, so act only on requests to `__ws` and leave the rest to the host framework. For those, attach an `error` handler to the socket, then call `devtools.handleUpgrade(req, socket, head)` when the check passes, or write a `403` response and destroy the socket when it fails. The built-in socket gate checks `Origin` and lets `Origin`-less clients through, so with `auth: false` on a non-loopback bind any client that reaches the port can open the socket.
- **The MCP route trusts same-machine callers, harden it when that's not your boundary.** Two gates enforce that default: an origin gate (loopback-only, `Origin`-less rejected) is browser DNS-rebinding hardening, and a peer-address gate rejects a non-loopback caller even with a forged loopback `Origin` (the socket address can't be forged the way a header can). So the `'auto'` default - which mounts the route once agent tools exist - and `mcp: true` are enough for a local dev tool. Neither gate proves *which* caller it is, though, so to intentionally reach the route beyond loopback (a widened `allowedOrigins`, a hosted app) or to expose destructive tools, add an identity check with `mcp: { authorization }` (a bearer from an env var, or a callback), which also lifts the loopback-peer restriction - or turn the route off with `mcp: false`. See [MCP](/adapters/mcp).
- **Treat tokens as secrets.** Never log the bearer token or the one-time code, or bake either into build output.
- **Authorize every handler.** Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them.
diff --git a/docs/content/2.adapters/1.initiate.md b/docs/content/2.adapters/1.initiate.md
index ff625b6e..fd21331a 100644
--- a/docs/content/2.adapters/1.initiate.md
+++ b/docs/content/2.adapters/1.initiate.md
@@ -121,7 +121,7 @@ Fetch handlers only hand over `Request`s, so the host framework binds the RPC so
1. **`ws.port`**: a side-car server on that exact port.
2. **`server`**: share the host framework's `node:http` server; the upgrade binds at `__ws`. No extra ports.
3. **`ws: { sidecar: true }`**: a side-car server on a free port, for host frameworks whose handlers never see upgrades (Next.js route handlers, Nitro, Rsbuild).
-4. **The host framework's own upgrades.** With none set, the socket waits: `devtools.attach(server)` routes a server's `upgrade` events (returning a detach fn); `devtools.handleUpgrade(req, socket, head)` completes a single one from a listener you own.
+4. **The host framework's own upgrades.** With none set, the socket waits: `devtools.attach(server)` routes a server's `upgrade` events (returning a detach fn); `devtools.handleUpgrade(req, socket, head)` completes a single one from a listener you own. To run your own check before the socket opens, own the listener, match the `__ws` path first (the listener sees every upgrade on the server, including the host framework's own), and call `handleUpgrade` once the check passes; the `server` option attaches its listener asynchronously, after any guard that wraps the listeners present at startup.
`ws.url` controls the *advertisement* instead, so the browser dials it verbatim. Alone, an external WebSocket server owns the transport and its auth (wire the running devframe's `context` via `createContextRpcServer` + a WS transport); alongside a local binding it overrides only the advertisement (the tunnel pattern).