<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahsan Ali</title>
    <description>The latest articles on DEV Community by Ahsan Ali (@ahsanalidotme).</description>
    <link>https://dev.to/ahsanalidotme</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160688%2F7fed2be1-207b-4a94-b95e-7444172feffa.jpg</url>
      <title>DEV Community: Ahsan Ali</title>
      <link>https://dev.to/ahsanalidotme</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahsanalidotme"/>
    <language>en</language>
    <item>
      <title>How Anthropic reviews code when Claude writes 90% of it</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/how-anthropic-reviews-code-when-claude-writes-90-of-it-38ge</link>
      <guid>https://dev.to/ahsanalidotme/how-anthropic-reviews-code-when-claude-writes-90-of-it-38ge</guid>
      <description>&lt;p&gt;Claude writes ~90% of Claude Code's own code. So how does Anthropic review it? Not by having a human read every line.&lt;br&gt;
Their team published their setup, and one idea stuck with me: "did a human read it?" is the wrong safety check. The real question is what could break, and which check should catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated checks first
&lt;/h2&gt;

&lt;p&gt;Every PR goes through automated checks before a human sees it. Claude has to prove its own work with tests and screenshots. Several AI reviewers hunt for bugs and vote, and only the agreed findings get posted. Security issues block the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Humans scaled to risk
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low risk&lt;/strong&gt; (docs, copy, flagged additive changes): zero human approvals- &lt;strong&gt;Medium&lt;/strong&gt; (normal product changes): one approval- &lt;strong&gt;High&lt;/strong&gt; (billing, auto-updater, CI workflows): two approvalsA classifier sets the tier from the change, not the author. And an agent's approval never counts as a person's.
## Results
Merged PRs doubled in three months. Reverts dropped 40%. PRs passing review in one round went from 65% to 78%.
## The bit I'm stealing
Every past incident becomes an eval for the AI reviewer. If a prompt change doesn't catch more old bugs, it doesn't ship.
I run something similar with my own agents, and the humans don't go away. They just stop reading lint output and start making the calls machines can't.
Source: the Claude Code team's post on claude.dev, October 2026.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>codereview</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Mobile E2E testing three ways: Detox, Maestro, and now plain-English agents</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Sat, 24 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/mobile-e2e-testing-three-ways-detox-maestro-and-now-plain-english-agents-2b4c</link>
      <guid>https://dev.to/ahsanalidotme/mobile-e2e-testing-three-ways-detox-maestro-and-now-plain-english-agents-2b4c</guid>
      <description>&lt;p&gt;I've written mobile E2E tests three ways now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detox
&lt;/h2&gt;

&lt;p&gt;Painful. Flaky tests, long setup, and every small UI change broke something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maestro
&lt;/h2&gt;

&lt;p&gt;Made it easy. Simple YAML, and it just worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now: e2e by TesterArmy
&lt;/h2&gt;

&lt;p&gt;You write the test in plain English, like "upgrade the workspace to the Pro plan," and an agent drives the app to do it.&lt;br&gt;
The clever part: once a step passes, it records what the agent did. After that it just replays those actions, with no AI call, until the app changes. So it doesn't cost money on every run, and you don't get a different result each time.&lt;br&gt;
It works on iOS and Android simulators, and there's an Expo example.&lt;/p&gt;

&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;It's still pre-1.0, so I wouldn't move a critical suite onto it yet. But it's interesting to see where testing is heading.&lt;br&gt;
What are you using for mobile E2E right now?&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>testing</category>
      <category>mobile</category>
      <category>ai</category>
    </item>
    <item>
      <title>GitHub stacked PRs are the review tool agentic development needed</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/github-stacked-prs-are-the-review-tool-agentic-development-needed-8a</link>
      <guid>https://dev.to/ahsanalidotme/github-stacked-prs-are-the-review-tool-agentic-development-needed-8a</guid>
      <description>&lt;p&gt;Agents can write a 2,000-line PR in ten minutes. Nobody can review it in ten minutes.&lt;br&gt;
That's the real bottleneck in agentic development. Not writing code. Reviewing it.&lt;br&gt;
GitHub just made stacked PRs generally available, and I think they matter more for agents than for humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  One feature, four layers
&lt;/h2&gt;

&lt;p&gt;Instead of one giant PR, the agent splits the feature into layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#101&lt;/strong&gt; DB schema- &lt;strong&gt;#102&lt;/strong&gt; API (built on #101)- &lt;strong&gt;#103&lt;/strong&gt; UI (built on #102)- &lt;strong&gt;#104&lt;/strong&gt; Tests (built on #103)Each PR targets the one below it. Each one is small enough to actually read.
## Two ways to merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge the bottom one.&lt;/strong&gt; The next PR automatically retargets to main. Repeat.- &lt;strong&gt;Merge the top one.&lt;/strong&gt; Every approved layer below it lands in one click.## Let the agent do the splitting
GitHub ships an agent skill: &lt;strong&gt;gh skill install github/gh-stack&lt;/strong&gt;. With it, the agent can turn a big diff into a stack, or plan the stack before writing a line.
## What changes&lt;/li&gt;
&lt;li&gt;Reviews you can do properly, not skim- Feedback on layer 1 while the agent builds layer 3- Impact assessment per layer, not per featureOne caution: check your merge strategy first. Squash merges can fight with stacks.
&lt;strong&gt;Agents made writing code cheap. Stacks make reviewing it possible.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
    </item>
    <item>
      <title>iOS SceneDelegate and React Native: what changed and how to fix it</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:39:52 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/ios-scenedelegate-and-react-native-what-changed-and-how-to-fix-it-lia</link>
      <guid>https://dev.to/ahsanalidotme/ios-scenedelegate-and-react-native-what-changed-and-how-to-fix-it-lia</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Any UIKit app built with the iOS 27 SDK (Xcode 27) that hasn't adopted the UIScene lifecycle won't launch. React Native apps are UIKit apps, so every RN app is affected, whatever the RN version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deadline:&lt;/strong&gt; the day you build with Xcode 27. Binaries built with Xcode 26 keep launching on iOS 27; Apple says it strives for binary compatibility but won't guarantee zero issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RN 0.88+:&lt;/strong&gt; move startup into a &lt;code&gt;SceneDelegate&lt;/code&gt; using the new &lt;code&gt;startReactNative(withModuleName:in:connectionOptions:)&lt;/code&gt;, forward links to &lt;code&gt;RCTLinkingManager&lt;/code&gt;, add &lt;code&gt;UIApplicationSceneManifest&lt;/code&gt; to &lt;code&gt;Info.plist&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RN 0.79–0.87:&lt;/strong&gt; same shape, but you build the root view yourself from &lt;code&gt;reactNativeFactory.rootViewFactory&lt;/code&gt; and handle cold-start links by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RN 0.74–0.78:&lt;/strong&gt; keep your &lt;code&gt;RCTAppDelegate&lt;/code&gt; subclass, but build the window in a &lt;code&gt;SceneDelegate&lt;/code&gt; from its &lt;code&gt;rootViewFactory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RN below 0.74:&lt;/strong&gt; create the root view in &lt;code&gt;SceneDelegate&lt;/code&gt; from the &lt;code&gt;RCTBridge&lt;/code&gt; the AppDelegate owns, with &lt;code&gt;RCTRootView(bridge:moduleName:initialProperties:)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch:&lt;/strong&gt; deep links, Universal Links, push-tap routing and third-party SDKs that hook &lt;code&gt;AppDelegate&lt;/code&gt; callbacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before: one app, one window, one AppDelegate
&lt;/h2&gt;

&lt;p&gt;Until iOS 13, the &lt;code&gt;UIApplicationDelegate&lt;/code&gt; (your &lt;code&gt;AppDelegate&lt;/code&gt;) owned everything: process launch, the single &lt;code&gt;UIWindow&lt;/code&gt;, foreground/background state, deep links and push.&lt;/p&gt;

&lt;p&gt;A typical flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;UIKit calls &lt;code&gt;application(_:didFinishLaunchingWithOptions:)&lt;/code&gt; with a &lt;code&gt;launchOptions&lt;/code&gt; dictionary. If a URL or Universal Link opened the app, it's inside that dictionary.&lt;/li&gt;
&lt;li&gt;The AppDelegate creates &lt;code&gt;UIWindow(frame: UIScreen.main.bounds)&lt;/code&gt;, sets a root view controller and calls &lt;code&gt;makeKeyAndVisible()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;State changes arrive as &lt;code&gt;applicationDidBecomeActive&lt;/code&gt;, &lt;code&gt;applicationWillResignActive&lt;/code&gt;, &lt;code&gt;applicationDidEnterBackground&lt;/code&gt; and &lt;code&gt;applicationWillEnterForeground&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Warm deep links arrive in &lt;code&gt;application(_:open:options:)&lt;/code&gt; and &lt;code&gt;application(_:continue:restorationHandler:)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This model assumes the app &lt;em&gt;is&lt;/em&gt; its window. Any code can reach the UI through &lt;code&gt;UIApplication.shared.delegate.window&lt;/code&gt; or &lt;code&gt;keyWindow&lt;/code&gt;, and lots of libraries do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now: AppDelegate for the process, SceneDelegate for the UI
&lt;/h2&gt;

&lt;p&gt;A scene (&lt;code&gt;UIWindowScene&lt;/code&gt;) is one instance of your app's UI. iOS 13 split responsibilities in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AppDelegate&lt;/strong&gt; keeps process-level work: SDK init, push token registration, &lt;code&gt;UNUserNotificationCenter&lt;/code&gt; delegate, background tasks, and choosing a scene configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SceneDelegate&lt;/strong&gt; (&lt;code&gt;UIWindowSceneDelegate&lt;/code&gt;) owns a window and its lifecycle: create the window, go foreground/background, receive URLs and user activities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key behaviour change: once &lt;code&gt;UIApplicationSceneManifest&lt;/code&gt; is in &lt;code&gt;Info.plist&lt;/code&gt;, UIKit stops calling the AppDelegate's UI-lifecycle and deep-link methods and calls the scene equivalents instead.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AppDelegate (old)&lt;/th&gt;
&lt;th&gt;SceneDelegate (new)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create window in &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scene(_:willConnectTo:options:)&lt;/code&gt;, using &lt;code&gt;UIWindow(windowScene:)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;launchOptions[.url]&lt;/code&gt; (cold-start link)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connectionOptions.urlContexts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;launchOptions[.userActivityDictionary]&lt;/code&gt; (cold-start Universal Link)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connectionOptions.userActivities&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;launchOptions[.shortcutItem]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;connectionOptions.shortcutItem&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application(_:open:options:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scene(_:openURLContexts:)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application(_:continue:restorationHandler:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scene(_:continue:)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application(_:performActionFor:completionHandler:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;windowScene(_:performActionFor:completionHandler:)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;applicationDidBecomeActive&lt;/code&gt; / &lt;code&gt;WillResignActive&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sceneDidBecomeActive&lt;/code&gt; / &lt;code&gt;sceneWillResignActive&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;applicationDidEnterBackground&lt;/code&gt; / &lt;code&gt;WillEnterForeground&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sceneDidEnterBackground&lt;/code&gt; / &lt;code&gt;sceneWillEnterForeground&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unchanged on AppDelegate: &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; (minus window work), &lt;code&gt;didRegisterForRemoteNotificationsWithDeviceToken&lt;/code&gt;, notification delegate methods, background fetch and &lt;code&gt;supportedInterfaceOrientationsFor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Apple changed it, and when it bites
&lt;/h2&gt;

&lt;p&gt;Apple's UI no longer fits "one app = one full-screen window". iPad multi-window, Stage Manager, resizable windows, external displays, Mac Catalyst and visionOS all need an app to show several independent UIs, each with its own state. A scene gives each window its own lifecycle and restoration state, which a single AppDelegate can't express. Apple has been deprecating the window-centric APIs (&lt;code&gt;keyWindow&lt;/code&gt;, &lt;code&gt;UIScreen.main&lt;/code&gt;-sized windows) in the same direction.&lt;/p&gt;

&lt;p&gt;Apple rolled this out gradually (&lt;a href="https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle" rel="noopener noreferrer"&gt;TN3187&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;th&gt;What happens to apps without UIScene&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;iOS 13 (2019)&lt;/td&gt;
&lt;td&gt;Scenes introduced; optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS 18.4&lt;/td&gt;
&lt;td&gt;Console log: the process doesn't adopt the UIScene lifecycle and this will become an assert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS 26&lt;/td&gt;
&lt;td&gt;Log hardened: UIScene lifecycle will soon be required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS 27 SDK (Xcode 27)&lt;/td&gt;
&lt;td&gt;Assert at launch. The app doesn't launch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details from Apple engineers on the &lt;a href="https://developer.apple.com/forums/thread/789004" rel="noopener noreferrer"&gt;developer forums&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The assert is only evaluated when you build with the new SDK. An Xcode 26 build still runs on iOS 27, though Apple doesn't guarantee zero issues (&lt;a href="https://developer.apple.com/forums/thread/813300" rel="noopener noreferrer"&gt;thread&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Only adopting the scene lifecycle is required. Supporting multiple scenes is encouraged, not mandatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, App Store Connect will eventually require the latest SDK for submissions, so "stay on Xcode 26" only buys time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How React Native booted on iOS before 0.88
&lt;/h2&gt;

&lt;p&gt;Every RN version up to 0.87 starts React Native from &lt;code&gt;didFinishLaunchingWithOptions&lt;/code&gt; and creates the window itself from &lt;code&gt;UIScreen.main.bounds&lt;/code&gt;. Only the wrapper changed over time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RN version&lt;/th&gt;
&lt;th&gt;Entry point&lt;/th&gt;
&lt;th&gt;What it does at launch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≤ 0.70&lt;/td&gt;
&lt;td&gt;Hand-written &lt;code&gt;AppDelegate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Creates &lt;code&gt;RCTBridge(delegate:launchOptions:)&lt;/code&gt;, then &lt;code&gt;RCTRootView(bridge:moduleName:initialProperties:)&lt;/code&gt;, puts it in a &lt;code&gt;UIViewController&lt;/code&gt;, creates the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.71–0.73&lt;/td&gt;
&lt;td&gt;Subclass of &lt;code&gt;RCTAppDelegate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same work hidden inside the superclass's &lt;code&gt;didFinishLaunching&lt;/code&gt;; you set &lt;code&gt;moduleName&lt;/code&gt; and &lt;code&gt;initialProps&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.74–0.78&lt;/td&gt;
&lt;td&gt;Subclass of &lt;code&gt;RCTAppDelegate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Root view creation moves into &lt;code&gt;RCTRootViewFactory&lt;/code&gt; (&lt;code&gt;self.rootViewFactory&lt;/code&gt;), which also handles bridgeless/New Architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.79–0.87&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RCTReactNativeFactory&lt;/code&gt; + &lt;code&gt;RCTDefaultReactNativeFactoryDelegate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;factory.startReactNative(withModuleName:in:launchOptions:)&lt;/code&gt; on a window built from &lt;code&gt;UIScreen.main.bounds&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SceneDelegate&lt;/code&gt; + &lt;code&gt;RCTReactNativeFactory&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;startReactNative(withModuleName:in:connectionOptions:)&lt;/code&gt; inside &lt;code&gt;scene(_:willConnectTo:options:)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 0.87 template shows the pattern every older version shares: the window and React Native both live on the AppDelegate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RN 0.87 template AppDelegate (abridged)&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ReactNativeDelegate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTReactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dependencyProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTAppDependencyProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;reactNativeDelegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;
  &lt;span class="n"&gt;reactNativeFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;

  &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScreen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startReactNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withModuleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"HelloWorld"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the JS side, three modules depend on this native plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Linking.getInitialURL()&lt;/code&gt; reads the cold-start URL out of the &lt;code&gt;launchOptions&lt;/code&gt; handed to the bridge/host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Linking.addEventListener('url')&lt;/code&gt; fires when &lt;code&gt;RCTLinkingManager&lt;/code&gt; receives a forwarded &lt;code&gt;open URL&lt;/code&gt; or &lt;code&gt;continue userActivity&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AppState&lt;/code&gt; listens for &lt;code&gt;UIApplication&lt;/code&gt; active/background notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What breaks in a React Native app
&lt;/h2&gt;

&lt;p&gt;If you do nothing and build with Xcode 27, the app asserts on launch. Adding only the scene manifest without moving code gives you subtler bugs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crash on launch (Xcode 27)&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;UIApplicationSceneManifest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blank or white screen&lt;/td&gt;
&lt;td&gt;Manifest present, but the window is still created in AppDelegate. A scene-based app needs a window attached to a &lt;code&gt;UIWindowScene&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Linking.getInitialURL()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The cold-start URL moved from &lt;code&gt;launchOptions&lt;/code&gt; to &lt;code&gt;connectionOptions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm deep links and Universal Links do nothing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;application(_:open:)&lt;/code&gt; and &lt;code&gt;continue userActivity&lt;/code&gt; on AppDelegate are no longer called&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home-screen quick actions ignored&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;performActionFor&lt;/code&gt; moved to the window scene&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Libraries can't find the window&lt;/td&gt;
&lt;td&gt;They read &lt;code&gt;UIApplication.shared.delegate.window&lt;/code&gt;, which is now &lt;code&gt;nil&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attribution/analytics SDKs lose deep-link events&lt;/td&gt;
&lt;td&gt;Their AppDelegate hooks or swizzles stop firing (Branch, AppsFlyer, Adjust, Airbridge, Firebase Dynamic Links-style SDKs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layout surprises on iPad&lt;/td&gt;
&lt;td&gt;The window becomes resizable; &lt;code&gt;Dimensions&lt;/code&gt; and &lt;code&gt;useWindowDimensions&lt;/code&gt; now change at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Attribution vendors are already publishing scene migration guides for this. &lt;a href="https://help.airbridge.io/en/developers/troubleshooting-deprecated-react-native-sdk-v2" rel="noopener noreferrer"&gt;Airbridge's guide&lt;/a&gt;, for example, lists a blank screen and missing deep-link events as the two headline symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix A: upgrade to React Native 0.88
&lt;/h2&gt;

&lt;p&gt;RN 0.88 adds scene support as an additive change: &lt;code&gt;RCTReactNativeFactory&lt;/code&gt; gains &lt;code&gt;startReactNativeWithModuleName:inWindow:connectionOptions:&lt;/code&gt; and a variant with &lt;code&gt;initialProperties:&lt;/code&gt;. The old AppDelegate path still works under Xcode 26, so you can upgrade first and migrate second (&lt;a href="https://reactnative.dev/blog/2026/10/19/react-native-0.88" rel="noopener noreferrer"&gt;RN 0.88 release notes&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add &lt;code&gt;SceneDelegate.swift&lt;/code&gt;.&lt;/strong&gt; This is the &lt;a href="https://github.com/react-native-community/template/blob/0.88-stable/template/ios/HelloWorld/SceneDelegate.swift" rel="noopener noreferrer"&gt;0.88 template file&lt;/a&gt;; replace &lt;code&gt;HelloWorld&lt;/code&gt; with your module name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;React&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;React_RCTAppDelegate&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;ReactAppDependencyProvider&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;UIKit&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SceneDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RCTDefaultReactNativeFactoryDelegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UIWindowSceneDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;reactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RCTReactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;willConnectTo&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UISceneSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="nv"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ConnectionOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;windowScene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;UIWindowScene&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;dependencyProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTAppDependencyProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;reactNativeFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTReactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
    &lt;span class="n"&gt;reactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;devMenuConfiguration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTDevMenuConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;devMenuEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;shakeGestureEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;keyboardShortcutsEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#endif&lt;/span&gt;

    &lt;span class="n"&gt;reactNativeFactory&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startReactNative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;withModuleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"HelloWorld"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openURLContexts&lt;/span&gt; &lt;span class="kt"&gt;URLContexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;UIOpenURLContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;RCTLinkingManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;openURLContexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URLContexts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="nv"&gt;userActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSUserActivity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;RCTLinkingManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userActivity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;bundleURL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
    &lt;span class="kt"&gt;RCTBundleURLProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sharedSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsBundleURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forBundleRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#else&lt;/span&gt;
    &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"jsbundle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#endif&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Slim down &lt;code&gt;AppDelegate&lt;/code&gt;.&lt;/strong&gt; Remove the window, the factory and the RN start call. Keep Firebase, push registration and other process-level setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@main&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AppDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIResponder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UIApplicationDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// FirebaseApp.configure(), push setup, SDK init...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add the scene manifest to &lt;code&gt;Info.plist&lt;/code&gt;.&lt;/strong&gt; Keep &lt;code&gt;UIApplicationSupportsMultipleScenes&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt;: 0.88 supports a single scene only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UIApplicationSceneManifest&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UIApplicationSupportsMultipleScenes&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;false/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UISceneConfigurations&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UIWindowSceneSessionRoleApplication&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UISceneConfigurationName&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Default Configuration&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;UISceneDelegateClassName&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;$(PRODUCT_MODULE_NAME).SceneDelegate&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Move any custom AppDelegate code&lt;/strong&gt; for links, quick actions or lifecycle into the matching scene method (mapping table above).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limits in 0.88:&lt;/strong&gt; only one scene, and at cold start RN forwards only the first URL context or user activity from &lt;code&gt;connectionOptions&lt;/code&gt;. Expo apps get this through Expo SDK 58.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix B: stay on an older React Native version
&lt;/h2&gt;

&lt;p&gt;You can adopt scenes on any RN version. React Native only needs a root &lt;code&gt;UIView&lt;/code&gt;, and UIKit doesn't care who creates it. Without 0.88's helpers, your &lt;code&gt;SceneDelegate&lt;/code&gt; does three jobs by hand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the root view and attach it to a scene-backed window.&lt;/li&gt;
&lt;li&gt;Turn &lt;code&gt;connectionOptions&lt;/code&gt; back into a &lt;code&gt;launchOptions&lt;/code&gt;-shaped dictionary, so &lt;code&gt;Linking.getInitialURL()&lt;/code&gt; keeps working.&lt;/li&gt;
&lt;li&gt;Forward warm URLs and user activities to &lt;code&gt;RCTLinkingManager&lt;/code&gt;'s existing &lt;code&gt;application(...)&lt;/code&gt; class methods.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add the same &lt;code&gt;Info.plist&lt;/code&gt; manifest as in Fix A, then pick the variant for your version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared helper (all versions)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rebuilds the dictionary RCTLinkingManager reads for getInitialURL()&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;rnLaunchOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ConnectionOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlContexts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userActivities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userActivityDictionary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s"&gt;"UIApplicationLaunchOptionsUserActivityKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"UIApplicationLaunchOptionsUserActivityTypeKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activityType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;launch&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  RN 0.79–0.87 (&lt;code&gt;RCTReactNativeFactory&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Keep creating the factory in &lt;code&gt;AppDelegate&lt;/code&gt;, but delete the window and &lt;code&gt;startReactNative&lt;/code&gt; lines. Keep a &lt;code&gt;window&lt;/code&gt; property on &lt;code&gt;AppDelegate&lt;/code&gt; too, because libraries still read &lt;code&gt;delegate.window&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SceneDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIResponder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UIWindowSceneDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;willConnectTo&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UISceneSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="nv"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ConnectionOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;windowScene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;UIWindowScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;appDelegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;AppDelegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appDelegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reactNativeFactory&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;rootView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootViewFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nv"&gt;withModuleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"HelloWorld"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;initialProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rnLaunchOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;vc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIViewController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rootView&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootViewController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vc&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeKeyAndVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;
    &lt;span class="n"&gt;appDelegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="c1"&gt;// for libraries that read delegate.window&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openURLContexts&lt;/span&gt; &lt;span class="kt"&gt;URLContexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;UIOpenURLContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLContexts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;RCTLinkingManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[:])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="nv"&gt;userActivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSUserActivity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;RCTLinkingManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userActivity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                  &lt;span class="nv"&gt;restorationHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  RN 0.74–0.78 (&lt;code&gt;RCTAppDelegate&lt;/code&gt; subclass)
&lt;/h3&gt;

&lt;p&gt;The superclass's &lt;code&gt;didFinishLaunching&lt;/code&gt; builds &lt;code&gt;rootViewFactory&lt;/code&gt; and then also creates a screen-sized window. You want the first part, not the second.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your &lt;code&gt;RCTAppDelegate&lt;/code&gt; exposes &lt;code&gt;automaticallyLoadReactNativeWindow&lt;/code&gt; (check the header in &lt;code&gt;node_modules/react-native/Libraries/AppDelegate/RCTAppDelegate.h&lt;/code&gt;), set it to &lt;code&gt;false&lt;/code&gt; before calling &lt;code&gt;super&lt;/code&gt;. Then use the 0.79 &lt;code&gt;SceneDelegate&lt;/code&gt; above with &lt;code&gt;appDelegate.rootViewFactory.view(...)&lt;/code&gt; in place of &lt;code&gt;factory.rootViewFactory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it doesn't, the clean fix is a small &lt;code&gt;patch-package&lt;/code&gt; patch that skips the window block in &lt;code&gt;RCTAppDelegate.mm&lt;/code&gt;, or upgrading to 0.79+. Calling &lt;code&gt;super&lt;/code&gt; and then making a second root view starts the JS app twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  RN 0.70 and earlier, or 0.71–0.73 on the old architecture (&lt;code&gt;RCTBridge&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Stop subclassing &lt;code&gt;RCTAppDelegate&lt;/code&gt; (on 0.71–0.73) and use a plain &lt;code&gt;UIApplicationDelegate&lt;/code&gt;. Create the bridge inside the scene, so it receives the cold-start link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SceneDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIResponder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UIWindowSceneDelegate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;RCTBridgeDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RCTBridge&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;willConnectTo&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UISceneSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="nv"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;ConnectionOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;windowScene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;UIWindowScene&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;bridge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTBridge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rnLaunchOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;connectionOptions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;rootView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RCTRootView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;moduleName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"HelloWorld"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;initialProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;vc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIViewController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rootView&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;windowScene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rootViewController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vc&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makeKeyAndVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;sourceURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;RCTBridge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cp"&gt;#if DEBUG&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;RCTBundleURLProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sharedSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsBundleURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forBundleRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#else&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withExtension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"jsbundle"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="cp"&gt;#endif&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// openURLContexts / continue userActivity: same forwarding as the 0.79 variant&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a native SDK needs the bridge during &lt;code&gt;didFinishLaunching&lt;/code&gt;, keep creating it there. Then, once JS has loaded, forward the scene's cold-start URL with &lt;code&gt;RCTLinkingManager.application(_:open:options:)&lt;/code&gt;, so it arrives as a &lt;code&gt;Linking&lt;/code&gt; &lt;code&gt;url&lt;/code&gt; event instead of through &lt;code&gt;getInitialURL()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expo
&lt;/h3&gt;

&lt;p&gt;Expo SDK 58 ships RN 0.88 with this built in. On older SDKs, write a config plugin that applies the changes above at &lt;code&gt;prebuild&lt;/code&gt;. Editing &lt;code&gt;ios/&lt;/code&gt; by hand gets wiped on the next prebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be realistic about very old versions
&lt;/h3&gt;

&lt;p&gt;Xcode 27 is a new toolchain. RN versions several releases behind may hit unrelated build failures with it (C++ headers, Folly, Hermes builds). Scene adoption is necessary but may not be enough. Below roughly 0.76, budget for an upgrade anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep links, push and third-party SDKs
&lt;/h2&gt;

&lt;p&gt;This is where most migrations actually break. Audit everything that touches &lt;code&gt;AppDelegate&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom URL schemes (warm):&lt;/strong&gt; handled in &lt;code&gt;scene(_:openURLContexts:)&lt;/code&gt; and forwarded to &lt;code&gt;RCTLinkingManager&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal Links (warm):&lt;/strong&gt; handled in &lt;code&gt;scene(_:continue:)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start links:&lt;/strong&gt; come from &lt;code&gt;connectionOptions&lt;/code&gt; in &lt;code&gt;willConnectTo&lt;/code&gt;. In 0.88 only the first item is forwarded. On older versions, use the &lt;code&gt;rnLaunchOptions&lt;/code&gt; helper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution SDKs&lt;/strong&gt; (Branch, AppsFlyer, Adjust, Airbridge, your own): upgrade to a scene-aware version and call their scene APIs from &lt;code&gt;SceneDelegate&lt;/code&gt;. Check that their swizzling doesn't still assume AppDelegate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push notifications:&lt;/strong&gt; token registration and &lt;code&gt;UNUserNotificationCenterDelegate&lt;/code&gt; stay on AppDelegate. A notification tap that opens a deep link still reaches JS through your push library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home-screen quick actions:&lt;/strong&gt; move to &lt;code&gt;windowScene(_:performActionFor:completionHandler:)&lt;/code&gt;, plus &lt;code&gt;connectionOptions.shortcutItem&lt;/code&gt; at cold start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth / SSO callbacks&lt;/strong&gt; (&lt;code&gt;react-native-app-auth&lt;/code&gt;, Google Sign-In, Facebook Login): they usually resume through &lt;code&gt;open URL&lt;/code&gt;, so forward from the scene method to the SDK as well as to &lt;code&gt;RCTLinkingManager&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Libraries reading &lt;code&gt;delegate.window&lt;/code&gt; or &lt;code&gt;keyWindow&lt;/code&gt;&lt;/strong&gt; (splash screens, toasts, modal hosts, &lt;code&gt;react-native-navigation&lt;/code&gt;): set &lt;code&gt;appDelegate.window&lt;/code&gt; from the scene, or upgrade the library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AppState&lt;/code&gt; and analytics session timing:&lt;/strong&gt; verify foreground/background events still fire as expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing checklist and gotchas
&lt;/h2&gt;

&lt;p&gt;Test on a release build, on a real device, with the app killed between cold-start cases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;How to trigger&lt;/th&gt;
&lt;th&gt;Expect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal launch&lt;/td&gt;
&lt;td&gt;Tap icon&lt;/td&gt;
&lt;td&gt;No blank screen; no UIScene warning in the Xcode console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold custom-scheme link&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xcrun simctl openurl booted myapp://path&lt;/code&gt; with the app killed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;getInitialURL()&lt;/code&gt; returns the URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm custom-scheme link&lt;/td&gt;
&lt;td&gt;Same command, app in background&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Linking&lt;/code&gt; &lt;code&gt;url&lt;/code&gt; event fires once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold Universal Link&lt;/td&gt;
&lt;td&gt;Tap a link in Notes or Messages, app killed&lt;/td&gt;
&lt;td&gt;Correct screen opens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm Universal Link&lt;/td&gt;
&lt;td&gt;Same, app in background&lt;/td&gt;
&lt;td&gt;Correct screen opens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push tap (killed / background)&lt;/td&gt;
&lt;td&gt;Send a test push&lt;/td&gt;
&lt;td&gt;Routing works; token still registers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick action&lt;/td&gt;
&lt;td&gt;Long-press icon&lt;/td&gt;
&lt;td&gt;Action handled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background → foreground&lt;/td&gt;
&lt;td&gt;Home, then reopen&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AppState&lt;/code&gt; changes; no JS reload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPad resize / Stage Manager&lt;/td&gt;
&lt;td&gt;Resize window&lt;/td&gt;
&lt;td&gt;Layout responds to &lt;code&gt;useWindowDimensions&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev menu&lt;/td&gt;
&lt;td&gt;Shake (debug)&lt;/td&gt;
&lt;td&gt;Menu opens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UISceneDelegateClassName&lt;/code&gt; must be &lt;code&gt;$(PRODUCT_MODULE_NAME).SceneDelegate&lt;/code&gt; for a Swift class. For Objective-C, use the bare class name.&lt;/li&gt;
&lt;li&gt;Don't create React Native in both &lt;code&gt;AppDelegate&lt;/code&gt; and &lt;code&gt;SceneDelegate&lt;/code&gt;. Two root views means two JS app instances.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;UIApplicationSupportsMultipleScenes&lt;/code&gt; set to &lt;code&gt;false&lt;/code&gt; until RN supports multiple scenes; a second scene would need its own RN surface.&lt;/li&gt;
&lt;li&gt;Remove leftover &lt;code&gt;window = UIWindow(frame: UIScreen.main.bounds)&lt;/code&gt; lines. A window with no scene never shows.&lt;/li&gt;
&lt;li&gt;Watch the Xcode console for the UIScene log message. If it's gone, UIKit sees you as scene-based.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle" rel="noopener noreferrer"&gt;TN3187: Migrating to the UIKit scene-based life cycle&lt;/a&gt; (Apple)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/forums/thread/789004" rel="noopener noreferrer"&gt;Regarding the deadline for adopting the UIScene life cycle&lt;/a&gt; (Apple Developer Forums)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/forums/thread/813300" rel="noopener noreferrer"&gt;Mandatory UIScene lifecycle support in iOS 27&lt;/a&gt; (Apple Developer Forums)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://reactnative.dev/blog/2026/10/19/react-native-0.88" rel="noopener noreferrer"&gt;React Native 0.88 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/react-native-community/template/blob/0.88-stable/template/ios/HelloWorld/SceneDelegate.swift" rel="noopener noreferrer"&gt;RN 0.88 template SceneDelegate.swift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/react-native-community/template/blob/0.88-stable/template/ios/HelloWorld/Info.plist" rel="noopener noreferrer"&gt;RN 0.88 template Info.plist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/react-native-community/template/blob/0.87-stable/template/ios/HelloWorld/AppDelegate.swift" rel="noopener noreferrer"&gt;RN 0.87 template AppDelegate.swift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebook/react-native/pull/46298" rel="noopener noreferrer"&gt;RCTReactNativeFactory PR #46298&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://help.airbridge.io/en/developers/troubleshooting-deprecated-react-native-sdk-v2" rel="noopener noreferrer"&gt;Airbridge: Xcode 27 SceneDelegate migration for React Native&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The older-version code in Fix B isn't from an official template. It's built on public RN APIs (&lt;code&gt;RCTRootViewFactory&lt;/code&gt;, &lt;code&gt;RCTRootView&lt;/code&gt;, &lt;code&gt;RCTLinkingManager&lt;/code&gt;), so check it against the headers in your installed RN version.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ios</category>
    </item>
    <item>
      <title>EAS Update pricing: why shipping less often won't save you money</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/eas-update-pricing-why-shipping-less-often-wont-save-you-money-17dj</link>
      <guid>https://dev.to/ahsanalidotme/eas-update-pricing-why-shipping-less-often-wont-save-you-money-17dj</guid>
      <description>&lt;p&gt;Ship once a month or thirty times — two million users cost you the same $9,750 either way.&lt;br&gt;
I had the EAS Update pricing model backwards, and I think most people do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual billing rule
&lt;/h2&gt;

&lt;p&gt;From Expo's own docs: "A monthly active user counts only once per billing period, regardless of how many updates this user downloads."&lt;br&gt;
So the user charge is a flat line. $0.005 per updated user past your allowance, Production includes 50,000, and at two million installs that's 1,950,000 × $0.005 = $9,750. Your release cadence does not appear in that sum anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does scale: bandwidth
&lt;/h2&gt;

&lt;p&gt;Bandwidth is billed separately at $0.10 per GiB.&lt;br&gt;
Fifteen releases a month, 5 MB bundle, two million installs. If each install pulls roughly a third of those releases (most people don't open your app daily), that's about 48,000 GiB, so another ~$4,800. If your users are genuinely daily-active and pull most releases, it's closer to $14,500.&lt;br&gt;
Call it $15,000 to $24,000 a month, all in. The $10k is the floor, not the ceiling.&lt;br&gt;
"Let's ship less often to save money" doesn't touch your biggest line at all. Shipping smaller bundles does, and so does not pushing an update that only changes one screen to your entire install base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-hosting
&lt;/h2&gt;

&lt;p&gt;Self-hosted on Cloudflare R2 the same thing runs $50 to $100 a month, for one reason: zero egress fees. The bandwidth line that costs you five figures is simply free there.&lt;br&gt;
But that $50 is infrastructure. It is not the cost. What you're also taking on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprinting:&lt;/strong&gt; deciding whether a JS bundle is safe to serve to a given native binary. Get it wrong and you push a bundle importing a native module the installed app doesn't have. Crash on launch, on devices you cannot reach, with no kill switch.- &lt;strong&gt;Rollback&lt;/strong&gt;, measured in minutes from "we broke it" to "it's reverted".- &lt;strong&gt;Staged rollout&lt;/strong&gt; by percentage, because an OTA that hits 100% at once is just an outage with better timing.None of that is hard. All of it is yours now, forever, including at 2am.
## The takeaway
Expo is genuinely the right default, and I'd still start every new app there. But the economics invert somewhere around a million installs. Do the sum with your own install count and bundle size. Then decide.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>devops</category>
      <category>performance</category>
    </item>
    <item>
      <title>Shopify used agents to leave React Native. Expo used one to fix it. Same bet.</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Mon, 19 Oct 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/shopify-used-agents-to-leave-react-native-expo-used-one-to-fix-it-same-bet-1odh</link>
      <guid>https://dev.to/ahsanalidotme/shopify-used-agents-to-leave-react-native-expo-used-one-to-fix-it-same-bet-1odh</guid>
      <description>&lt;p&gt;Shopify used AI agents to get off React Native. Expo shipped an AI agent that fixes React Native bugs.&lt;br&gt;
Both came out the same month. Same bet, pointed in opposite directions.&lt;br&gt;
&lt;strong&gt;The bet: agents make large-scale mobile work cheap.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What each one did
&lt;/h2&gt;

&lt;p&gt;Shopify's Helix is the internal tooling behind their move to Swift and Kotlin. They rebuilt the Shop app in 12 weeks, and now they're working through the main app, which has 300+ screens.&lt;br&gt;
Expo's agent starts when someone adds a label to a GitHub issue. It reproduces the bug on a cloud simulator, fixes it and opens a PR with before and after recordings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong lesson
&lt;/h2&gt;

&lt;p&gt;Everyone will read the Shopify post as "native won." I don't think that's the lesson.&lt;br&gt;
Helix doesn't one-shot a spec. It works in small checkpoints, and each one has to pass gates: behaviour tests, UI review, two adversarial agent reviews, then a human.&lt;br&gt;
That only works if logic and UI are separable enough to test on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lesson
&lt;/h2&gt;

&lt;p&gt;That's a testability problem, not a React Native problem. Most RN codebases I've seen in 10 years have it too.&lt;br&gt;
Every native vs cross-platform cycle I've lived through was decided by team shape, not framework quality. This one is being decided by which codebase an agent can safely navigate.&lt;br&gt;
So I wouldn't migrate on Monday. I'd measure how much of my app an agent can touch without breaking something. That number matters more than the framework.&lt;br&gt;
&lt;strong&gt;How much of your app could an agent safely change today?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>ai</category>
      <category>agents</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How I stop 18 Claude Code agents from pushing to main or touching secrets</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:00:01 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/how-i-stop-18-claude-code-agents-from-pushing-to-main-or-touching-secrets-3fij</link>
      <guid>https://dev.to/ahsanalidotme/how-i-stop-18-claude-code-agents-from-pushing-to-main-or-touching-secrets-3fij</guid>
      <description>&lt;p&gt;18 AI agents. 7 repos. Now none of them can push to main, touch a secret, or wander into another repo.&lt;br&gt;
Claude Code Mods made that possible.&lt;br&gt;
Every time Claude is about to act, it fires an event first. Your hook runs before the action and can let it through, change it, or block it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm setting up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  tool.call: keep each agent in its lane
&lt;/h3&gt;

&lt;p&gt;An edit outside its worktree pauses and asks me. No answer means refused.&lt;/p&gt;

&lt;h3&gt;
  
  
  tool.check: no pushing to main
&lt;/h3&gt;

&lt;p&gt;It checks the current branch and denies git push on main, even if a permission rule allows it.&lt;/p&gt;

&lt;h3&gt;
  
  
  tool.call: protect secrets
&lt;/h3&gt;

&lt;p&gt;It blocks any read or edit of .env, keystores or .p8 files.&lt;/p&gt;

&lt;h3&gt;
  
  
  prompt.submit: add context automatically
&lt;/h3&gt;

&lt;p&gt;Every prompt gets the repo, branch and ticket number added. No more repeating myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  turn.step: watch costs
&lt;/h3&gt;

&lt;p&gt;It logs token usage and cache hits on every model call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;When a hook blocks something, Claude reads the reason and changes approach instead of just failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One gotcha
&lt;/h2&gt;

&lt;p&gt;If a hook crashes, Claude Code skips it and runs the action anyway. Add a .catch that denies.&lt;br&gt;
&lt;strong&gt;CLAUDE.md says what you want. Mods decide what's allowed.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your AI agent can't open your mobile app. Here's the plumbing that fixes it</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Wed, 14 Oct 2026 18:43:45 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/your-ai-agent-cant-open-your-mobile-app-heres-the-plumbing-that-fixes-it-3n9h</link>
      <guid>https://dev.to/ahsanalidotme/your-ai-agent-cant-open-your-mobile-app-heres-the-plumbing-that-fixes-it-3n9h</guid>
      <description>&lt;p&gt;On mobile, the real problem is that the agent can't open the app. It can write a screen. It can't tap through it, see it break, or prove it works.&lt;br&gt;
The model isn't the bottleneck. The plumbing is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Give it a device
&lt;/h3&gt;

&lt;p&gt;Maestro's MCP server lets the agent tap, assert and explore on a simulator.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Give it typed tools, not a debug menu
&lt;/h3&gt;

&lt;p&gt;Things like "log in as test user" or "reset cart" get a declared name, a schema and a timeout. Callstack's Appduct does this without shipping a debug menu in your production binary.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make it leave a file, not a transcript
&lt;/h3&gt;

&lt;p&gt;Exploration ends as a Maestro flow in .maestro/. The agent writes the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CI owns it
&lt;/h3&gt;

&lt;p&gt;That flow runs on every PR like any other test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden cost
&lt;/h2&gt;

&lt;p&gt;Seed data and auth. Agents aren't flaky because of complex UI. They're flaky because the test fixtures are unstable.&lt;br&gt;
Keep agents on exploratory work like new flows and edge cases nobody wrote. The critical-path suite stays deterministic and owned by humans.&lt;br&gt;
&lt;strong&gt;Build the plumbing and the model barely matters.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>testing</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
    <item>
      <title>CLAUDE.md is not a quality gate. Here's how to build one with hooks</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Sat, 10 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/claudemd-is-not-a-quality-gate-heres-how-to-build-one-with-hooks-1e66</link>
      <guid>https://dev.to/ahsanalidotme/claudemd-is-not-a-quality-gate-heres-how-to-build-one-with-hooks-1e66</guid>
      <description>&lt;p&gt;Most devs think writing "always run tests before finishing" in CLAUDE.md is enough. It isn't.&lt;br&gt;
CLAUDE.md is context, not enforcement. The agent reads it, and most of the time it follows it. Most of the time isn't a quality gate.&lt;br&gt;
Long session, big context, a tricky fix, and suddenly it says "Done ✅" while your build is red.&lt;br&gt;
&lt;strong&gt;If a check matters, don't ask for it. Enforce it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks
&lt;/h2&gt;

&lt;p&gt;Hooks are shell commands Claude Code runs automatically at set points. The agent can't skip them, forget them or talk its way past them. Here's a real quality gate in .claude/settings.json:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PostToolUse: fast checks after every edit
&lt;/h3&gt;

&lt;p&gt;Matcher: Edit|Write. Run eslint --fix on just the file that changed. The file path comes in on stdin, so it's scoped and quick.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Stop: the hard gate before the agent can finish
&lt;/h3&gt;

&lt;p&gt;Run the full typecheck and tests: tsc --noEmit &amp;amp;&amp;amp; vitest run. If they fail, the agent can't end its turn. It gets the errors and has to fix them.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The part most people miss: exit code 2
&lt;/h3&gt;

&lt;p&gt;Only exit code 2 blocks the agent and sends the error back to it. tsc and vitest exit with 1 when they fail, so the agent just carries on. Wrap it:&lt;br&gt;
(tsc --noEmit &amp;amp;&amp;amp; vitest run) &amp;gt;&amp;amp;2 || exit 2&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop closes by itself
&lt;/h2&gt;

&lt;p&gt;Agent writes code → check fails → agent reads the error → fixes it → tries again. You stop being the agent's test runner.&lt;br&gt;
&lt;strong&gt;CLAUDE.md tells the agent what you want. Hooks make sure it actually happens.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The device matrix was a cost problem, not a QA problem</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:20:01 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/the-device-matrix-was-a-cost-problem-not-a-qa-problem-3kfj</link>
      <guid>https://dev.to/ahsanalidotme/the-device-matrix-was-a-cost-problem-not-a-qa-problem-3kfj</guid>
      <description>&lt;p&gt;I have never signed off on a mobile release feeling confident about the device matrix. Not once. Nobody I've worked with has either.&lt;/p&gt;

&lt;p&gt;The reason is boring. Walking the same screen across fifteen screen sizes, three OS versions and two accessibility scales is a full day of someone's life, and it's a day nobody budgets for. So you test on three devices, you ship, and you let the reports tell you about the other twelve.&lt;/p&gt;

&lt;p&gt;That's the constraint four separate releases quietly removed this week.&lt;/p&gt;

&lt;p&gt;Callstack took Apex generally available — a coding model specialised in React, strongest on RN and Next.js. Codex shipped Mobile Dev: Simulator, Instruments, logcat and Metro in one surface. Stim Desktop 0.1 watches and steers an agent's React Native work with live devices, replay and logs. Medula shipped headless devtools built for agents.&lt;/p&gt;

&lt;p&gt;Nobody coordinated that. Four teams hit the same wall in the same quarter.&lt;/p&gt;

&lt;p&gt;And the wall was never code generation. Agents write RN components fine. The wall was that the agent couldn't see the device — and neither could we, at any width that mattered.&lt;br&gt;
So the shift isn't "agents write my tests". It's that device breadth stopped being expensive. Twenty simulator configurations running the same journey is now a cheap thing to kick off, not a sprint you have to argue for.&lt;/p&gt;

&lt;p&gt;What I'd stay honest about: breadth is not confidence. An agent exploring twenty devices and reporting back is twenty chances to be told something plausible and wrong. It only becomes real when the run leaves an artefact behind — a flow file your CI replays on every PR. Then the second run is deterministic and the twentieth is free.&lt;/p&gt;

&lt;p&gt;That's the whole play. Pick the matrix you've been quietly ignoring. Have the agent explore it once. Keep what it finds as tests you own.&lt;/p&gt;

&lt;p&gt;We spent a decade calling the device matrix a QA problem. It was a cost problem, and the cost just moved.&lt;/p&gt;

&lt;h1&gt;
  
  
  react-native #ai #agentic-development
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>mobile</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Running a Multi-Repo Product with Parallel AI Agents (Claude Code)</title>
      <dc:creator>Ahsan Ali</dc:creator>
      <pubDate>Mon, 05 Oct 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/ahsanalidotme/running-a-multi-repo-product-with-parallel-ai-agents-claude-code-2ab4</link>
      <guid>https://dev.to/ahsanalidotme/running-a-multi-repo-product-with-parallel-ai-agents-claude-code-2ab4</guid>
      <description>&lt;h1&gt;
  
  
  Running a Multi-Repo Product with Parallel AI Agents (Claude Code)
&lt;/h1&gt;

&lt;p&gt;How I set up one workspace where a single Claude Code session hands work to one agent per repository, keeps them out of each other's code, and runs several tasks at the same time without them stepping on each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup:&lt;/strong&gt; 7 independent git repos (API backend, customer dashboard, admin portal, marketing site, iOS SDK, Android SDK, an internal Python tool) sitting side by side in one folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The idea in one picture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workspace/                      ← its own small git repo (agents, rules, contracts)
├── CLAUDE.md                   ← map of all repos + shared rules
├── contracts/                  ← the ONLY place cross-repo changes are agreed
├── .claude/
│   ├── agents/&amp;lt;repo&amp;gt;.md        ← one agent per repo + a read-only test auditor
│   ├── hooks/                  ← guards that enforce the rules
│   └── settings.json
├── clone-all.sh                ← one command to get every repo
├── backend/                    ← separate repo (ignored by the workspace repo)
├── client-dashboard/           ← separate repo
└── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You talk to &lt;strong&gt;one main session&lt;/strong&gt; started at the workspace root. It plans, starts one agent copy per task per repo (each in its own git worktree), and collects the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Make the workspace root its own repo
&lt;/h3&gt;

&lt;p&gt;Don't use submodules. Keep each product repo as a normal clone, and make the root a tiny repo that tracks only workspace files. Use an &lt;strong&gt;allowlist&lt;/strong&gt; &lt;code&gt;.gitignore&lt;/code&gt; so a product repo can never be committed by accident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*
!/.gitignore
!/README.md
!/CLAUDE.md
!/clone-all.sh
!/contracts/
!/.claude/
/.claude/settings.local.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;clone-all.sh&lt;/code&gt; that clones every repo into its folder, so a new machine or teammate gets the same layout with one command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: A CLAUDE.md in every repo
&lt;/h3&gt;

&lt;p&gt;Keep each one short (under 300 lines): purpose, stack, key folders, entry points, run/test/build commands, conventions. Generate it by having an agent read &lt;code&gt;package.json&lt;/code&gt;, build files and the existing docs, then &lt;strong&gt;check the commands against the real scripts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;End each one with a pointer to the shared rules instead of copying them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Agent rules
Shared rules live in the workspace root ../CLAUDE.md.
In short: edit only this repo, put cross-repo needs in ../contracts/.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code automatically loads &lt;code&gt;CLAUDE.md&lt;/code&gt; files from parent folders, so the root rules apply inside every repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The root CLAUDE.md
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;One line per repo: name, stack, role.&lt;/li&gt;
&lt;li&gt;How the repos connect (who calls which API, which auth).&lt;/li&gt;
&lt;li&gt;The core rule: &lt;strong&gt;route work to the matching agent; cross-repo changes go through /contracts.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A git workflow (branch from &lt;code&gt;develop&lt;/code&gt;, never skip hooks, one PR per repo).&lt;/li&gt;
&lt;li&gt;Parallel-work rules (see section 4).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: One agent type per repo, one copy per task
&lt;/h3&gt;

&lt;p&gt;A file in &lt;code&gt;.claude/agents/&lt;/code&gt; defines an agent &lt;strong&gt;type&lt;/strong&gt;, not a single worker. The main session can start many copies of the &lt;code&gt;backend&lt;/code&gt; agent at once, one per task. Each copy works in &lt;strong&gt;its own git worktree&lt;/strong&gt;, never in your main checkout.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.claude/agents/backend.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;work&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/backend:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node/Express&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;API,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;database,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;webhooks,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;endpoints&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;used&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dashboards&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SDKs."&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Edit, Write, Grep, Glob, Bash&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="s"&gt;You own the backend repo. Many copies of you can run at once, one per task,&lt;/span&gt;
&lt;span class="s"&gt;so you never work in the main checkout.&lt;/span&gt;

&lt;span class="s"&gt;1. Read backend/CLAUDE.md first, and follow it.&lt;/span&gt;
&lt;span class="s"&gt;2. Work in your own worktree, one per task&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
   &lt;span class="s"&gt;git -C backend worktree add ../_wt/backend-&amp;lt;task&amp;gt; -b &amp;lt;type&amp;gt;/&amp;lt;task&amp;gt; origin/develop&lt;/span&gt;
   &lt;span class="s"&gt;Install dependencies there for real; never symlink node_modules.&lt;/span&gt;
&lt;span class="s"&gt;3. Edit only inside _wt/backend-&amp;lt;task&amp;gt;/. Never edit backend/ itself (it's the user's&lt;/span&gt;
   &lt;span class="s"&gt;checkout) and never edit another repo.&lt;/span&gt;
&lt;span class="na"&gt;4. Cross-repo needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write contracts/&amp;lt;task&amp;gt;.md (owner, consumers, status), then stop.&lt;/span&gt;
&lt;span class="s"&gt;5. Write tests for what you changed, including edge cases, and run them. Then reply&lt;/span&gt;
   &lt;span class="s"&gt;with your worktree path so the main session can run test-auditor. Fix any gaps first.&lt;/span&gt;
&lt;span class="s"&gt;6. Commit, push (hooks on, never --no-verify), open one PR to develop.&lt;/span&gt;
&lt;span class="na"&gt;7. Append a 2-line summary to backend/.agent-notes.md. Final reply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worktree, branch,&lt;/span&gt;
   &lt;span class="s"&gt;PR link, tests run, open contract items.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The description is what routing uses&lt;/strong&gt;, so make it specific ("Use for any work in /backend: …").&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: A read-only test auditor that judges quality, not just presence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test-auditor&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use after a code change, before pushing, to audit one task's worktree. Read-only.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob, Bash&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="s"&gt;1. List what changed in the worktree vs origin/develop. Read the task.&lt;/span&gt;
&lt;span class="na"&gt;2. Coverage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;every new or changed behaviour has a test; no untested branch,&lt;/span&gt;
   &lt;span class="s"&gt;error path or validation rule.&lt;/span&gt;
&lt;span class="na"&gt;3. Missing edge cases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;name the specific ones that should exist but don't&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
   &lt;span class="s"&gt;empty/null, boundaries (0, 1, max), invalid input, auth/permission failures,&lt;/span&gt;
   &lt;span class="s"&gt;cross-tenant access, duplicates/idempotency, retries/timeouts, dates/time zones,&lt;/span&gt;
   &lt;span class="s"&gt;pagination, UI loading/empty/error states.&lt;/span&gt;
&lt;span class="na"&gt;4. Value: flag tests that would still pass if the code were broken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no real&lt;/span&gt;
   &lt;span class="s"&gt;assertion, asserting a mock's own return value, testing implementation details,&lt;/span&gt;
   &lt;span class="s"&gt;snapshot-only logic, copy-paste cases, over-mocking the thing under test.&lt;/span&gt;
&lt;span class="s"&gt;5. Run the tests. Output PASS, or GAPS&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;one specific bullet per gap, most important first.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has no Edit or Write tools, so it reports problems instead of quietly "fixing" them. Each gap is specific enough for the repo agent to write the missing test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: /contracts for cross-repo work
&lt;/h3&gt;

&lt;p&gt;A folder of small spec files, one per topic. Each says: &lt;strong&gt;owner repo, consumers, status (proposed / agreed / implemented)&lt;/strong&gt;, and the shape (types, endpoints, examples). An agent never edits another repo; it writes the need here, and the owning agent implements it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Enforce the rules with hooks (instructions alone aren't enough)
&lt;/h3&gt;

&lt;p&gt;Rules written in &lt;code&gt;CLAUDE.md&lt;/code&gt; are suggestions. Hooks are code the harness runs every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PreToolUse guard:&lt;/strong&gt; a repo agent may edit only its own worktrees, &lt;code&gt;contracts/&lt;/code&gt;, and its notes file. Not other repos, not other agents' worktrees, and not your main checkout. Hook input includes &lt;code&gt;agent_type&lt;/code&gt; when a subagent makes the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .claude/hooks/repo-guard.js (trimmed)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// fail open&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool_input&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nx"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// set only inside a subagent&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-auditor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CLAUDE_PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ownWorktree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;_wt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// _wt/backend-&amp;lt;task&amp;gt;/&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notesFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.agent-notes.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Allowed: own worktrees, contracts/, own notes file. Not the main checkout.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ownWorktree&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contracts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;notesFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;hookSpecificOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;hookEventName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PreToolUse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;permissionDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;permissionDecisionReason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Agent "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;": work in _wt/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&amp;lt;task&amp;gt;/; cross-repo needs go to contracts/.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PostToolUse formatter:&lt;/strong&gt; each repo gets a hook that formats the file Claude just edited, using &lt;strong&gt;that repo's own pinned prettier&lt;/strong&gt; (&lt;code&gt;node_modules/.bin/prettier&lt;/code&gt;, not &lt;code&gt;npx&lt;/code&gt;), limited to the file types its CI checks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt; at the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit|NotebookEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/repo-guard.js&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/format-dispatch.js&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also add a guard that blocks reading &lt;code&gt;.env&lt;/code&gt; files. It caught me (the AI setting this up) trying to link a &lt;code&gt;.env&lt;/code&gt; into a temp checkout, which is exactly what it's for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Keep local git hooks fast
&lt;/h3&gt;

&lt;p&gt;Pre-push runs only &lt;strong&gt;lint and the formatter check&lt;/strong&gt; (seconds). Typecheck, unit tests, build and E2E run in &lt;strong&gt;CI on the pull request&lt;/strong&gt;, where branch protection gates the merge. Slow local hooks teach people (and agents) to use &lt;code&gt;--no-verify&lt;/code&gt;, which then skips the fast checks too.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How a task flows
&lt;/h2&gt;

&lt;p&gt;Start Claude Code at the workspace root and describe the task. Example: &lt;em&gt;"Add utm_campaign to link analytics and show it on the dashboard."&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; The main session sees that two repos are involved and that it's a cross-repo change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract first.&lt;/strong&gt; A backend copy writes &lt;code&gt;contracts/utm.md&lt;/code&gt; (owner, consumers, shape, status).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build each side in parallel.&lt;/strong&gt; One backend copy in &lt;code&gt;_wt/backend-utm/&lt;/code&gt;, one dashboard copy in &lt;code&gt;_wt/client-dashboard-utm/&lt;/code&gt;, each on its own branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit.&lt;/strong&gt; Each copy runs its tests and returns its worktree path. The main session runs &lt;code&gt;test-auditor&lt;/code&gt; on it, and gaps go back to that same copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship.&lt;/strong&gt; Each copy pushes (lint + format hooks) and opens its PR. CI runs the heavy checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report.&lt;/strong&gt; The main session summarizes with PR links and removes the worktrees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Subagents can't start other subagents, so every handoff goes through the main session. You only talk to that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Running many tasks at the same time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every task gets its own agent copy and its own worktree&lt;/strong&gt;, so tasks never share files or branches, even when ten of them touch the same repo.&lt;/p&gt;

&lt;p&gt;Example: 10 tasks, 8 touching backend + dashboard and 2 backend-only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8 full-stack tasks → 8 backend copies + 8 dashboard copies (same task name, joined by a contract)
2 backend-only     → 2 backend copies
                     = 18 agent copies, each in its own _wt/&amp;lt;repo&amp;gt;-&amp;lt;task&amp;gt;/ worktree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Limits and what to do about them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Machine load (installs, builds):&lt;/strong&gt; run in waves of ~4 copies; keep local hooks to lint + format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests that share one local database:&lt;/strong&gt; run that repo's test suite one copy at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token cost and rate limits:&lt;/strong&gt; waves; the main session only keeps each copy's short summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task B needs task A's API:&lt;/strong&gt; contract first; list dependencies in the prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A prompt that works:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here are 10 tasks. Run one agent copy per task per repo, each in its own worktree. Cross-repo tasks go contract-first, then both sides in parallel. Max 4 copies at a time. Run test-auditor on every worktree before pushing, open one PR per repo per task, then give me one summary with the PR links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Lessons learned (the painful ones)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hooks can be silently off.&lt;/strong&gt; Git runs nothing if &lt;code&gt;core.hooksPath&lt;/code&gt; points to a folder that doesn't exist (for example, husky never installed in a stale checkout). CI failed on a formatting issue the pre-push hook would have caught. Check that &lt;code&gt;.husky/_&lt;/code&gt; exists, or run the hook once by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A PostToolUse hook only sees Claude's file-edit tools.&lt;/strong&gt; Files changed through shell scripts, or written before the hook existed, are not formatted. CI is still the backstop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that a PR is still open before pushing to its branch.&lt;/strong&gt; I pushed follow-up commits to PRs that had already been merged; they never reached &lt;code&gt;develop&lt;/code&gt; and had to be moved to new PRs. This is now a written rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never --no-verify to get past a hook,&lt;/strong&gt; not even for a doc-only commit. If a hook can't run in a temp checkout, fix the checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't symlink node_modules into a worktree.&lt;/strong&gt; Next.js/Turbopack refuses a symlink that points outside the project. Do a real install (&lt;code&gt;pnpm install --prefer-offline&lt;/code&gt; is fast thanks to the shared store).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settings load only from the folder you start in.&lt;/strong&gt; Root hooks apply when you start at the root; a repo's own &lt;code&gt;.claude/settings.json&lt;/code&gt; applies when you start inside it. Ancestor &lt;code&gt;CLAUDE.md&lt;/code&gt; files load either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative paths only.&lt;/strong&gt; Absolute machine paths in shared docs break for every teammate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep settings.local.json out of git.&lt;/strong&gt; It holds personal permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Checklist to copy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Workspace root repo with an allowlist &lt;code&gt;.gitignore&lt;/code&gt; + &lt;code&gt;clone-all.sh&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Short &lt;code&gt;CLAUDE.md&lt;/code&gt; per repo, with commands checked against real scripts&lt;/li&gt;
&lt;li&gt;Root &lt;code&gt;CLAUDE.md&lt;/code&gt;: repo map, connections, routing rule, git workflow, parallel rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/agents/&amp;lt;repo&amp;gt;.md&lt;/code&gt; (one type per repo, one copy + worktree per task) + a read-only &lt;code&gt;test-auditor&lt;/code&gt; that checks edge cases and test value&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contracts/&lt;/code&gt; with owner / consumers / status per spec&lt;/li&gt;
&lt;li&gt;PreToolUse repo guard + &lt;code&gt;.env&lt;/code&gt; guard; PostToolUse formatter using each repo's pinned tools&lt;/li&gt;
&lt;li&gt;Pre-push = lint + format only; everything heavy in CI on the PR&lt;/li&gt;
&lt;li&gt;Guard blocks agents from the main checkout; waves of ~4 copies&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
