I build a free CLI, npx nativekeel, that checks React Native and Expo apps for the problems that turn a routine upgrade into a three-week project. To keep its false alarms down, I run it on every open-source React Native app I can find. The test set is now 470 apps, from abandoned 2019 demos to large production apps like wallets, chat clients and note-taking apps.
Old apps fail in boring, expected ways: unsupported versions, the New Architecture off, Google Play's target SDK missed. So the numbers below are for the 194 apps that are already on React Native 0.76 or newer. These are apps whose developers keep them up to date, and they still have these problems.
What we found in up-to-date apps
| Problem | Share of up-to-date apps |
|---|---|
| A dependency with a known high or critical vulnerability in the exact version shipped | 21% |
| Still on React Native 0.76, which is not ready for 16 KB memory pages (Google Play requires this since Nov 2025 for apps targeting Android 15+; 0.77 fixes it) | 16% |
The template URL scheme myapp:// still in place |
14% |
30+ console.log calls shipped in the release build |
13% |
Whole-library lodash or moment imports |
12% |
| Images over 500 KB required by the app | 9% |
A secret committed to the repo or compiled into the bundle (cloud API keys, release signing passwords, tokens in eas.json) |
5% |
Across all 470 apps, including the old ones: 58% still have the New Architecture off, which means they cannot go past React Native 0.81. 28% target an Android SDK that Google Play no longer accepts for updates.
Four findings worth checking in your own app today
1. Expo config is not a safe place for secrets. Anything in app.json → extra, and every EXPO_PUBLIC_ variable, ships inside the app and can be read by anyone who downloads it. And eas.json is committed with your code: three apps had a Sentry auth token in its build env, which can read and change the whole Sentry organisation. Public SDK keys (RevenueCat, Firebase web config) are fine. Server keys belong in EAS secrets (eas env:create).
2. myapp:// is shared with every other app that kept it. The Expo template sets "scheme": "myapp". If you sign in with expo-auth-session, Clerk or AppAuth, the OAuth redirect goes to myapp://, and on Android any other installed app with the same scheme can show up to receive it. Use a scheme unique to your app.
3. Unverified App Links open in the browser. An https://your.domain/... intent filter without android:autoVerify="true" (and a matching assetlinks.json) opens in the browser on Android 12+, not in your app. On older Android, any app can register the same link. This matters most for login, password reset and invite links.
4. Passwords travel further than you think. The leak that surprised us most was not a hacked server. It was a password that ended up in a crash report or analytics event after passing through a couple of variables (const info = email + ':' + password; track('signup_failed', { info })). Searching for password next to track( does not find it; you have to follow the variable.
What we got wrong, and fixed
Scanning real apps is mostly about finding where the tool is wrong:
- A wallet app installed a package from git that has the same name as an npm package with a malware advisory. Git dependencies are now never matched against npm advisories.
-
.DS_Storefiles caught in a patch-package patch are harmless. They are no longer reported as "build output that will not apply". - Android verifies every web host when any intent filter has
autoVerify. Our first deep-link check missed that.
Every release lists what changed: https://nativekeel.com/changelog
Try it
npx nativekeel # report in the terminal
npx nativekeel plan # an ordered upgrade plan
npx nativekeel --html report.html
It runs locally. Your code never leaves your machine: the only network calls are package-name lookups and the same advisory request npm audit makes. It has zero dependencies, publishes with npm provenance, and is MIT licensed. There is also a GitHub Action that adds findings to pull requests.
These are static checks. They find the common, detectable mistakes, but they are not a penetration test. A clean report means none of these known problems, not "unhackable".
If something it reports is wrong for your app, please open an issue on GitHub. False alarms are the bugs I care about most: https://github.com/AlicanAkyol/nativekeel
Top comments (0)