Closing the React Native + Claude Code Workflow Gaps: Metro Logs, Screen Awareness, and cmux

Closing the React Native + Claude Code Workflow Gaps

Note: This post describes a setup I designed and installed, but haven't yet battle-tested in a real debugging session or release cycle. The architecture is based on documented tool capabilities and community patterns. I'll update with lessons learned once I put it through real work.

I've been using Claude Code for React Native development on my Expo project (Odak, a Pomodoro-style focus app). The setup was decent — ios-simulator MCP for screenshots and taps, cmux for notifications, RTK for token savings — but three things were missing:

  1. Metro log awareness — Claude had no idea what Metro was saying. Build errors, console logs, network requests — all invisible unless I manually copy-pasted.
  2. Screen awareness — The ios-simulator MCP gives you screenshot and ui_describe_all, but Claude only sees the screen when you explicitly ask. No proactive awareness of what's on screen after a code change.
  3. cmux + simulator integration — cmux gives web developers a browser pane with DevTools. Mobile developers get nothing. The simulator runs as a separate window with no cmux visibility.

Here's the setup I built to close these gaps.


Gap 1: Metro Log Awareness with metro-mcp

metro-mcp connects to Metro's Hermes runtime via Chrome DevTools Protocol (CDP). It auto-discovers Metro on ports 8081, 8082, and 19000-19002. No app code changes needed.

Setup

cd ~/Repos/ios/odak
claude mcp add --scope project metro-mcp -- bunx metro-mcp

This gives Claude access to ~90 tools including:

  • get_logs — Buffered console logs (500 entries) with filtering
  • get_bundle_errors — Metro compilation errors, auto-detected
  • get_network_requests — Full network tracking with timing and headers
  • execute_js — JavaScript REPL in the running app (inspect state, test fixes live)
  • open_devtools — Opens Chrome DevTools without stealing the CDP connection
  • Redux/Apollo state inspection, component tree, and performance profiling

The intended workflow: Claude checks get_bundle_errors before investigating UI bugs, since many apparent UI issues are actually build failures.

Critical Caveats

metro-mcp runs a CDP proxy (port 9222 by default) that multiplexes connections so Chrome DevTools and MCP can work simultaneously. But there are two things that will break it:

  • Never press "j" in the Metro terminal — it opens the debugger and steals the single CDP connection from metro-mcp
  • Never use "Open Debugger" from the dev menu — same problem. Use the open_devtools MCP tool instead

If metro-mcp disconnects, restart Metro and the Claude Code session.


Gap 2: Proactive Screen Awareness via Hooks

The ios-simulator MCP has screenshot, ui_tap, ui_describe_all, etc. — but it's reactive. Claude doesn't see the screen unless you ask. I wanted Claude to automatically know what's on screen after every code change.

Screenshot-on-Change Hook

I added a PostToolUse hook in .claude/settings.json that captures the simulator screen after any .ts/.tsx file edit:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$TOOL_INPUT\" | grep -qE '\\.(tsx?|jsx?)'; then xcrun simctl io booted screenshot /tmp/claude-sim-latest.png 2>/dev/null; fi"
          }
        ]
      }
    ]
  }
}

After every code edit, /tmp/claude-sim-latest.png contains the current simulator screen. Claude can read it when investigating UI issues without a manual screenshot request.

CLAUDE.md Workflow Instructions

I also updated the project's CLAUDE.md to document the debugging priority:

### Metro MCP Debugging Workflow
- Use `get_logs` to check console output when investigating issues
- Use `get_bundle_errors` BEFORE investigating UI bugs (might be a build error)
- Use `execute_js` to inspect runtime state (variables, Redux store)
- After code edits, check /tmp/claude-sim-latest.png for current UI state

The goal: Claude follows the same flow a human developer would — check if it compiles, check the logs, look at the screen — instead of jumping straight to expensive operations like screenshots or deep code inspection.


Gap 3: Deep cmux Integration

The initial setup just used cmux notify for Metro errors. But cmux has a much richer API — status pills, sidebar logs, progress bars, and notifications. Here's the full integration.

cmux API Primitives

| Primitive | Command | Purpose | |---|---|---| | Status pills | cmux set-status <key> <value> --icon <name> --color <hex> | Always-visible sidebar indicators | | Sidebar logs | cmux log "<msg>" --level <level> --source <src> | Scrollable persistent error feed | | Progress bars | cmux set-progress <0.0-1.0> --label "<text>" | Visual progress tracking | | Notifications | cmux notify --title "<title>" --body "<body>" | Desktop alerts for critical events |

Status Pills Configuration

Three persistent sidebar indicators:

| Pill | Shows | Updated when | |---|---|---| | metro | Connected / Error / OK | Any metro-mcp tool call | | screen | Updated | Code edit to .ts/.tsx file | | test | Running / Passed / Failed | Autonomous test flow execution |

Hook Configuration

The full .claude/settings.json hooks:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "if echo \"$TOOL_INPUT\" | grep -qE '\\.(tsx?|jsx?)'; then xcrun simctl io booted screenshot /tmp/claude-sim-latest.png 2>/dev/null && cmux set-status screen \"Updated\" --icon iphone --color \"#3498db\" 2>/dev/null; fi"
        }]
      },
      {
        "matcher": "mcp__metro-mcp__get_bundle_errors",
        "hooks": [{
          "type": "command",
          "command": "if echo \"$TOOL_OUTPUT\" | grep -qiE 'error|failed'; then cmux log \"Bundle error detected\" --level error --source metro 2>/dev/null && cmux set-status metro \"Error\" --icon exclamationmark.circle --color \"#ff0000\" 2>/dev/null && cmux notify --title \"Metro\" --body \"Bundle error detected\" 2>/dev/null; else cmux set-status metro \"OK\" --icon bolt.fill --color \"#00ff00\" 2>/dev/null; fi"
        }]
      },
      {
        "matcher": "mcp__metro-mcp__get_logs",
        "hooks": [{
          "type": "command",
          "command": "cmux set-status metro \"Connected\" --icon bolt.fill --color \"#00ff00\" 2>/dev/null"
        }]
      },
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": "if echo \"$TOOL_INPUT\" | grep -qE 'screen_mapper|navigator|accessibility_audit|test_recorder'; then cmux set-status test \"Running\" --icon hammer --color \"#f39c12\" 2>/dev/null; fi"
        }]
      }
    ],
    "Notification": [{
      "hooks": [{
        "type": "command",
        "command": "cmux notify --title \"$CLAUDE_NOTIFICATION_TITLE\" --body \"$CLAUDE_NOTIFICATION_BODY\" 2>/dev/null"
      }]
    }]
  }
}

Key design decisions:

  • All cmux commands have 2>/dev/null so they fail silently outside cmux
  • metro pill goes green on get_logs (proves connection works), red on errors
  • screen pill updates on code edits (proves screenshot was captured)
  • test pill tracks autonomous test execution (triggered when skill scripts run)
  • Triple-action on errors: sidebar log (persistent) + status pill (visible) + notification (alert)

Gap 4: Autonomous Testing with ios-simulator-skill

Inspired by a viral tweet by @0x__tom (101K views) showing Claude Code autonomously testing an iOS app via the accessibility tree, I added the ios-simulator-skill — 21 Python scripts for semantic UI navigation.

Installation

git clone https://github.com/conorluddy/ios-simulator-skill.git \
  .claude/skills/ios-simulator-skill

Why Semantic Navigation

Per the skill's docs, the accessibility tree costs 10-50 tokens per screen. A screenshot costs 1,600-6,300 tokens. Semantic navigation (find by text/type/ID) should also survive layout changes — the same test should work on iPhone SE and iPad Pro.

Navigation priority (from the skill's SKILL.md):

  1. screen_mapper.py → structured element list (~10 tokens)
  2. navigator.py --find-text "Button" --tap → semantic interaction
  3. Screenshots → only for visual verification or bug reports

Autonomous Test Flows in CLAUDE.md

I defined test flows in CLAUDE.md that Claude should be able to execute end-to-end. Each flow has preconditions, steps using skill scripts, and assertions. The flows are derived from the manual QA checklist in migration-docs/QA.md.

Example — the full app crawl:

# Launch app and crawl every tab
python scripts/app_launcher.py --launch com.omc345.odak

for tab in Focus Dates Bank You; do
  cmux set-progress ... --label "Crawling: $tab"
  python scripts/navigator.py --find-text "$tab" --tap
  python scripts/screen_mapper.py --json
  python scripts/accessibility_audit.py
  xcrun simctl io booted screenshot "/tmp/claude-crawl-${tab}.png"
done

cmux set-status test "Passed" --icon checkmark --color "#00ff00"
cmux log "Full crawl complete" --level success --source test

In the @0x__tom demo, Claude crawls every screen, runs accessibility audits, captures screenshots, and compiles a bug report in about 8 minutes. The skill + flow definitions here are set up to enable the same pattern — whether it'll actually produce similar results on a real app is something I'll find out.


The Full Setup

| Tool | Purpose | |------|--------| | metro-mcp | Metro logs, bundle errors, JS REPL, state inspection via CDP | | ios-simulator MCP | Screenshots, UI taps, accessibility tree | | ios-simulator-skill | Semantic navigation, WCAG audit, test recording (21 scripts) | | PostToolUse screenshot hook | Auto-capture simulator after code edits | | cmux status pills | Metro/screen/test state always visible in sidebar | | cmux sidebar logs | Persistent scrollable Metro error feed | | cmux progress bar | Visual test execution tracking | | cmux notifications | Desktop alerts for errors and completion | | RTK | Token-optimized CLI proxy (60-90% savings) | | react-native-best-practices skill | Callstack's RN performance guidelines |


What's Still Missing

  1. Continuous Metro log streaming — metro-mcp buffers logs, but doesn't push them. A background script that watches localhost:8081/logs and pipes to cmux sidebar would close this gap.
  2. Simulator embedding in cmux — Waiting on cmux surface plugin API documentation. The WebKit browser pane is first-party, no plugin equivalent exists.
  3. Hot reload awareness — Claude doesn't know when Metro finishes a hot reload. A hook that detects reload completion and takes a screenshot would make the feedback loop tighter.
  4. Actual validation — Every claim about how this improves the workflow is a hypothesis until I run a real debugging session or ship a release through it.

The design aims at parity with the web workflow in cmux (browser pane, DevTools, instant visual feedback). Whether it gets there is an open question I'll revisit after putting it through real work.

← All notes