diff --git a/PRIVACY.md b/PRIVACY.md
index 7f75b23..60f786c 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -1,6 +1,6 @@
# Privacy policy — Clay Slip
-_Last updated: 2026-05-13_
+_Last updated: 2026-05-19_
Clay Slip is a developer tool. It runs entirely on your device, in your browser. **It does not collect, transmit, sell, or share any personal data.**
@@ -22,10 +22,10 @@ This document is the canonical privacy disclosure for the extension. It's distri
Clay Slip uses the standard WebExtension storage APIs (`chrome.storage` on Chromium, `browser.storage` on Firefox — same shape, same data, same guarantees). Stored data never leaves the user's device or browser-vendor account.
-| Storage area | Contents | Why |
-| --------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
-| `storage.sync` | UI preferences (theme, panel position/size, site host mappings, highlight mode + intensity, shortcut toggle) | Carries your settings across browsers when you're signed in to Chrome / Firefox Sync. |
-| `storage.local` | Sticky-note annotations pinned to component URIs; "recently viewed components" history (capped, configurable) | Keeps notes and history available offline; not synced because they may include page-specific context. |
+| Storage area | Contents | Why |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
+| `storage.sync` | UI preferences (theme, panel position/size, site host mappings, window globals list, highlight mode + intensity, shortcut toggle) | Carries your settings across browsers when you're signed in to Chrome / Firefox Sync. |
+| `storage.local` | Sticky-note annotations pinned to component URIs; "recently viewed components" history (capped, configurable) | Keeps notes and history available offline; not synced because they may include page-specific context. |
You can clear everything from the extension's **Options** page (Reset preferences, Clear history) or via your browser's _Manage extensions_ → _Site access / storage_ controls (Chromium) or `about:addons` → Clay Slip → _Remove_ (Firefox).
@@ -38,6 +38,7 @@ To do its job, the content script reads:
- The `data-uri` and `data-editable` attributes that Clay sites set on rendered components.
- Standard `
` metadata (``, `` tags, ``, JSON-LD) for the SEO tab.
- The text/HTML of components you explicitly select for the JSON tab and Diff tab.
+- **Only the top-level `window.*` globals you explicitly list on the Options page (Window globals)**, and only when you open the **Globals** tab or click **Refresh**. The extension injects a tiny one-time bridge script into the page's main world, which reads `window[key]` for each configured key, `JSON.stringify`s it, and posts the result back to the panel. The script never reads any global you didn't configure, and never reads anything until you ask the Globals tab for data.
This data is **only ever displayed inside the panel on your machine.** It is never sent anywhere except, when you explicitly ask, to the same Clay host the page came from (see "Outbound network requests" below).
diff --git a/README.md b/README.md
index 8510a8d..6a29aa7 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ The same source builds for both browser families:
- **Shareable selection links** — copy a `?clay-slip-select=…` URL that auto-opens the panel and selects the same component on someone else's machine
- **Component screenshot to clipboard** — one-click PNG of any selected component, panel auto-hides during capture
- **SEO tab** — title / meta / og / twitter / JSON-LD with a Twitter + Facebook card preview and lints (length, missing image, duplicate `
`, etc.)
+- **Window globals tab** — surface any top-level `window.*` value your page sets at boot (e.g. `nymGtmPage`, `dataLayer`, custom analytics payloads) as syntax-highlighted JSON. Configurable list per install; arrays and objects render identically; per-row and tab-level refresh, no ambient polling
- **Recently viewed components** persisted across sessions, with one-click jump back
- **Resizable + dockable panel** — drag the inner edges (or the inner-corner grabber) to resize width _and_ height; choose any of four corners or a full-height left/right side dock
- **Refined highlight modes** — _Off_, _Selection_ (default; pristine page, hover and click highlight in blue, hold ⌃ Control to flash the rainbow over every component), _Editable only_ (always-on subtle corner accents on `[data-editable]`), or _All components_ (always-on rainbow over every component, like the original Clay devtools). Hover and selected always paint in a single blue accent — outline + inset tint — so the "you clicked it" feedback reads consistently across every mode, on top of either the rainbow or the corner-accent ambient layer. Top-left labelled badge follows your hover and selection. Switch modes from the panel header dropdown or with the h shortcut.
@@ -140,6 +141,7 @@ Stored preferences/notes can also be cleared from the Options page (**Clear rece
| Show shortcut overlay | Press ? |
| Toggle FAB ↔ panel | Press [ or click the collapse button / the FAB |
| Switch tabs | Press i (Inspect) or t (Tree) |
+| Read a window global | **Globals** tab → expand the row for the configured global; **↻** re-reads from the current page |
| Open settings | Click the gear icon in the panel header |
## Screenshots
@@ -170,6 +172,24 @@ Example for a Vox-Media-style multi-brand setup:
Hostnames are matched **exactly** (case-insensitive) — no prefix stripping or wildcards — so the mapping does what you wrote and nothing more. There’s no separate global env config; if a host isn’t in any mapping, the extension falls back to whatever host the Clay component URI itself encodes, which is also the page’s host.
+### Window globals
+
+The **Window globals** section on the options page lists the top-level `window.*` keys you want to inspect on every Clay page. Each configured global gets its own collapsible card in the **Globals** panel tab.
+
+- Enter the name as either `nymGtmPage` or `window.nymGtmPage` — the extension strips the prefix and normalizes the rest. Whitespace around the value is trimmed.
+- Both **objects** and **arrays** render identically — the panel just shows their `JSON.stringify` output with syntax highlighting.
+- Reads happen on initial tab open and on explicit **Refresh** clicks (per-row or tab-level **Refresh all**). There is no background polling — the page only does work when you ask it to, so this tab has no measurable runtime cost.
+
+Limitations and edge cases (surfaced inline in the tab):
+
+- **Top-level only.** Nested paths (`foo.bar.baz`) and array indices (`dataLayer[0]`) aren’t supported yet. Add the top-level global; expand the row to drill into the structure.
+- **`(not defined on this page)`** — the global isn’t set on the current page. Most often a navigation timing issue: try Refresh after the page has finished loading.
+- **`(value is not JSON-serializable)`** — the global is a function or `Symbol`. `JSON.stringify` can’t round-trip those; nothing the extension can do beyond surfacing the fact.
+- **`(could not serialize: …)`** — the value contains a circular reference. The underlying JS error is embedded in the message.
+- **`(could not read from this page)`** — a strict Content Security Policy or Trusted Types policy blocked the page-bridge injection. Refresh the page and try again; if it persists, the host page’s CSP is the cause.
+
+Internally this works by injecting a tiny one-time script into the page’s main world that listens for postMessages and replies with the JSON-stringified value. The script never reads anything you didn’t configure on the options page. Full design write-up: [`docs/specs/2026-05-19-window-globals-tab.md`](docs/specs/2026-05-19-window-globals-tab.md).
+
## Build from source
For contributors and anyone who wants to run the extension from a local checkout instead of a release zip:
diff --git a/docs/specs/2026-05-19-window-globals-tab.md b/docs/specs/2026-05-19-window-globals-tab.md
new file mode 100644
index 0000000..8fabe4f
--- /dev/null
+++ b/docs/specs/2026-05-19-window-globals-tab.md
@@ -0,0 +1,184 @@
+# Window Globals tab
+
+**Status:** approved 2026-05-19 — ready for implementation
+**Branch:** `feat/window-globals-tab` (off `master` @ `0641e15`)
+**Target:** v2.4.0 (new feature, additive, no breaking changes)
+
+## Summary
+
+A new **Globals** tab in the panel that surfaces the values of user-configured `window.*` globals on the host page. Built for inspecting analytics data layers (GTM, Segment, etc.) without opening DevTools.
+
+The user configures a list of top-level global names on the Options page (e.g. `nymGtmPage`, `window.dataLayer`). The Globals tab renders one collapsible section per configured global, each showing the value as syntax-highlighted JSON. Arrays and objects render identically. Data is read on tab open and on explicit per-section / "Refresh all" clicks — there is no ambient polling.
+
+## Motivation
+
+Clay pages routinely carry per-page analytics data on `window.*` globals. Today you have to switch to DevTools, type the path, and re-stringify on each navigation. Surfacing them inside the panel:
+
+- Lets you eyeball analytics data while inspecting components in the same panel.
+- Standardises naming across the team (everyone configures the same set once via Options).
+- Works in passive edit mode (the panel still mounts on `?edit=true` pages, this is read-only and DOM-noninteractive — fine to run there).
+
+## User-facing behavior
+
+### Options page (`Options.tsx`)
+
+A new section **"Window globals"** below "Site host mappings". Same UX as the site-host rows: one text input per entry, add/remove buttons. Inline help text:
+
+> Top-level globals to show in the Globals tab. Enter `nymGtmPage` or `window.nymGtmPage` — both forms work. Values are serialized with `JSON.stringify`, so functions and `Symbol` values are dropped.
+
+The list persists to `chrome.storage.sync` as `preferences.windowGlobals: string[]`.
+
+### Globals tab (`GlobalsTab.tsx`)
+
+- **Position:** last tab in the bar (after Notes).
+- **Empty state:** when `windowGlobals.length === 0`, render copy: _"No globals configured. Add some in Options → Window globals."_ with a button that opens the Options page.
+- **Populated state:** one `` element per configured global, **all closed by default**. Summary line shows the path name (the normalised form — `nymGtmPage`, not `window.nymGtmPage`).
+- **Opening a section** triggers a read for that path if no cached value exists. Already-cached values render immediately; the section also shows a "Refresh" button to re-read.
+- **Tab-level "Refresh all" button** in the tab header re-reads every configured path.
+- **Resolved values** render via the existing `JsonPreview` (which uses `highlightJson` + the existing copy-button + JSON-syntax-highlighting). Arrays and objects look identical apart from their bracket style — that's the whole point.
+- **Unresolved values** (`undefined` on `window`) render as muted text: _"(not defined on this page)"_, with a tooltip explaining common causes (typo, global set after load, page not yet hydrated).
+- **Serialization errors** (circular ref, throwing getters) render as muted text: _"(could not serialize: )"_.
+
+### Tab order
+
+```
+Inspect | Tree | JSON | Diff | SEO | Notes | Globals
+```
+
+## Architecture
+
+### Why a page-bridge is needed
+
+Content scripts run in an **isolated world** — their `window` is not the page's `window`. Reading `window.nymGtmPage` directly from the content script returns `undefined` even if the page has it. To bridge:
+
+1. Inject a `