DEV Community

Cover image for Claude Code + Expo: The Repo Config That Makes It Write Decent React Native
Chris F A
Chris F A

Posted on Originally published at applighter.hashnode.dev

Claude Code + Expo: The Repo Config That Makes It Write Decent React Native

TL;DR

  • Claude Code forgets your project every session. Write the facts down in CLAUDE.md: stack, commands, and do/don't rules like "use npx expo install".
  • Allowlist routine commands in .claude/settings.json, put risky ones behind ask or deny.
  • A PostToolUse hook runs ESLint and tsc after every edit and feeds errors back to Claude with exit 2.
  • Skills (.claude/skills/<name>/SKILL.md) turn "add a screen" into a repeatable /add-screen command.
  • Still read the diff and run it on a phone.

CLAUDE.md: keep it short and factual

CLAUDE.md in the repo root loads into every session. /init will draft one; then cut it down. The docs suggest staying under 200 lines. Here's a trimmed version for an Expo app:

## Stack
- Expo SDK 54, TypeScript (strict), Expo Router, NativeWind, Supabase

## Commands
- Dev: `npx expo start`
- Typecheck: `npx tsc --noEmit`
- Lint: `npx expo lint`
- Dep check: `npx expo install --check`

## Rules
- Add packages with `npx expo install <pkg>`, never npm/yarn add.
- Every file in app/ is a route. Components go in components/.
- Style with NativeWind className, not StyleSheet.create.
- Navigate with expo-router `Link` / `router`.
- Never put secrets in EXPO_PUBLIC_ vars. They're inlined into the bundle.
- Don't hand-edit ios/ or android/.
Enter fullscreen mode Exit fullscreen mode

Why these lines:

  • npx expo install picks versions that match your SDK. Plain npm install doesn't.
  • Expo Router treats anything in the routes folder as a route, so a stray utils.ts in app/ becomes a broken screen. (Newer Expo docs use src/app/. Write what your repo uses.)
  • Expo's own docs say EXPO_PUBLIC_ values end up in plain text in your compiled app.

Got an AGENTS.md already? Put @AGENTS.md at the top of CLAUDE.md to import it instead of duplicating.

For rules that only matter in one folder, use .claude/rules/ with a paths field. They only load when Claude touches matching files:

---
paths:
  - "supabase/**/*"
---
- New tables get RLS enabled in the same migration.
- Never edit an applied migration.
Enter fullscreen mode Exit fullscreen mode

Permissions: stop clicking "yes" forty times

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(npx expo install *)",
      "Bash(npx expo lint *)",
      "Bash(npx tsc --noEmit *)",
      "Bash(npx eslint *)"
    ],
    "ask": ["Bash(git push *)", "Bash(npx expo prebuild *)", "Bash(eas *)"],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git reset --hard *)",
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Quick gotchas:

  • Rules are checked deny, then ask, then allow. Deny always wins.
  • Bash(npx tsc --noEmit *) also matches the bare npx tsc --noEmit.
  • cmd && other gets split, so an allow on cmd doesn't sneak other through.
  • I skip Bash(npx expo *). It would approve prebuild --clean too.
  • Bash deny rules don't catch /bin/rm or bash -c. They prevent accidents, they're not a sandbox.

The hook that does the most work

CLAUDE.md is a suggestion. Hooks always run. Add this to the same settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-ts.sh" }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

.claude/hooks/check-ts.sh (needs jq, and chmod +x):

#!/usr/bin/env bash
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in *.ts|*.tsx) ;; *) exit 0 ;; esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
errors=""
out=$(npx eslint "$FILE" 2>&1) || errors+="$out"$'\n'
out=$(npx tsc --noEmit --pretty false 2>&1) || errors+="$out"
if [ -n "$errors" ]; then echo "$errors" >&2; exit 2; fi
Enter fullscreen mode Exit fullscreen mode

For PostToolUse, exit 2 doesn't undo the edit. It sends stderr to Claude, so it sees the type errors and fixes them right away. Check it registered with /hooks. If whole-project tsc gets slow, move it to a Stop hook.

Commands and skills

Custom commands were merged into skills. .claude/commands/check.md and .claude/skills/check/SKILL.md both give you /check. I use skills for anything multi-step:

---
name: add-screen
description: Add an Expo Router screen with NativeWind styling and navigation
argument-hint: [route-path] [what it does]
disable-model-invocation: true
allowed-tools: Bash(npx tsc --noEmit *) Bash(npx expo lint *)
---

Add a screen at app/$0.tsx. Purpose: $ARGUMENTS

1. Read app/_layout.tsx and a nearby screen to match patterns.
2. For dynamic segments like [id], use useLocalSearchParams with typed params.
3. Reusable UI goes in components/. NativeWind className only.
4. Link to it from the screen that should open it.
5. Run typecheck and lint, fix what you introduced, list changed files.
Enter fullscreen mode Exit fullscreen mode
  • disable-model-invocation: true means only you can trigger it.
  • allowed-tools pre-approves those commands while it runs.
  • $0 is the first argument, $ARGUMENTS is everything.

Then:

/add-screen orders/[id] "order detail with status and a cancel button"
Enter fullscreen mode Exit fullscreen mode

For bigger changes, ask for a plan first. Plan mode is where you catch "I'll just add React Navigation" before it touches your files.

Don't want to write all this?

Fair. I work on AppLighter: Expo SDK 54 templates with TypeScript, Expo Router, NativeWind and Supabase (RLS on every table), shipped with CLAUDE.md, AGENTS.md, .cursorrules, and skills/commands like /add-screen, /swap-backend, /audit-security and /ship. The files above work fine on their own too.

Review before you trust it

  • git diff everything. Look for out-of-scope files, new deps, non-route files in app/.
  • npx expo install --check for SDK version mismatches.
  • Grep new EXPO_PUBLIC_ vars. Assume they're public, because they are.
  • Run it on a real phone. Back nav, keyboard, safe areas. Typecheck can't see those.

If Claude gets the same thing wrong twice, that's a missing line in CLAUDE.md.

What's in your React Native CLAUDE.md that I didn't cover? Drop your best rule in the comments.

Top comments (0)