Privacy
Last updated 4 August 2026
Using LoupeKit needs no account. The identifier the product runs on is a random UUID your browser generates on first run and keeps locally — nothing to sign up for, no email, no password. That covers every feature except one.
The exception is team sharing, and it is opt-in. Handing a palette to a colleague means the product has to know who the colleague is, so that feature asks for an email address — the only personal data in the whole system. If you never turn it on, none of it applies to you and no address is ever stored.
This page describes every byte that leaves your browser, field by field. It is written to be checked against the code, not taken on trust.
Running an audit
A Vibe Audit is the only feature that sends anything to us, and it runs only when you press the button. It sends the payload below to https://api.loupekit.comand nothing else.
Identity
- domain
- Hostname only — example.com, never the path or query
- deviceId
- A random UUID this browser minted. Not tied to a person
stats.dom — integers
- totalDomNodes
- How many elements the page has
- inlineSvgCount
- Inline <svg> elements
- maxNestingDepth
- Deepest element nesting
- deepNestingCount
- Elements past the depth threshold
- stockRadixCount
- Unstyled Radix/shadcn data attributes
- semanticElementCount
- header, nav, main, section, article, footer
- emptyWrapperCount
- Bare <div>s wrapping exactly one child
- duplicateBlockCount
- Repeated subtree shapes — tag names only, never text
- textLength
- How many characters of visible text the page has — the count, never the text
- markupLength
- How many characters of markup surround it
stats.css
- longTailwindCount
- Elements carrying a long utility string
- maxUtilityClassCount
- The longest such string, as a count
- arbitraryValueCount
- Uses of Tailwind arbitrary values
- stockColorClasses
- Class names such as bg-slate-50 — names, not content
- hasCustomProperties
- Whether the page declares CSS variables
- distinctSpacingValues
- How many different padding, margin and gap values are painted
- declaredClassCount
- Class names in the stylesheets this browser could read
- unusedClassCount
- How many of those match nothing on the page
- readableStylesheets
- How many stylesheets were readable at all
- distinctRadiusValues
- How many different corner radii are painted
- maxShadesPerHue
- Most shades of one hue — a count, never a colour value
- inlineStyleCount
- Elements carrying a style attribute
- classedElementCount
- Elements carrying a class attribute
stats.js
- aiBuilderGlobals
- Names of generator globals found on window
- heavyLibraries
- Names of large libraries detected
- totalScriptCount
- How many script tags
stats.copy — the only free text
- pageTitle
- The <title>, truncated and redacted
- h1Texts
- Heading text, truncated and redacted
- h2Texts
- Heading text, truncated and redacted
- hasCustomFavicon
- Whether a favicon is declared
- placeholderImageCount
- Images pointing at a placeholder service
- wideTextBlocks
- Paragraphs estimated to run past a comfortable line length
- textBlockCount
- How many paragraphs were long enough to measure
stats.a11y — integers
- imageCount
- How many images the page has
- imagesMissingAlt
- How many carry no alt attribute
- controlCount
- Buttons and links
- unnamedControls
- How many have no readable name
- inputCount
- Form fields
- unlabelledInputs
- How many have no label of any kind
Headings are the only free text in the payload. Before they leave the page they are stripped of street addresses and long identifier runs, then truncated — in that order, because truncating first can leave a recognisable fragment that the pattern no longer matches. The server truncates everything a second time, on the principle that a script running inside someone else's page is not a trustworthy place to enforce a limit.
What is never sent
- — Body text, paragraphs or any content below the headings listed above
- — Values typed into any field, including passwords, emails and payment details
- — Cookies,
localStorage, or session tokens - — Full URLs. Only the hostname is transmitted — never a path, query string or fragment
- — Anything at all from a tab you did not run the audit on
- — Screenshots, markup, or a copy of the page in any form
What is stored afterwards
Two rows. One in a users table, keyed by your device id, holding your remaining free audits and — if you buy Pro — your plan and Stripe customer id. One in an aggregate table recording the hostname, the score and the verdict.
The aggregate row carries no device id. Not omitted by policy — the table has no column for one. Joining a device to the domains it audited would reconstruct browsing history, which is the one thing this extension exists to avoid doing.
Abuse limits
The API counts requests per network address so a script cannot mint device ids in a loop and exhaust the service. The counter belongs to Cloudflare, resets within a minute, and holds a number — your address is never written to our database, never logged, and never joined to an audit, a domain or a device id. Your browser reveals its address to every server it contacts, including this one; the limit reads it and stores nothing.
Vulnerability dating
On the Stack tab, Pro users can press a button that checks the detected versions against published security advisories. It is the only part of that tab that leaves your browser, it only ever runs when you press it, and it is listed here on its own because it is a second request to us — the audit is not the only one any more.
- What is sent is a list of package names and version numbers, such as
jquery 3.4.0. That is the whole request body. - No hostname, no URL, no page title, no headings, no page content. Unlike an audit, there is nothing in this request that identifies the site you are on.
- Nothing from it is written to our database. Audits are counted per domain; this is not counted at all, because there is no domain in it to count.
- Our API asks OSV.dev, Google's aggregator over the GitHub Advisory Database and the CVE list. Your browser never contacts them, and they never see your address.
- Answers are cached at our edge under a hash of the package list and under each advisory's own identifier. Neither key contains anything about you or your device.
A result says a version has a published advisory against it. It does not say the page can be attacked through it — that depends on how the library is used, which nothing outside the browser can see. Silence is equally narrow: versions we could not read, or could not resolve to a package, are reported as unchecked rather than counted as clean.
Everything else stays local
Stack detection, the colour palette, the font matrix, the asset inventory, the box model, live element edits, the class optimizer, the copy sanitizer and the responsive tester all run entirely inside your browser. None of them sends anything to us on its own. Results leave when you copy them or save the asset archive — both built on this device — and, if you have signed in, when you press Share on a palette or an environment. That third one is the only route by which anything extracted reaches a server, it needs an explicit press every time, and what it sends is described under "Accounts and team sharing" below.
Four of these keep something between sessions, and all four keep it here. Your audit history is the last twenty scores, with the domain, the verdict, the date and up to six earlier scores for the same site so the panel can say whether it got worse — it is stored in this browser and is never uploaded, because the same list held on our side would be a record of which sites you looked at, which is the thing our anonymous audit log is deliberately built not to contain. The stack comparison holds exactly one pinned report. The environment diff holds exactly one captured page — its stack, palette and fonts, plus the hostname and the path, never the query string. The colour picker keeps the last hundred colours you sampled alongside the site each came from, for the same reason and under the same rule. Each is replaced rather than accumulated, cleared from its own panel, and removed with the extension.
The colour picker takes a screenshot
There is no way to read a rendered pixel out of a page. The colour on screen at a point is the result of a background, an image, a gradient, an overlay, opacity and blend modes, and no single element reports it — so the loupe magnifies an image of the tab instead. That image is captured by the browser, handed straight to the page script that draws the lens, and discarded when the lens closes. It is never written to disk, never uploaded, and never taken of a tab you have not opened LoupeKit on: the capture runs on the same one-tab grant every other feature does. LoupeKit hides its own panel and overlays for the instant the picture is taken, so what you sample is the site rather than our own interface.
The form checker types into fields
It is the one tool that writes to the page rather than reading it, and it does so to ask the browser a question only a filled field can answer: whether a pattern, a minimum or a maximum rejects a value a person would reasonably type. Each field is written, checked and restored to its original value within the same synchronous pass, so nothing else on the page ever observes the value we wrote. Nothing is submitted. Password and payment-card fields are skipped entirely. Only the browser’s own validation message is returned to the panel — never anything that was in the field.
The Developer tab changes the page
That is what it is for, and every one of its switches is reversible: turning one off restores exactly what it changed — stylesheets re-enabled, inserted labels deleted, marker attributes removed, and any style attribute it stripped put back with the value it held. Reset page does all of them at once. The handful of one-shot actions that cannot be undone — removing form validation, releasing image sizes, re-requesting images past the cache — each say so on their own description, and a reload is what puts them back. Nothing the tab reads or writes is transmitted, and none of it survives the tab being closed.
Its cookie view reads document.cookie — the same cookies the page itself can read — rather than asking for the browser’s cookies permission, which would grant access to every cookie on every site the extension can reach. HttpOnly cookies are therefore invisible to it, which is exactly what that flag is for. Nothing read there is stored, logged or sent anywhere.
The User-Agent switcher
When you switch it on, the browser asks for permission to rewrite request headers, and the extension installs one rule: it sets the User-Agent header and the two client hints that travel with it. It applies to every tab, not only the one you invoked LoupeKit on — there is no per-tab form of that rule — which is why there is a green dot on the toolbar icon and a strip inside the panel for as long as it is on. Nothing is read from any request; the rule only sets an outgoing header, and no record of it leaves your browser. Turning it off removes the rule and hands the permission back in the same action.
The WebRTC address control
A page can open a peer connection with no permission and no click from you, and the candidates it gathers carry addresses of your machine — the public one, and the private one it holds on your own network. This control changes what the browser is willing to offer. When you choose a policy, the browser asks for the privacy permission, which is broad in name: it covers read and write over every privacy setting the browser exposes, and Chrome offers no narrower form. LoupeKit writes exactly one,network.webRTCIPHandlingPolicy, and reads that same setting to detect when another extension is holding it. It reads and writes no other privacy setting.
Like the switcher above, it applies to every tab — it is a browser setting rather than a page one — so the same green dot appears on the toolbar icon while it is on. Choosing Browser default clears the setting and removes the permission in the same action, in that order. Nothing about the choice, the setting or any address is transmitted anywhere: the whole feature is a call to the browser’s own settings API.
If you uninstall
The browser opens a feedback page on this site afterwards. It carries the version you had installed and nothing else — no device identifier, no plan, no install date, nothing that could connect a removal to any audit you ever ran. Everything the extension kept was kept in your browser, so it goes with it: the audit history, the picked colours, the pinned stack and any API key you had saved. There is nothing on our side to delete.
Requests made on your behalf
Four, and none of them goes to us. They are listed here because "everything stays local" would otherwise be a sentence with an exception hidden behind it.
Domain and DNS lookups
The Domain tab can look up who a domain is registered to and what its DNS records are. Both lookups are buttons and neither runs on its own, because each one tells somebody else which site you are looking at — which is the thing the rest of this page says LoupeKit does not do.
- Registration goes to rdap.org, which redirects to the registry that actually holds the record. It receives the domain name and nothing else.
- DNS goes to cloudflare-dns.com. It receives the domain name and the record type being asked about.
- Both are sent with
credentials: 'omit'and no referrer: no cookie, and nothing that says which page you were on. - The full URL is never sent — only the registrable domain. A path, a query string and a fragment are all stripped before the request is built.
- Neither answer is stored. Closing the panel forgets it.
The rest of that tab — the page's language, its favicons and the measurement ids it carries — is read from the document you already have open and is never transmitted at all.
Reading the page's own response headers
Stack detection makes a single HEAD request to the page you already have open, when you open the panel. It is how a CDN, a cache or a web server can be identified at all — those leave nothing in the page itself, only a header on its response.
- It goes to the same address as the page. No third party is contacted.
- It is sent with
credentials: 'omit'— no cookie, no session, noAuthorizationheader. The site cannot tie it to your logged-in account. - Only the response headers are read. The body is never requested.
- Nothing from it is stored or transmitted anywhere.
Fetching the page again, for the rendering check
SEO → Rendering compares the HTML a server sends with the page your browser ended up with — the difference a search crawler sees. Answering that needs the served document, so this is the one reader that asks for a page rather than reading the one in front of it. It is a button and it never runs on its own.
- It requests the same address as the page you are on, from that page. No third party is contacted and no permission for other sites is involved.
- It is sent with
credentials: 'omit'and no referrer, so the server sees a signed-out visitor. The panel says so on the result — a page that differs for a logged-in visitor differs here for that reason. - The HTML that comes back is parsed without a browsing context: its scripts never run, and nothing it links to — images, stylesheets, trackers — is fetched.
- Neither document is stored or transmitted. The comparison happens in the tab and is forgotten when the panel closes.
Asking the site for robots.txt and its sitemaps
SEO → Crawler answers whether a search engine or an AI crawler may fetch the page you are on. That is decided in a file the page never loads, so this reader asks the site for it. Two buttons, two different amounts of asking, and neither happens until you press one — the panel names the exact files above them.
- The first press requests two things from the site you already have open:
/robots.txt, and aHEADon the page itself to read one header. No third party is contacted. - Both are sent with
credentials: 'omit'— no cookie, no session — because a crawler is signed out and that is the reading being taken. - The second press follows the sitemaps the file declares. Only sitemaps on the same address as the page are fetched: one declared on another host is listed and marked, never requested. The extension holds no permission for other sites and this does not ask for one.
- The walk stops at a fixed number of files and says how many it left unread, so a partial count is never presented as a total.
- Nothing from any of it is stored or transmitted. No address, no rule and no sitemap reaches our server or anybody else's.
Checking whether a build ships its source maps
Tools → Source maps answers a question nothing in a browser shows: whether the scripts on this page serve a map, and what that map exposes. It is a button and it never runs on its own.
- It asks the site you are on for the last few kilobytes of each of its own scripts, using a
Rangerequest, and then for any map those point at. Same address as the page, signed out, no third party. - Scripts hosted somewhere else are counted and never requested. The extension holds no permission for other sites.
- When a map embeds the original source, the extension records only that it does. The source text itself is discarded where it is read and never reaches the panel, a report or anywhere else.
Looking up field data (Pro)
Tools → Load timing can look up what real visitors to the page got over the last 28 days. That data lives in Google’s Chrome UX Report and is keyed on the address, sothis is the one feature where the address of a page you are looking at leaves your machine. It is a button, it never runs on its own, and the panel says so above it.
- The address goes to our API, which forwards it to the Chrome UX Report. Nothing else about the page travels — no content, no headers, no measurements.
- The query string and the fragment are removed before it goes anywhere, along with any username or password in the address. A session token, a promo code or a document id in a query string never leaves.
- Our server writes none of it down. There is no row in our database, no analytics event and no log line containing the address or the site — the lookup is a pass-through and the answer is discarded once it reaches your panel.
- Google receives the address as part of the lookup, as they would for anyone querying that API. What they do with it is governed by their own terms; we send them nothing that identifies you, your device or your account.
- Every other timing number in the extension — the whole load breakdown above this panel — is measured in your own browser and stays there. Only this one leaves.
Showing you an image, or copying one
In the asset list, hovering a row previews that image, and pressing PNG copies it to your clipboard. For an inline SVG nothing is fetched at all — the markup is already in the panel. For a linked image both point at the same URL the page itself loaded, so it is normally served from your browser's cache and no new request leaves at all. When one does, it is sent with no referrer and no credentials, only at the moment you hover or press the button, and never to us.
Copying a linked image needs the server to permit a cross-origin read. Many do not, and LoupeKit holds no permission for other sites that could work around it — in that case the button copies the image's URL instead and says so, rather than reporting a success it did not have.
The Server tab (Pro)
Six readings ask our server to fetch the page you are on, from its own address, signed out: what each crawler is served, what the very first response already sets before any consent banner, what the site does when asked for in another language, how each asset is cached and compressed, whether the hosts it links to still answer, and what link preview each platform would draw. None of these can be answered from your browser — a browser cannot become Googlebot, cannot un-see a banner that has already run, and cannot check a cross-origin link without announcing you to it.
They are grouped into one tab with one paragraph above them, because a promise that holds on one screen and not the next is not a promise. Nothing here runs on its own; every button says what it will request before you press it.
- The query string is removed before anything is fetched, along with any username or password in the address, and the report tells you it happened. Beyond the usual reason, this feature replays the address from our machine — a sign-in link or a password reset would be spent by it.
- The page itself never comes back to us or to you. Each reading returns counts, statuses and header values read out of the response; the document is read and discarded. Cookie names are kept and cookie values are not — the value is the identifier the reading is about.
- Our server writes none of it down. No row in our database, no analytics event, no log line containing the address. When something fails we log the shape of the failure and never the exception, which would name the resolved address.
- We refuse to fetch addresses that are not on the public internet — private and reserved ranges, localhost, names that only resolve on your own network — and we re-check that on every redirect the site sends us to, not only on the address you started with.
- The site you are reading about sees a request from our address, as it would from any crawler or link checker. It does not see anything about you: no cookies, no referrer, and nothing that identifies your device or your account.
- Each report says where it was taken from. Our server runs in the Cloudflare location nearest you, so a behaviour that varies by country is being read from one region — and that is the only place your rough location appears. It is shown to you and stored nowhere.
Reports you publish at a link
The one feature that keeps anything about a page on our server, and it only runs when you press Publish. What is being sold is the hosting: an audit dies when the panel closes, and an extension cannot produce a link.
- Stored: the score, its verdict, and each finding’s label, severity and sentence — plus a title and the site’s hostname.
- Not stored: the page. No markup, no selectors, no screenshots, and nothing of the address beyond the hostname — a path like /invoices/4182is a fact about your site that would otherwise travel with a document you forward.
- The link is unguessable and is the only credential, so the page is served
noindex, with no caching by shared caches and no referrer. It contains no JavaScript and fetches nothing, so opening it tells nobody — including us — that you did. - Every report carries an expiry you choose, up to ninety days. After it, the link answers “no longer available” whether or not anything has deleted the row. You can withdraw one earlier from the panel.
Opening on page load
By default LoupeKit can only reach the tab you clicked the toolbar button on, and that access ends at the next navigation. There is one setting that changes this: Open on page load, which is off unless you turn it on. Switching it on asks your browser for access to every site — appearing before you click cannot be done any other way — and switching it off hands that access straight back.
What it does not change is what leaves your browser. An audit still runs only when you press the button, still sends only the fields listed above, and still sends nothing at all from a page you did not audit. The setting decides when the panel appears, not what is collected.
Your own AI keys
If you add an OpenAI, Anthropic or Google key, it is stored in your browser's extension storage and sent directly from your browser to that provider. It never reaches our servers — not in a request, not in a log line, not in an error report. We never see it, so we cannot lose it.
Extension storage is not encrypted. Anything running in this extension can read a saved key, and so can anything with access to your browser profile on disk. That is a real limitation and the settings screen states it before you paste anything. Remove a key there and the browser permission for that provider is handed back with it.
Payments
Checkout is handled by Stripe on Stripe's own pages. Card details never touch our infrastructure. We receive a customer id, a plan, and the email you gave Stripe.
This website
No analytics, no cookies, no tag manager, no embedded third-party content. Fonts are served from this domain. There would be something absurd about a privacy page that loaded a tracker to tell you it was measuring how many people read it.
Accounts and team sharing
Only if you sign in. Nothing below happens otherwise, and every other feature keeps working without it.
- One field is stored: your email address. No name, no password, no profile. Signing in sends an eight-character code to that address; the code is stored only as a SHA-256 hash, expires in fifteen minutes, and works once.
- The address is passed to Brevo, which sends that message. It is the only processor that ever sees it, and the only message ever sent to it.
- Members of a team can see each other's addresses. That is what a team is; there is no way to share with someone anonymously.
- Nothing about your browsing is stored by signing in. Audits stay counted per domain with no user attached to them, and signing in does not change that.
What a shared item contains
A shared palette or environment snapshot is not the extraction the panel shows you. It is rebuilt from a small set of named fields before it is sent, and rebuilt again from those same fields on our side — so a client that sent more would not succeed in storing it.
- A palette carries colour values, the role LoupeKit guessed, and a rank. The element counts do not travel:
675 elementsis a measurement of how large your application is. - An environment carries the host, the technologies with their versions, the palette and the font families. The host is what an environmentis. The path, query and fragment are dropped, not shortened.
- Detection evidence never travels — the script URL, header value or CSS variable that matched. Those name internal routes and internal hosts.
- Asset URLs, CSS selectors and page content are not part of a shared item in any form.
Removing your data
Uninstalling the extension deletes the device id, meaning nothing links you to the stored row any more. To have the row itself deleted, emailprivacy@loupekit.com with your device id — it is shown in the extension's settings.