← All field notes

What goes wrong inside Framer code components

Exposed API keys, code imported from hosts nobody recognises, and content turned into executable markup. What I look for in Framer code components, and why the safe-looking keys matter as much as the dangerous ones.

Jul 26, 2026 · 7 min read


Field Notes from Thunkle, a studio that takes AI-built apps from prototype to secure, production-ready software.

Most of a Framer site is not code, which is the appeal. The parts that are code are the code components, and they are where the risk collects. A component that needs to call an API needs a key. A component that needs a library sometimes imports it from a URL. A component that renders content from somewhere else has to decide how much it trusts that content. None of those decisions get reviewed again once the thing works.

So I built a plugin that reads them and reports what it finds, gave it away free, and this is what it looks for.

Why it reads components and not the Code panel

Worth saying up front, because it is the first question anyone sensible asks.

Framer has two things called code. There are code components, the .tsx files under Assets, and there is custom code, the named scripts under Settings with a placement dropdown. They carry the same risk. A key pasted into either one ships to the browser and can be read by anyone who opens developer tools.

A plugin can only read the first.

Framer's plugin API has a method called getCustomCode(), and the obvious reading of the name is that it returns the project's custom code. It does not. It returns whatever your own plugin put there. I confirmed it the dull way: pasted a script into the Code panel of a real project, saved it, and printed the character count of all four locations. Zero, zero, zero, zero, while the script sat visible in the settings panel. The documentation says a plugin can detect custom code "set by your plugin", which read carefully is the whole answer, and read at speed looks like a description of custom code in general.

There is no way around it. Nothing in the API returns user-authored custom code or site settings.

The practical consequence for anyone using the plugin: a clean scan does not mean a clean site. If you keep keys in Settings, check that panel yourself. Everything below is about the part a tool can actually see.

Credentials that end up in components

This is the most common real finding, and it happens for an ordinary reason. A component needs to fetch something. Standing up a server to hold the key is a bigger job than the component itself. The key goes in the file, the component works, and it stays there.

The scanner looks for fifteen credential formats, including the ones people reach for when a component needs to call a model: OpenAI, Anthropic, Groq, Replicate, Hugging Face and Perplexity, alongside Stripe, AWS, Supabase, GitHub, SendGrid, Slack, Twilio, Resend and Convex.

Here is the part that separates a useful scanner from a noisy one, and it is the same point I have made about reviewing apps by hand.

Supabase issues two keys per project. Both are JSON web tokens, both are long, and side by side you cannot tell them apart. The anon key belongs in the browser, and its safety comes from row-level security policies on the tables behind it. The service_role key bypasses those policies completely: read, change or delete any row in the database. The only thing separating them is one field inside the token, so the scanner decodes it and branches on that. service_role is critical. anon is not reported at all, because reporting it would be wrong.

Same for Firebase browser keys, which are protected by security rules rather than secrecy, and Stripe publishable keys, which exist to be shipped in a page. A tool that flags those has not found anything. It has taught you its output is noise, and the next thing you skip past will be the real one.

Code imported from somewhere else

Framer lets a component import a module straight from a URL. This is genuinely useful and mostly fine, and it has a consequence people do not always think through: whoever controls that host controls part of your site. If the file at that address changes tomorrow, your site runs the new version.

The scanner flags two cases. Anything imported over plain http, which can be altered in transit and which browsers block anyway. And anything from a host that is not a recognised package CDN. Imports from esm.sh, jsDelivr, unpkg and Skypack stay quiet, because that is normal Framer practice. An import from a domain nobody recognises is worth a second look, particularly on a site someone else built.

Patterns that turn content into code

Three things in a component are worth knowing about regardless of where the content comes from.

dangerouslySetInnerHTML puts raw HTML into the page. If any part of that HTML comes from a URL parameter, a CMS field, or anything a visitor can influence, someone can run their own scripts on your site. React names it the way it does for a reason.

eval and new Function run whatever string they are handed. If that string can be influenced from outside the file, it is a direct route to arbitrary code.

And reading a key, token or password out of location.search or location.hash. Anything in a URL is kept in browser history, sent to other sites in referrer headers, and captured by analytics. It is a common copy-paste pattern and it quietly leaks the thing it is handling.

Redirects that quietly broke

Not a component problem, but it is the other thing on a Framer site nobody goes back and reads, so the plugin checks it too.

When a redirect points at a page that has since been deleted, Framer sets its destination to null. That is a broken redirect, and there is no indication of it in the redirect list. Alongside that: chains that hop twice before landing, loops that error out in the browser, and two rules fighting over the same source path where only one can win and nothing tells you which. After a site migration this is usually a mess, and it is invisible until someone follows an old link.

What I got wrong first

Before publishing I ran the detection patterns against strings that are not secrets, which I should have done before writing any of them. Four out of six benign inputs came back flagged.

A git commit hash in a comment was reported as an exposed Twilio credential, because a Twilio key identifier is the letters SK followed by thirty-two hex characters, and so is half the hashes in a codebase. A CSS class name, sk-spinner-container-large-variant, was reported as an OpenAI key, because I had allowed hyphens in the pattern and any long kebab-case string satisfies it.

Both were fixed the same way. Require a match to mix character classes, so an all-lowercase string cannot qualify. And for formats that are shape-identical to ordinary text, require supporting context in the file before reporting at all: no mention of Twilio, no Twilio finding. Zero false positives on the same inputs afterwards, with every real key still caught.

The lesson is not about regular expressions. One confident wrong finding costs more trust than ten correct ones earn, and the only way to know which you have written is to test the tool against things that are fine.

What it will not tell you

It cannot see custom code, for the reason above. It cannot see your published pages, because there is no API for the rendered site. And it cannot tell you whether a tracking script fires before someone consents, because proving that needs a browser loading the live page and watching the network, which nothing inside the editor can do.

It runs entirely in your browser and makes no network requests at all. It is a small static bundle, so you can open developer tools and confirm that rather than take my word for it.

Get it

The plugin is free on the Framer Marketplace. No account, no tiers, no licence key.

If it finds something that needs more than a copy-paste fix, that is the work we do at Thunkle: reviewing apps for exactly these issues, fixing them, and making sure the fixes hold as the app changes. You can request a review or reach us at thunkle.ai.

Working on something like this?

If you're migrating, securing or building an AI-made app, Thunkle can help. Get a fixed quote in 24 hours.

Get a quote