Open More → Code in a Rork project and a file tree appears, with src/ and components/ stretching down the page. I've scrolled through that tree and closed it again more than once, with no idea where to start reading.
Rork's documentation says keys belong in Secrets, not in your code and not in a chat message. What we rarely do is check that the promise held, that no key actually ended up in the code.
Here's the short version: you don't have to read every file. Keys have habits, in how they look and in what they're called, and you can have a machine look for exactly those. Below is a small script, and what to do when it finds something.
Get the code onto your own machine first
The Code tab is readable as it is. On the free plan the editor shows "Read-only" and won't let you edit, but the docs say you can still read every file. If you only want to look with your own eyes, that's enough.
To run a script, connect GitHub and clone the repository. According to the docs, a connected project becomes an ordinary repository with one commit per agent turn. Point the script at the cloned folder and you're set.
One reassuring detail: values you put in Secrets live with the project, not in the code, and are not committed (the docs' FAQ says so). The reverse follows. If a key shows up in the GitHub copy, it got there without going through Secrets.
Keys show up in three recognizable ways
I look for three things.
| Signal | How to find it | Typical hit |
|---|---|---|
| The shape of the key | Provider prefixes such as sk-, sk_live_, AIza, ghp_ | OpenAI-style keys, Stripe secret keys, GitHub tokens |
| A secret-looking name given a literal value | Names containing SECRET, TOKEN or API_KEY, assigned a string of 16+ characters | A weather API key written into a constant |
A secret under EXPO_PUBLIC_ | EXPO_PUBLIC_ followed by SECRET, SERVICE_ROLE and similar | A Supabase service role key read under a public name |
The third one ties straight to a warning in the docs. In Rork, a variable starting with EXPO_PUBLIC_ is sent to the app itself, and everything else stays on the server. Give a private key that prefix and anyone who installs the app can extract it.
The script
It runs on the Python 3 standard library alone and looks at .ts, .tsx, .js, .swift, .kt, .plist and a few other file types.
#!/usr/bin/env python3
"""Scan code exported from Rork (via GitHub) for hard-coded keys."""
import re, sys, pathlib
SKIP_DIRS = {"node_modules", ".git", "Pods", "build", "dist", ".expo"}
EXTS = {".ts", ".tsx", ".js", ".jsx", ".json", ".swift", ".kt", ".env", ".plist", ".md"}
# 1) The shape of the key itself
SHAPES = {
"OpenAI-style sk-": re.compile(r"\bsk-[A-Za-z0-9_-]{20,}"),
"Stripe secret key": re.compile(r"\b[sr]k_(live|test)_[A-Za-z0-9]{16,}"),
"Google API key": re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b"),
"GitHub token": re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,}"),
"Resend": re.compile(r"\bre_[A-Za-z0-9]{20,}"),
}
# 2) A secret-looking name assigned a long literal string
ASSIGN = re.compile(
r"""(?i)\b([A-Z0-9_]*(SECRET|TOKEN|PRIVATE|SERVICE_ROLE|API_?KEY)[A-Z0-9_]*)\b\s*[:=]\s*["']([^"'\s]{16,})["']"""
)
# 3) A secret-looking name under the public prefix
PUBLIC_SECRET = re.compile(
r"(?i)\bEXPO_PUBLIC_[A-Z0-9_]*(SECRET|PRIVATE|SERVICE_ROLE|TOKEN)[A-Z0-9_]*"
)
def walk(root):
for p in root.rglob("*"):
if p.is_file() and p.suffix in EXTS and not any(s in p.parts for s in SKIP_DIRS):
yield p
hits = 0
for path in walk(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")):
for n, line in enumerate(path.read_text(errors="ignore").splitlines(), 1):
found = [name for name, rx in SHAPES.items() if rx.search(line)]
if ASSIGN.search(line) and "process.env" not in line:
found.append("secret-looking name with literal value")
if PUBLIC_SECRET.search(line):
found.append("secret-looking name under EXPO_PUBLIC_")
for f in found:
hits += 1
print(f"{path}:{n}: [{f}]") # never print the line itself
print(f"-- {hits} hits")
sys.exit(1 if hits else 0)It prints only the file, line number and kind of match. I left out the line's contents on purpose, so that screen-sharing or pasting the output into Rork's chat can't leak the key a second time.
Run it from the cloned folder:
python3 scan_keys.py ./my-rork-projectWhat it printed on a sample project
I can't use real keys here, so I built a small project with fake values that only imitate the shapes: a file with a weather API key in a constant, one with an sk- style string in apiKey, one reading EXPO_PUBLIC_SERVICE_ROLE_KEY, a Firebase config JSON, and one clean file.
The output:
demo/src/weather.ts:1: [secret-looking name with literal value]
demo/src/ai.ts:1: [OpenAI-style sk-]
demo/src/ai.ts:1: [secret-looking name with literal value]
demo/src/ai.ts:3: [secret-looking name under EXPO_PUBLIC_]
demo/config/firebase.json:1: [Google API key]
-- 5 hitsThree things stand out. Lines like process.env.RESEND_API_KEY, which only read an environment variable, and a setting like TOKEN_REFRESH_SECONDS = 3600 were not flagged. Line 1 of ai.ts matched two signals at once, so one line can be counted twice by design. And the last point matters most.
A Firebase warning is usually not a leak
The final hit, in firebase.json, is not a real leak. The API key inside a Firebase config is described in Google's documentation as different in nature from an ordinary secret key. It ships inside the app by design, and the protection comes from Firebase's security rules and restrictions, not from hiding the key.
The GoogleService-Info.plist in my own iOS apps has an API key field too. I included .plist files in the scan for coverage, and I read hits there as false positives.
So treat the output as a list of what to doubt first, not as a breach notice. Sort each hit with three questions:
- Would it matter if anyone who installs the app could see this value?
- If yes, who could use it, and who would receive the bill or the damage?
- Is the key still live, or already revoked?
When it's real, the order is everything
The tempting mistake is to delete the key from the code in Rork and call it done. The docs give four steps for a key that has ended up somewhere public:
- Revoke or rotate the key in the provider's own dashboard. That is the only step that actually stops it working.
- Create a new key and copy its value.
- In Rork's Secrets, put the new value under the same variable name and press Save.
- If the key was in your code, ask Rork to remove it and read the value from the variable instead.
Fix only the code first and the old key is still in your GitHub history, where it still works. Revoking at the provider always comes first.
A request that names the file and the destination tends to land better:
src/ai.ts has an API key written directly on line 1.
Remove it and read OPENAI_API_KEY from a server function instead.
Do not use an EXPO_PUBLIC_ name.Where a key should live in the first place, environment variables or an Edge Function, is covered separately in Is That API Key Inside Your App?.
One thing to do today
A place for your keys isn't decided until you've checked that they're there. Run this script once before you publish, and again at the end of any day you added a big feature. That alone is a good first habit.
Zero hits means the signals didn't match, not that you're safe. Still, it costs far less than reading dozens of files by eye, and it catches the most common mistake first.
Once keys live on the server, the next question is how much to trust what the app tells that server. A premium article covers why a rewarded-ad payout shouldn't depend on a signal from the device alone: Don't Pay Out Rewarded-Ad Credit on the Device's Word Alone.