diff --git a/README.md b/README.md
index 1acc166..80bfcee 100644
--- a/README.md
+++ b/README.md
@@ -42,9 +42,10 @@ heatmap**, a gym SBD panel and links to friends' sites.
A spinner covers the frame while the site boots. Discord sends `DENY` and keeps a link card
instead.
- **Friend sites in windows** — clicking a friend's desktop icon opens their site in a
- window. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they load live;
- `skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so the window frames the bundled mirror
- at `/skopec.html` and the `www` button opens the real site.
+ window. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they load directly;
+ `skopec.dev` sends `X-Frame-Options: SAMEORIGIN`, so it loads through `/api/frame`, a
+ header-stripping proxy that serves the live page from our origin. The `www` button always
+ opens the real site.
- **Minecraft-style SMP window** — the SMP shortcut opens a join menu modelled on
Minecraft's multiplayer screen, with live status, player count, MOTD and version pulled
from mcstatus.io through the site's own ping API.
@@ -156,6 +157,7 @@ they contend for `:3000` and `.next/dev/lock`.
| --- | --- | --- |
| `/api/heatmap` | `GET` | Proxies the user's contribution heatmap from `git.dvorakmortonxd.dev`. |
| `/api/activities` | `GET` | Fetches the GitDXD activity feed and keeps only the user's own events. |
+| `/api/frame?url=…` | `GET` | Header-stripping proxy for a friend's site that refuses framing (`skopec.dev`). **Allow-listed hosts only**; fetches the HTML server-side (so the site's `X-Frame-Options` never reaches the browser), injects a `` for relative assets and a click shim for in-site links. Rejects other hosts, non-http(s) schemes and credentials, and never follows redirects. |
| `/api/ping?url=…` | `GET` | Health-checks an **allow-listed** service (the site's own `*.dvorakmortonxd.dev` hosts and the friends' sites). Handles `minecraft://host:port` via mcstatus.io; treats any non-5xx response as "up". Other hosts and schemes are rejected and redirects are never followed, so it can't be used to probe the LAN (SSRF guard). |
## 📁 Project Structure
diff --git a/agents.md b/agents.md
index eb293ce..d947705 100644
--- a/agents.md
+++ b/agents.md
@@ -84,6 +84,7 @@ src/app/
heatmap/route.ts → proxy for git.dvorakmortonxd.dev contribution heatmap
activities/route.ts → proxy for the user's GitDXD activity feed
ping/route.ts → service health check (HTTP + minecraft:// via mcstatus.io); host allowlist
+ frame/route.ts → header-stripping proxy so an unframeable friend site loads in a window; host allowlist
components/
ThemeRoot.tsx → picks which shell to show (the router of the UI)
TerminalLayout.tsx → "terminal" theme (Gruvbox)
@@ -299,10 +300,16 @@ list reordering, not free placement. Don't confuse the two.
is the `embed` flag; `friend.url` is what the frame loads and `friend.realUrl`
is what the www button opens — `createWebsiteApp`'s `embedUrl` option lets the
two differ. `bukovinafilip.com` and `vlcak.me` send no framing headers, so they
- frame the live site; `skopec.dev` sends `X-Frame-Options: SAMEORIGIN` on every
- path, so `friends[0].url` points at the bundled `/public/skopec.html` mirror
- and only the button is live. Making a blocking site load *live* would need a
- header-stripping proxy (allowlisted) — not implemented.
+ frame the live site. `skopec.dev` sends `X-Frame-Options: SAMEORIGIN` on every
+ path, so `friend.proxy` routes it through `/api/frame`.
+- `/api/frame` is a **host-allowlisted** header-stripping proxy for sites that
+ refuse framing. It fetches the HTML server-side (so the upstream
+ `X-Frame-Options` never reaches the browser), injects ``
+ so relative assets still resolve to the real site, and injects a click shim so
+ same-host navigation is proxied too. It only proxies `ALLOWED_HOSTS`
+ (`skopec.dev`), never follows redirects, rejects credentials and non-http(s),
+ and returns HTML only — it is public, so keep it that narrow. The framed page
+ executes under our origin, so only allow-list hosts you trust.
- **Embedded iframes swallow the pointer.** While dragging/resizing a window,
the moment the cursor passes over a service `