expo/expo · error · CommandError

INVALID_OPTIONS

INVALID_OPTIONS

Error message

Cannot publish with sdkVersion UNVERSIONED.

What it means

Thrown by `getPublicExpoManifestAsync` (the manifest publisher) with code `INVALID_OPTIONS` when the resolved `sdkVersion` is the literal string `UNVERSIONED` and no `EXPO_SKIP_MANIFEST_VALIDATION_TOKEN` env var is set. `UNVERSIONED` is reserved for the Expo team's internal unreleased builds; publishing such a manifest would break OTA updates for real clients. This guard blocks accidental publishes from a dev/unversioned CLI.

Source

Thrown at packages/@expo/cli/src/export/getPublicExpoManifest.ts:27

/** Get the public Expo manifest from the local project config. */
export async function getPublicExpoManifestAsync(
  projectRoot: string,
  { skipValidation }: { skipValidation?: boolean } = {}
): Promise<ExpoConfig & { locales: LocaleMap; sdkVersion: string }> {
  // Read the config in public mode which strips the `hooks`.
  const { exp } = getConfig(projectRoot, {
    isPublicConfig: true,
    // This shouldn't be needed since the CLI is vendored in `expo`.
    skipSDKVersionRequirement: true,
  });

  // Only allow projects to be published with UNVERSIONED if a correct token is set in env
  if (
    !skipValidation &&
    exp.sdkVersion === 'UNVERSIONED' &&
    !env.EXPO_SKIP_MANIFEST_VALIDATION_TOKEN
  ) {
    throw new CommandError('INVALID_OPTIONS', 'Cannot publish with sdkVersion UNVERSIONED.');
  }

  return {
    ...exp,
    locales: await getResolvedLocalesAsync(projectRoot, exp),
    sdkVersion: exp.sdkVersion!,
  };
}

View on GitHub (pinned to b09195aac2)

Solutions

  1. Install and use a published version of `@expo/cli`/`expo` matching a released SDK (`npm i -g expo@latest` or the project pin).
  2. If you are Expo staff and legitimately need to publish an unversioned manifest, set `EXPO_SKIP_MANIFEST_VALIDATION_TOKEN` to the correct token.
  3. Confirm `app.json` does not hardcode `sdkVersion: 'UNVERSIONED'` and that `expo` in `package.json` resolves to a versioned package.
  4. Use `expo export` instead of `expo publish` if you only need static output (export passes `skipValidation`).

Example fix

// before — using a source build of expo, sdkVersion resolves to UNVERSIONED
expo publish

// after — install a published version
npm install --save-dev expo@latest
npx expo publish
Defensive patterns

Strategy: validation

Validate before calling

import { getConfig } from '@expo/config';
function assertVersionedSdk(projectRoot: string): void {
  const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true });
  if (exp.sdkVersion === 'UNVERSIONED' && !process.env.EXPO_SKIP_MANIFEST_VALIDATION_TOKEN) {
    throw new Error('Refusing to publish UNVERSIONED sdkVersion; install a published expo package');
  }
}

Type guard

function isVersionedSdk(v: unknown): boolean {
  return typeof v === 'string' && v !== 'UNVERSIONED' && /^\d+\.\d+\.\d+/.test(v);
}

Try / catch

try {
  await getPublicExpoManifestAsync(projectRoot, { skipValidation: false });
} catch (e) {
  if (e instanceof CommandError && e.code === 'INVALID_OPTIONS' && /UNVERSIONED/.test(e.message)) {
    // switch to a published CLI, or use expo export (skipValidation) instead of publish
  }
  throw e;
}

Prevention

When it happens

Trigger: Running `expo publish` (or any manifest publish) with a CLI built from source against an unreleased SDK, which reports `sdkVersion: 'UNVERSIONED'`, and no validation token in the environment. `skipValidation` (set during normal export) bypasses it.

Common situations: Using a monorepo that symlinks a source build of `@expo/cli`. Running a local fork of Expo. CI that inadvertently resolves to an unversioned CLI. Users who cloned expo/expo and ran their app against it.

Related errors


AI-assisted analysis of expo/expo@b09195aac2 (2026-08-12). Data as JSON: /api/errors/eb3dcc5f475e7071. Report an issue: GitHub.