You hop on a plane, want to keep iterating on the Rork prototype you started yesterday, type EXPO_OFFLINE=1 expo start --offline — and the CLI dies with Setting EXPO_NO_DEPENDENCY_VALIDATION or skipping validation via flags is forbidden in this environment. I'm Masaki Hirokawa, an indie developer running an iOS / Android app business since 2014 (collectively over 50 million downloads). Lately I've been using Rork for fast prototypes and testing them in offline-only environments — planes, exhibition halls, low-bandwidth trips — which is exactly where this error keeps showing up.
It's the collision between Expo CLI's safety net and Rork's default template configuration. Simply dropping --offline is not the fix. Triaged in the right order, every case has a clean path through.
What the error actually is — env vars and flags fighting for priority
Since Expo CLI 53, expo start validates that installed package versions match the SDK's expectations. Useful in normal use; awkward when you combine flags to go fully offline:
# The combination that triggers "forbidden" on Rork projects
EXPO_OFFLINE=1 EXPO_NO_DEPENDENCY_VALIDATION=1 expo start --offlineThe CLI maintains an internal list of "environments where skipping validation must not be allowed." When Rork's generated app.json matches certain shapes (more below), the CLI refuses your implicit or explicit attempt to skip validation and aborts with forbidden. You only wanted "no network," but the CLI heard "also turn off the safety check," and said no.
First move: see where 'forbidden' actually fires
Same wording, multiple sources. The first thing to do is re-run with verbose logs and look at the lines immediately before the failure.
# Verbose logs, captured to a file for inspection
EXPO_DEBUG=1 EXPO_OFFLINE=1 expo start --offline --verbose 2>&1 | tee /tmp/expo-start.log
grep -B5 'forbidden' /tmp/expo-start.log | tail -20In my experience the last few lines before forbidden always mention either validateDependenciesVersions (internal auto-skip path) or EXPO_NO_DEPENDENCY_VALIDATION (you set it explicitly). That distinction tells you which cause you're chasing.
Cause 1: Rork's expo.experiments.tsconfigPaths is on
Recent Rork templates set expo.experiments.tsconfigPaths: true in app.json. When that's on, Expo CLI refuses to auto-skip validation during --offline because TypeScript path resolution depends on it.
// app.json — typical shape generated by Rork
{
"expo": {
"name": "MyRorkApp",
"experiments": {
"tsconfigPaths": true,
"typedRoutes": true
},
"plugins": [
"expo-router",
"expo-font"
]
}
}Two paths through this: drop --offline and use expo start --no-dev --minify instead, or temporarily flip those experiments off for the offline session. For venue-side emergency starts I keep this script around:
#!/usr/bin/env bash
# scripts/expo-offline-safe.sh — start a Rork project safely offline
set -euo pipefail
cp app.json app.json.backup
# Temporarily disable tsconfigPaths and typedRoutes
node -e "
const fs = require('fs');
const j = JSON.parse(fs.readFileSync('app.json', 'utf8'));
if (j.expo && j.expo.experiments) {
j.expo.experiments.tsconfigPaths = false;
j.expo.experiments.typedRoutes = false;
}
fs.writeFileSync('app.json', JSON.stringify(j, null, 2));
"
trap "mv app.json.backup app.json" EXIT
EXPO_OFFLINE=1 expo start --offlineThe trap that restores app.json on exit is the important bit — without it you'll forget you turned the experiments off and spend tomorrow wondering why typed routes broke.
Cause 2: expo-router cache hasn't been generated yet
A fresh-clone Rork project using Expo Router won't have .expo/types/router.d.ts yet. On first offline start, the CLI declares "I need online access to generate this cache" and returns forbidden.
Run one online start to materialize the cache, then go offline:
# Run online once to generate the cache (Ctrl-C after 10–30s is fine)
expo start --tunnel
# Verify the cache exists
ls -la .expo/types/
# Now offline works
EXPO_OFFLINE=1 expo start --offlineIf .expo/ is in your .gitignore, every fresh clone needs this one-time online warmup.
Cause 3: Native prebuild hasn't run
If the project pulls in native modules like expo-haptics or expo-image and you haven't run npx expo prebuild, validateDependenciesVersions tries to compare against a non-existent native version and falls over.
Spot it: /tmp/expo-start.log mentions ios/ or android/. If yes, prebuild first:
# Prebuild without installing, then restart
npx expo prebuild --no-install
EXPO_OFFLINE=1 expo start --offline--no-install is intentional — you don't want pod install or Gradle sync attempting network operations while offline. The prebuild alone gives the validator something to compare against, and forbidden goes away.
Cause 4: Lockfile drift in pnpm / bun
On Rork projects using pnpm or bun, a package.json that has drifted from its lockfile makes the CLI distrust the resolved versions and respond with forbidden.
# Verify lockfile is in sync with package.json
pnpm install --frozen-lockfile # for pnpm
bun install --frozen-lockfile # for bunIf --frozen-lockfile fails, the lockfile is stale. Run a normal install online to refresh it, then retry the offline start.
Triage order
Any of the four can fire alone or together. The order I run:
First, look at the EXPO_DEBUG=1 log for tsconfigPaths — that's cause 1. If absent, check .expo/types/ for the router cache — cause 2. Then check whether ios/ and android/ directories exist — cause 3. Finally verify lockfile sync — cause 4.
Following that order avoids unnecessary prebuilds and tunnel restarts, and I can usually resolve forbidden in under five minutes even on flaky travel Wi-Fi.
A durable fix: package.json scripts
I don't trust myself to remember these commands, so I keep them in package.json:
{
"scripts": {
"start": "expo start",
"start:online-cache": "expo start --tunnel",
"start:offline-safe": "bash scripts/expo-offline-safe.sh",
"rebuild:native": "npx expo prebuild --clean --no-install",
"lock:sync": "pnpm install --frozen-lockfile || pnpm install"
}
}The pattern on a freshly cloned Rork project: npm run start:online-cache for 30 seconds to warm caches, then npm run start:offline-safe for everything else. With this combo I've had zero offline startup failures across the last several exhibition trips, even when the venue Wi-Fi was unreliable.
Stay aware of Rork's template defaults
The four causes above are general Expo knowledge, but Rork's template defaults change over time. The expo.experiments block sometimes ships with tsconfigPaths and typedRoutes set to true and sometimes to false, depending on when you generated the project. That single difference flips how prone you are to forbidden.
When you scaffold a new Rork project, eyeball app.json → expo.experiments first and decide whether it lines up with your offline plans. I recommend making that a habit — in my experience it's the single check that cuts offline-side troubleshooting time the most.
What to try next
Once offline startup is stable, the next two things worth checking are Hot Reload behavior offline and Metro's bundler cache management. Knowing when to clear .expo/cache/ keeps long-trip development calm. Thanks for reading.