CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-04-04Advanced

Shipping iOS/Android Apps Monthly as a Solo Developer with Claude Code and Expo

A workflow for solo developers shipping iOS/Android apps on a monthly cadence using Claude Code and Expo. Covers CLAUDE.md setup, AdMob monetization, EAS Build, and App Store review hurdles that come up in practice.

Claude Code197React Native3Expo2Mobile DevelopmentCross-PlatformIndie Dev22AdMob12

Why Maintaining Both Platforms Solo Gets Heavy Over Time

If you run a mobile app business as an individual, maintaining iOS and Android in parallel turns into a heavier load than people expect. Running a portfolio that has accumulated 50 million downloads, I spent years where most of my time went to absorbing platform differences rather than adding features. Native development means learning Swift and Kotlin separately, writing platform-specific UI from scratch, and managing two separate release pipelines with their own quirks and certification requirements. Even with Expo/React Native reducing that burden significantly, the gap between "working prototype" and "polished, monetized app in the store" still requires real effort — especially when you are building and maintaining multiple apps alone.

Claude Code closes that gap in ways that compound across every phase of development. By feeding Claude Code your project context through a well-crafted CLAUDE.md, you get an AI pair programmer that understands your architecture, follows your conventions, and generates code that fits your codebase on the first try. Combine this with Expo's unified toolchain — from development to App Store submission via EAS — and you have a workflow where a single developer can realistically ship a polished, monetized app in a fraction of the time it once took.

This guide is grounded in the practical experience of developers who have shipped multiple apps using this exact stack. It walks through every major phase: project setup, architecture design, UI component generation, AdMob and IAP monetization, automated CI/CD with EAS, error diagnosis, and App Store submission. If you already have some familiarity with Expo or React Native and want to ship faster and more consistently, this is the workflow to adopt.


Prerequisites and Tools

Before starting, make sure you have the following in place:

  • Node.js: 18 or higher. Version 20 LTS is recommended for the best Expo SDK 52 compatibility.
  • Claude Code: Latest version (npm install -g @anthropic-ai/claude-code). Verify with claude --version.
  • Expo CLI: Use npx expo without installing globally, or npm install -g expo-cli if you prefer.
  • EAS CLI: npm install -g eas-cli. This is essential for automated builds and store submissions via Expo Application Services.
  • Xcode: Required for iOS Simulator and provisioning profile management on macOS.
  • Android Studio: Required for Android Emulator. Ensure the Android SDK and emulator images are installed.

Claude Code integrates naturally with VS Code. The combination of Claude Code in the integrated terminal and your code in the editor — side by side — makes reviewing and refining generated code fast and intuitive. If you use a different editor, Claude Code still works from any terminal window.

One important setup note: before running claude in a new project, ensure your ANTHROPIC_API_KEY environment variable is set. Add it to your .zshrc or .bashrc so it persists across sessions.


Step 1: Project Initialization and Designing Your CLAUDE.md

Creating the Project

# Create a new Expo project with the blank TypeScript template
npx create-expo-app MyApp --template expo-template-blank-typescript
cd MyApp
 
# Add essential dependencies upfront
npx expo install expo-router expo-constants expo-status-bar
npx expo install @react-native-async-storage/async-storage
npx expo install react-native-safe-area-context react-native-screens
npx expo install zustand
npx expo install @tanstack/react-query

Why CLAUDE.md Is Your Most Important File

Before running claude for the first time, invest fifteen minutes writing a thorough CLAUDE.md. This is the single highest-leverage action you can take in the entire workflow. CLAUDE.md functions as a persistent system prompt that Claude Code reads at the start of every session, giving it the context it needs to generate code that actually fits your project — the right patterns, the right folder structure, the right naming conventions — without you having to explain it every time.

For mobile development specifically, CLAUDE.md needs to answer questions that are easy to overlook: What version of Expo SDK are you targeting? Are you using NativeWind or raw StyleSheets? Expo Router or React Navigation? Which state management library? These details determine whether generated code compiles on the first try or requires significant cleanup.

Designing Your CLAUDE.md for Mobile Development

# MyApp — CLAUDE.md
 
## Project Overview
Cross-platform app built with Expo SDK 52 + TypeScript + Expo Router.
Targets iOS and Android. Primary audience: daily-life users aged 20–40.
App category: productivity / lifestyle.
 
## Tech Stack
- Framework: Expo SDK 52 (React Native 0.76)
- Navigation: Expo Router v4 (file-system-based routing)
- State management: Zustand
- Styling: StyleSheet API (NativeWind is NOT used in this project)
- Data fetching: TanStack Query v5
- Type safety: TypeScript strict mode enabled
- Animation: React Native Reanimated 3
 
## Coding Conventions
- Always use functional components (never class components)
- Define props using interfaces, not type aliases
- File names must match the component name in PascalCase (e.g., HabitCard.tsx)
- Custom hooks must start with "use" (e.g., useHabitData.ts)
- Test files live in __tests__ folders adjacent to the source file being tested
- Always handle loading and error states explicitly in components that fetch data
 
## Folder Structure
app/          ← Expo Router routes (file = route)
components/   ← Reusable UI components
hooks/        ← Custom React hooks (logic only, no JSX)
stores/       ← Zustand stores
services/     ← API clients and third-party service wrappers
constants/    ← Colors, typography, spacing, and app-wide constants
assets/       ← Images, fonts, and icons
utils/        ← Pure utility functions
__tests__/    ← Tests (mirror the source structure)
 
## Environment and Secrets
- Use expo-constants to read APP_VARIANT and switch between dev/staging/production
- Never hardcode API keys — read them from .env via expo-constants
- .env.local is git-ignored; .env.example documents required variables
 
## Important Rules
- Minimize Platform.OS branching; prefer cross-platform patterns wherever possible
- Use React Native Reanimated (not the legacy Animated API) for all animations
- All screens must handle both portrait and landscape orientations
- Always wrap screens in SafeAreaView from react-native-safe-area-context
- Prefer expo-* packages over raw React Native community packages when Expo provides them

This CLAUDE.md pays dividends immediately. When you prompt "create the home screen component," Claude Code generates code that uses Reanimated (not Animated), wraps the root in SafeAreaView, reads colors from your constants file, and uses Zustand for state — all without you having to say any of that explicitly.


Step 2: Architecture Planning with Claude Code

Once your CLAUDE.md is ready, launch Claude Code:

claude

Start with a high-level architecture conversation before writing any code:

This is a simple daily habit-tracking app.
Please read CLAUDE.md first. Then propose:
1. A folder structure and file list for Expo Router
2. A component inventory for each screen (Home, Add/Edit, Stats, Settings)
3. The shape of the Zustand store for habit data
4. Any custom hooks we should extract upfront

Claude Code reads CLAUDE.md and returns a concrete, immediately actionable plan — file paths, component names, store interface shapes. Agreeing on this structure before writing code is what keeps the codebase consistent as it grows.

Generating the Zustand Store

Create stores/habitStore.ts.
Each habit has: id, name, color, reminderTime (string | null, HH:MM format),
and completedDates (string array, YYYY-MM-DD format).
Include AsyncStorage persistence using Zustand's persist middleware.
Also export a set of selectors: todayCompletionRate, streakForHabit(id).

Claude Code output:

// stores/habitStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { format } from 'date-fns';
 
interface Habit {
  id: string;
  name: string;
  color: string;
  reminderTime: string | null;
  completedDates: string[];
}
 
interface HabitStore {
  habits: Habit[];
  addHabit: (habit: Omit<Habit, 'id' | 'completedDates'>) => void;
  updateHabit: (id: string, updates: Partial<Omit<Habit, 'id'>>) => void;
  toggleCompletion: (id: string, date: string) => void;
  deleteHabit: (id: string) => void;
}
 
export const useHabitStore = create<HabitStore>()(
  persist(
    (set) => ({
      habits: [],
      addHabit: (habitData) =>
        set((state) => ({
          habits: [
            ...state.habits,
            { ...habitData, id: Date.now().toString(), completedDates: [] },
          ],
        })),
      updateHabit: (id, updates) =>
        set((state) => ({
          habits: state.habits.map((h) =>
            h.id === id ? { ...h, ...updates } : h
          ),
        })),
      toggleCompletion: (id, date) =>
        set((state) => ({
          habits: state.habits.map((h) =>
            h.id === id
              ? {
                  ...h,
                  completedDates: h.completedDates.includes(date)
                    ? h.completedDates.filter((d) => d !== date)
                    : [...h.completedDates, date],
                }
              : h
          ),
        })),
      deleteHabit: (id) =>
        set((state) => ({
          habits: state.habits.filter((h) => h.id !== id),
        })),
    }),
    {
      name: 'habit-storage',
      storage: createJSONStorage(() => AsyncStorage),
    }
  )
);
 
// Selectors (used outside the store to avoid re-renders)
export const todayCompletionRate = (habits: Habit[]): number => {
  if (habits.length === 0) return 0;
  const today = format(new Date(), 'yyyy-MM-dd');
  const completed = habits.filter((h) => h.completedDates.includes(today)).length;
  return completed / habits.length;
};

Type definitions, persistence middleware, AsyncStorage integration, and selector utilities — all in one shot. This is the kind of boilerplate that usually takes an hour to write carefully; Claude Code produces it in seconds, following the conventions in your CLAUDE.md.


Step 3: Scaling UI Component Production

The productivity gains from Claude Code compound dramatically during UI development. Here is how to drive component creation efficiently.

Establishing a Design System First

Always create your design system constants before generating any UI components:

Create constants/Colors.ts and constants/Typography.ts.
Primary: #6C63FF (purple), background: #F8F9FA (light gray), text: #1A1A2E.
Include dark mode variants using Appearance from react-native.
Font sizes: 12, 14, 16, 18, 24, 32. Include a useThemeColors hook that
returns the right palette based on the current color scheme.

With a design system in place, every component Claude Code generates pulls from the same color palette and type scale. The visual consistency you get without manually enforcing it is one of the underrated benefits of this workflow.

Generating Full Screen Components

Create app/(tabs)/index.tsx with:
- Today's date in the header (formatted as "Wednesday, April 4")
- Habits from useHabitStore rendered as a FlatList
- Each habit is a card with the habit color as a left border accent
- Tapping a card toggles today's completion with a spring animation (Reanimated)
- A completion rate progress bar below the header
- FAB in the bottom right to navigate to the add-habit screen
- Empty state illustration when no habits exist yet
Please use SafeAreaView and handle both portrait and landscape.

From this prompt, Claude Code generates a complete, typed, ~200-line component with proper Reanimated hooks, FlatList configuration with keyExtractor, and empty state handling. Iterative refinements are natural:

The completion toggle feels a bit harsh. Add a subtle scale animation
(scale to 0.95, then back to 1) when the user taps a card, in addition to
the existing color change.
The progress bar isn't animating on mount. Use withTiming from Reanimated
so it fills from 0 to the current value over 600ms when the screen loads.

Each refinement adds one focused behavior. After a few iterations, you have a polished component with animations that would take a junior developer half a day to produce independently.


Step 4: Implementing AdMob Monetization

AdMob remains the most accessible revenue stream for apps with a broad free user base. The implementation has several moving parts; Claude Code handles them cleanly.

Installation

npx expo install react-native-google-mobile-ads

Prompt for the Full Implementation

Implement AdMob using react-native-google-mobile-ads:
- Banner ad: always visible at the bottom of the home screen
- Interstitial ad: shown with 30% probability after a habit is marked complete
- Use test IDs in development, real IDs from expo-constants in production
- Suppress all ads when the user has isPremium: true in the store
- Export a useBannerAd hook and a useInterstitialAd hook separately

Generated interstitial hook:

// hooks/useInterstitialAd.ts
import { useEffect, useRef } from 'react';
import {
  InterstitialAd,
  AdEventType,
  TestIds,
} from 'react-native-google-mobile-ads';
import Constants from 'expo-constants';
 
const AD_ID =
  process.env.NODE_ENV === 'production'
    ? (Constants.expoConfig?.extra?.admobInterstitialId as string)
    : TestIds.INTERSTITIAL;
 
export function useInterstitialAd() {
  const ad = useRef(
    InterstitialAd.createForAdRequest(AD_ID, {
      requestNonPersonalizedAdsOnly: true,
    })
  );
  const loaded = useRef(false);
 
  useEffect(() => {
    const onLoaded = ad.current.addAdEventListener(AdEventType.LOADED, () => {
      loaded.current = true;
    });
    const onClosed = ad.current.addAdEventListener(AdEventType.CLOSED, () => {
      loaded.current = false;
      ad.current.load(); // Pre-load the next ad immediately
    });
    ad.current.load();
    return () => {
      onLoaded();
      onClosed();
    };
  }, []);
 
  const showWithProbability = (probability = 0.3) => {
    if (Math.random() < probability && loaded.current) {
      ad.current.show();
    }
  };
 
  return { showWithProbability };
}

In-App Purchases for a Premium Plan

Implement subscriptions using expo-iap:
- Monthly: $4.99 (product ID: premium_monthly)
- Annual: $34.99 (product ID: premium_yearly)
- Manage purchase state in Zustand, persisted to AsyncStorage
- Include a restorePurchases() function for the Settings screen
- Handle all error cases: user cancelled, already purchased, network failure,
  receipt validation failure

Adding the error handling prompt is critical. IAP error management is where many implementations fall short — Claude Code generates the complete state machine when you explicitly ask for it.


Step 5: Automating Builds with EAS

Configuring eas.json

Create eas.json with three build profiles:
- development: developmentClient, internal distribution
- preview: internal distribution for TestFlight / Firebase App Distribution
- production: autoIncrement enabled, both iOS and Android, with submit config
Reference environment variables from .env.development, .env.preview, and .env.production

Generated eas.json:

{
  "cli": { "version": ">= 10.0.0" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": { "APP_ENV": "development" }
    },
    "preview": {
      "distribution": "internal",
      "env": { "APP_ENV": "preview" }
    },
    "production": {
      "autoIncrement": true,
      "env": { "APP_ENV": "production" }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "YOUR_APPLE_ID",
        "ascAppId": "YOUR_APP_STORE_ID",
        "appleTeamId": "YOUR_TEAM_ID"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services.json",
        "track": "internal"
      }
    }
  }
}

autoIncrement: true eliminates the manual build number bumping that bites every indie developer at least once. EAS increments the build number automatically on every production build.

GitHub Actions for CI/CD

Create .github/workflows/eas-build.yml.
Trigger: push to the main branch.
Steps: checkout, setup Node, install EAS CLI, run eas build --platform all
--profile production --non-interactive, then eas submit --platform all
--profile production --non-interactive.
Use EXPO_TOKEN from GitHub Secrets for authentication.

With this workflow in place, every merge to main triggers a full build for both platforms and submits to TestFlight and the Google Play internal track automatically. You can review builds and promote them to release without ever opening App Store Connect's build uploader.


Step 6: Diagnosing and Fixing Errors with Claude Code

React Native errors are often cryptic. The key to getting useful help from Claude Code is providing complete context — not just the error, but also the relevant file contents and what action triggered it.

Native Module Conflicts

Running npx expo start throws this error on iOS simulator:
[paste the complete error message and stack trace]
Please diagnose the root cause.
Check package.json for version mismatches and app.json for missing native setup.

Claude Code typically identifies the exact library causing the conflict and whether the fix is a version pin, a metro configuration change, or a missing native module setup step.

Metro Bundler Cache Issues

When changes don't appear in the simulator, Metro's aggressive caching is usually the culprit:

# Ask Claude Code: "My changes aren't appearing. What should I try?"
# Typical response includes these commands:
npx expo start --clear
npx react-native start --reset-cache
watchman watch-del-all && watchman watch-project .

EAS Build Failures

Copy the full build log from the EAS dashboard (not just the final error line) and paste it:

My EAS iOS production build failed. Here is the complete build log:
[paste log]
Is this a Podfile issue, a signing issue, or something in app.json?
What is the minimum change needed to fix it?

Asking Claude Code to identify the minimum change avoids over-engineering the fix. A missing NSCameraUsageDescription in Info.plist, an incorrect bundle identifier, or a Podfile deployment target mismatch are the most common causes — Claude Code spots them immediately when given the full log.

TypeScript Compilation Errors After Adding a New Package

After installing react-native-google-mobile-ads, TypeScript shows:
"Cannot find module 'react-native-google-mobile-ads' or its type declarations"
How do I fix this without downgrading TypeScript?

This usually requires adding a @types/ package or adjusting tsconfig.json. Claude Code knows the resolution for most popular Expo-compatible libraries.


Step 7: Preparing App Store Submission Materials

Claude Code accelerates the metadata work that most developers dread.

App Store Description Copy

Write App Store descriptions for:
App name: HabitFlow
Core concept: minimalist daily habit tracker
Key features: habit tracking with color coding, weekly and monthly completion graphs,
  local reminder notifications, dark mode
Target audience: people building positive routines, aged 20–40
Constraints: English max 4,000 characters, Japanese max 800 characters
ASO goal: rank for "habit tracker", "daily habits", "routine builder"
Include a keyword section (hidden field, comma-separated, max 100 characters).

Screenshot Captions (for Framer/Fastlane)

Write screenshot captions for five screens.
Format: short headline (max 30 characters) + supporting line (max 60 characters).
Screens: home dashboard, adding a new habit, completion animation,
weekly stats graph, settings with dark mode.
Tone: motivating, not salesy. Focus on the feeling, not the feature.

Privacy Policy

Generate a privacy policy for HabitFlow.
Data collected: device identifiers via AdMob, crash reports via Expo error reporting.
User data (habit records) is stored locally on device and never transmitted externally.
In-app purchases are processed by Apple / Google — no payment data touches our servers.
Must comply with App Store Review Guidelines, GDPR, and CCPA.
Output as Markdown.

Having Claude Code draft the privacy policy gives you a solid starting point that a lawyer can review — which is far cheaper than having a lawyer write it from scratch.


Step 8: Claude Code Hooks for Mobile Dev Automation

As covered in Claude Code Hooks Complete Guide, hooks run automatically around Claude Code's tool calls. For mobile development, two hooks dramatically improve code quality.

Pre-Commit TypeScript Validation

.claude/hooks/pre-commit.sh:

#!/bin/bash
echo "🔍 Running TypeScript type check..."
npx tsc --noEmit 2>&1
 
if [ $? -ne 0 ]; then
  echo "❌ TypeScript errors found. Resolve them before committing."
  exit 1
fi
echo "✅ Types are clean"

Auto-Lint on File Change

.claude/hooks/post-tool-call.sh:

#!/bin/bash
CHANGED_FILE="$1"
if [[ "$CHANGED_FILE" =~ \.(tsx|ts)$ ]]; then
  npx eslint "$CHANGED_FILE" --fix --quiet 2>/dev/null
  echo "✅ Linted $CHANGED_FILE"
fi

Every file Claude Code touches is automatically linted and type-safe by the time the tool call completes. This eliminates the accumulation of lint debt that typically builds up during rapid generation sessions.


Looking back

Building cross-platform apps with Claude Code and Expo is one of the most effective workflows available to indie developers in 2026. The improvements compound: a strong CLAUDE.md means better generated code from day one; better generated code means less time debugging; less time debugging means more features per sprint; more features means a better app on the store.

A few principles that separate productive Claude Code sessions from frustrating ones:

Write CLAUDE.md before writing code. The fifteen minutes you invest in describing your stack, conventions, and constraints is returned many times over in generated code that compiles and integrates cleanly.

Prompt in focused, incremental steps. "Build the home screen" is too broad. "Build the habit card component that shows the name, today's completion state, and a streak count" gives Claude Code the scope it needs to produce something excellent. Build the screen component by component, and the whole will be greater than the sum of its parts.

Paste complete error messages. A snippet of an error leads to a guess. The full stack trace, the file content, and the exact action that triggered the error give Claude Code everything it needs to diagnose accurately.

Use hooks to automate quality gates. TypeScript checks and ESLint running automatically around every Claude Code tool call mean your codebase stays clean even during fast-paced generation sessions.

A concrete next step: drop a CLAUDE.md file in the root of an existing Expo project and ask Claude Code "based on this project's README, build one new screen component." A single round trip will tell you whether the workflow fits how you work.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-05-06
react-native-permissions vs Expo Permissions API — Claude Code
Should you use react-native-permissions or the Expo permissions API? This guide clarifies the decision criteria and shows practical iOS/Android implementation patterns using Claude Code, including common pitfalls and Privacy Manifest requirements.
Claude Code2026-06-19
An Article My Gate Rejected Got Published — The Cost of Chaining the Quality Gate and git push in One Call
In an unattended publishing pipeline, an article my quality gate had rejected went live anyway. The cause was chaining the gate and git push into a single shell call. Here is how the exit code gets swallowed, and a two-phase publish-marker design that refuses to push until every gate has demonstrably passed.
Claude Code2026-06-17
When an Announced Billing Change Is Withdrawn at the Last Minute, Change No Code
A billing change that was supposed to take effect was withdrawn on the day. To survive announce, apply, and revert without touching code, I keep platform behavior behind a single flag and project the monthly delta from real logs.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →