<?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: day</title>
    <description>The latest articles on DEV Community by day (@day_b3fa2204948ded3059f6f).</description>
    <link>https://dev.to/day_b3fa2204948ded3059f6f</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%2F1519994%2Fcd0504e4-ce19-45b8-8d56-11161fd684cd.jpg</url>
      <title>DEV Community: day</title>
      <link>https://dev.to/day_b3fa2204948ded3059f6f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/day_b3fa2204948ded3059f6f"/>
    <language>en</language>
    <item>
      <title>Claude Code deletes clean worktrees on exit without asking. git worktree lock stops it.</title>
      <dc:creator>day</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:12:29 +0000</pubDate>
      <link>https://dev.to/day_b3fa2204948ded3059f6f/claude-code-deletes-clean-worktrees-on-exit-without-asking-git-worktree-lock-stops-it-ej</link>
      <guid>https://dev.to/day_b3fa2204948ded3059f6f/claude-code-deletes-clean-worktrees-on-exit-without-asking-git-worktree-lock-stops-it-ej</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you &lt;code&gt;/exit&lt;/code&gt; a &lt;code&gt;claude --worktree &amp;lt;name&amp;gt;&lt;/code&gt; session with &lt;strong&gt;no uncommitted changes and no new commits&lt;/strong&gt;, Claude Code removes the worktree &lt;strong&gt;and its branch&lt;/strong&gt; without showing the Keep/Remove dialog.&lt;/li&gt;
&lt;li&gt;This is documented behavior, not a bug. There is no setting to turn it off.&lt;/li&gt;
&lt;li&gt;Claude Code holds its own &lt;code&gt;git worktree lock&lt;/code&gt; on the worktree and only ever releases locks whose reason matches its own format. &lt;strong&gt;Re-lock the worktree with your own reason and the cleanup backs off.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;SessionStart&lt;/code&gt; hook can do the re-lock automatically.&lt;/li&gt;
&lt;li&gt;Pre-creating the worktree with &lt;code&gt;git worktree add&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; protect it. If its branch is named &lt;code&gt;worktree-&amp;lt;name&amp;gt;&lt;/code&gt;, existing unmerged commits get force-deleted too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tested on Claude Code 2.1.263, git 2.52.0, macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;You start an isolated session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--worktree&lt;/span&gt; wt-clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You read some code, maybe run a few commands, and then &lt;code&gt;/exit&lt;/code&gt;. You never picked "Remove worktree". But:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree list
/path/to/demo  731024a &lt;span class="o"&gt;[&lt;/span&gt;main]
&lt;span class="c"&gt;# .claude/worktrees/wt-clean is gone, and so is branch worktree-wt-clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you blink you see &lt;code&gt;Cleaning up worktree (no pending changes)…&lt;/code&gt; flash by. That's the only notice you get.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the exit code actually checks
&lt;/h2&gt;

&lt;p&gt;I pulled the bundled source out of the Claude Code binary to see the decision. On &lt;code&gt;/exit&lt;/code&gt; from a worktree session it runs two git commands inside the worktree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;                                  &lt;span class="c"&gt;# changed + untracked files&lt;/span&gt;
git rev-list &lt;span class="nt"&gt;--count&lt;/span&gt; &amp;lt;HEAD at session start&amp;gt;..HEAD      &lt;span class="c"&gt;# new commits this session&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it branches like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Worktree state&lt;/th&gt;
&lt;th&gt;Session named?&lt;/th&gt;
&lt;th&gt;What &lt;code&gt;/exit&lt;/code&gt; does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 changed files, 0 new commits&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Removes worktree and branch, no prompt&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 changed files, 0 new commits&lt;/td&gt;
&lt;td&gt;yes (&lt;code&gt;/rename&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Keep / Remove dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any uncommitted change or any new commit&lt;/td&gt;
&lt;td&gt;either&lt;/td&gt;
&lt;td&gt;Keep / Remove dialog&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The silent path runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree unlock &amp;lt;path&amp;gt;
git worktree remove &lt;span class="nt"&gt;--force&lt;/span&gt; &amp;lt;path&amp;gt;
git branch &lt;span class="nt"&gt;-D&lt;/span&gt; worktree-&amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "new commits" check is relative to the HEAD &lt;strong&gt;at session start&lt;/strong&gt;. Anything committed before the session doesn't count. Keep that in mind for the pre-created worktree gotcha below.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's documented
&lt;/h3&gt;

&lt;p&gt;From the official worktrees page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The worktree is clean&lt;/strong&gt;: for an unnamed session, Claude removes the worktree and its branch automatically. A named session prompts you first so you can keep the worktree for later&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the design intent is "clean means disposable". Plenty of people disagree. Related issues (all closed):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/27753" rel="noopener noreferrer"&gt;#27753&lt;/a&gt; Worktree auto-deleted on exit when work is committed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/46444" rel="noopener noreferrer"&gt;#46444&lt;/a&gt; worktree auto-cleanup permanently deleted 10 days of uncommitted project work without any warning&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/58432" rel="noopener noreferrer"&gt;#58432&lt;/a&gt; WorktreeRemove hook suppresses the /exit keep-or-remove picker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no &lt;code&gt;worktree.keepOnExit&lt;/code&gt;-style setting. So we lean on git instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;git worktree lock&lt;/code&gt; works
&lt;/h2&gt;

&lt;p&gt;Claude Code already uses &lt;code&gt;git worktree lock&lt;/code&gt; on every worktree it creates. Look at the registry while a session is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
worktree /path/to/demo/.claude/worktrees/wt-lock
HEAD 731024acd920b9883ae2f00845fa4ebc8ae0230e
branch refs/heads/worktree-wt-lock
locked claude session wt-lock &lt;span class="o"&gt;(&lt;/span&gt;pid 72411 start Tue Sep  8 13:50:41 2026&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At exit, the cleanup routine reads the lock reason back and tests it against this regex (from the bundle):&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="o"&gt;/^&lt;/span&gt;&lt;span class="nf"&gt;claude &lt;/span&gt;&lt;span class="p"&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;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pid &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;})(?:&lt;/span&gt; &lt;span class="nf"&gt;start &lt;/span&gt;&lt;span class="p"&gt;(.{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;}))?&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the reason doesn't match, or it matches but the PID belongs to another live process, the routine keeps the worktree and logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cleanupWorktree: kept &amp;lt;path&amp;gt; — locked by another live Claude Code process, or with a reason we did not write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs say the same thing about the background stale-lock sweep: "&lt;strong&gt;The sweep never releases a lock you set yourself with &lt;code&gt;git worktree lock&lt;/code&gt;.&lt;/strong&gt;" A user-set lock is consistently treated as off-limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: re-lock by hand
&lt;/h2&gt;

&lt;p&gt;During the session, from another terminal (or with the &lt;code&gt;!&lt;/code&gt; prefix inside Claude Code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree unlock .claude/worktrees/wt-lock
git worktree lock &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"pinned by ryan"&lt;/span&gt; .claude/worktrees/wt-lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;You must unlock first.&lt;/strong&gt; Claude Code's lock is already there, so a bare &lt;code&gt;lock&lt;/code&gt; fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree lock &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"keep"&lt;/span&gt; .claude/worktrees/wt-lock
fatal: &lt;span class="s1"&gt;'.claude/worktrees/wt-lock'&lt;/span&gt; is already locked, reason: claude session wt-lock &lt;span class="o"&gt;(&lt;/span&gt;pid 72411 start ...&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;/exit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree list
/path/to/demo                             731024a &lt;span class="o"&gt;[&lt;/span&gt;main]
/path/to/demo/.claude/worktrees/wt-lock   731024a &lt;span class="o"&gt;[&lt;/span&gt;worktree-wt-lock] locked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason text doesn't matter. Running &lt;code&gt;git worktree unlock . &amp;amp;&amp;amp; git worktree lock .&lt;/code&gt; from inside the worktree, with no reason at all, also kept it.&lt;/p&gt;

&lt;p&gt;Next time, &lt;code&gt;claude --worktree wt-lock&lt;/code&gt; drops you back into the same worktree. Your lock stays, so it survives the next &lt;code&gt;/exit&lt;/code&gt; too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: automate it with a SessionStart hook
&lt;/h2&gt;

&lt;p&gt;Save this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/pin-worktree.sh&lt;/span&gt;
&lt;span class="c"&gt;# Keep Claude Code from auto-removing this linked worktree on exit.&lt;/span&gt;
&lt;span class="nv"&gt;git_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="nv"&gt;common_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-common-dir&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$git_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$common_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0      &lt;span class="c"&gt;# main worktree: nothing to pin&lt;/span&gt;
&lt;span class="nv"&gt;wt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
git worktree unlock &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$wt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null          &lt;span class="c"&gt;# drop Claude Code's own lock&lt;/span&gt;
git worktree lock &lt;span class="nt"&gt;--reason&lt;/span&gt; &lt;span class="s2"&gt;"pinned by SessionStart hook"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$wt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the main worktree &lt;code&gt;--git-dir&lt;/code&gt; and &lt;code&gt;--git-common-dir&lt;/code&gt; are the same path, so the script is a no-op there. In a linked worktree it swaps the lock.&lt;/p&gt;

&lt;p&gt;Register it:&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;"SessionStart"&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;"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="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;"sh &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$CLAUDE_PROJECT_DIR/.claude/hooks/pin-worktree.sh&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;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;Right after &lt;code&gt;claude --worktree&lt;/code&gt; starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;locked
locked pinned by SessionStart hook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/exit&lt;/code&gt; leaves the worktree in place, still locked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the settings file has to live
&lt;/h3&gt;

&lt;p&gt;This tripped me up, so here's what I measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Settings location&lt;/th&gt;
&lt;th&gt;Hook fires in the worktree session?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; committed to the repo&lt;/td&gt;
&lt;td&gt;yes (it's in the worktree checkout)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Untracked &lt;code&gt;.claude/settings.local.json&lt;/code&gt; only in the main checkout&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no&lt;/strong&gt; (the worktree checkout doesn't have it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A settings file outside the repo (passed with &lt;code&gt;--settings&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt; (user settings)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The worktree session reads project settings from the &lt;strong&gt;worktree's own checkout&lt;/strong&gt;. If you don't want to commit anything, put the hook in your user settings at &lt;code&gt;~/.claude/settings.json&lt;/code&gt; with an absolute script path. The script has no repo-specific logic, so it's safe globally.&lt;/p&gt;

&lt;p&gt;Side note from the docs: &lt;code&gt;$CLAUDE_PROJECT_DIR&lt;/code&gt; keeps pointing at the main checkout after Claude enters a worktree, while the hook's &lt;code&gt;cwd&lt;/code&gt; follows the worktree. That's why the script derives the path from &lt;code&gt;git rev-parse&lt;/code&gt; instead of the env var.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pre-creating the worktree doesn't help
&lt;/h3&gt;

&lt;p&gt;I assumed a worktree I made myself would be left alone. It wasn't. &lt;code&gt;claude --worktree &amp;lt;name&amp;gt;&lt;/code&gt; adopts an existing &lt;code&gt;.claude/worktrees/&amp;lt;name&amp;gt;&lt;/code&gt; and applies the same exit logic.&lt;/p&gt;

&lt;p&gt;Worse: if the branch is named &lt;code&gt;worktree-&amp;lt;name&amp;gt;&lt;/code&gt;, commits made &lt;strong&gt;before&lt;/strong&gt; the session don't count as "new", so an unmerged branch gets force-deleted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; worktree-wt-pre4 .claude/worktrees/wt-pre4
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; .claude/worktrees/wt-pre4 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;precious &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; precious.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"precious"&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;claude &lt;span class="nt"&gt;--worktree&lt;/span&gt; wt-pre4    &lt;span class="c"&gt;# do nothing, /exit&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;git branch &lt;span class="nt"&gt;--list&lt;/span&gt; worktree-wt-pre4
&lt;span class="c"&gt;# empty. No ref points at that commit any more.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit object still exists, so &lt;code&gt;git fsck --lost-found&lt;/code&gt; can dig it out, but only if you notice. If you hand a hand-made worktree to Claude Code, lock it first.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-off alternative: name the session
&lt;/h3&gt;

&lt;p&gt;A named session always gets the dialog, even when clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exiting worktree session
This session was named "keepme". Keep the worktree to resume it later, or remove it to clean up.
❯ 1. Keep worktree
  2. Remove worktree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/rename keepme&lt;/code&gt; then &lt;code&gt;/exit&lt;/code&gt; is the cheapest way to keep a worktree once. You have to remember it every time, which is why I prefer the hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up later
&lt;/h3&gt;

&lt;p&gt;Claude Code won't touch a worktree you locked, so removal is on you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree unlock .claude/worktrees/wt-lock
git worktree remove .claude/worktrees/wt-lock
git branch &lt;span class="nt"&gt;-D&lt;/span&gt; worktree-wt-lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Claude Code's worktree cleanup is aggressive by design: clean plus unnamed equals gone. The escape hatch is already built in, just not advertised. Claude Code refuses to release a &lt;code&gt;git worktree lock&lt;/code&gt; it didn't write, so a re-lock with your own reason, done by hand or by a &lt;code&gt;SessionStart&lt;/code&gt; hook, is enough to keep the worktree across sessions.&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code docs, worktrees: &lt;a href="https://code.claude.com/docs/en/worktrees" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/worktrees&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code docs, hooks: &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;https://code.claude.com/docs/en/hooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;git worktree manual: &lt;a href="https://git-scm.com/docs/git-worktree" rel="noopener noreferrer"&gt;https://git-scm.com/docs/git-worktree&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>git</category>
      <category>ai</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Turning Matt Pocock's /implement into a parallel orchestrator in Claude Code</title>
      <dc:creator>day</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:22:15 +0000</pubDate>
      <link>https://dev.to/day_b3fa2204948ded3059f6f/turning-matt-pococks-implement-into-a-parallel-orchestrator-in-claude-code-1d4a</link>
      <guid>https://dev.to/day_b3fa2204948ded3059f6f/turning-matt-pococks-implement-into-a-parallel-orchestrator-in-claude-code-1d4a</guid>
      <description>&lt;p&gt;I moved my Claude Code workflow from the superpowers plugin to &lt;a href="https://github.com/mattpocock/skills" rel="noopener noreferrer"&gt;Matt Pocock's skills&lt;/a&gt;. The chain &lt;code&gt;/grill-with-docs → /to-spec → /to-tickets → /implement&lt;/code&gt; is great, but &lt;code&gt;/implement&lt;/code&gt; is deliberately one ticket per session, with a &lt;code&gt;/clear&lt;/code&gt; in between. Matt's own docs say batch dispatch and subagent fan-out are "requested repeatedly, and neither exists."&lt;/p&gt;

&lt;p&gt;Meanwhile Claude Code already has everything an orchestrator needs: spawn a subagent, get its report, and send it a follow-up &lt;strong&gt;that resumes with its previous context intact&lt;/strong&gt;. That is the orchestrator-subagent pattern from Anthropic's &lt;a href="https://claude.com/blog/multi-agent-coordination-patterns" rel="noopener noreferrer"&gt;multi-agent coordination patterns&lt;/a&gt; post, and it maps onto Matt's tickets almost one to one.&lt;/p&gt;

&lt;p&gt;So I kept the planning half of Matt's flow and replaced only the build half with a skill called &lt;code&gt;/implement-orchestrated&lt;/code&gt;. Here is how it fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;/implement&lt;/code&gt; (Matt)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;implement-spec&lt;/code&gt; (Matt's unshipped draft)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;/implement-orchestrated&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;one ticket per session&lt;/td&gt;
&lt;td&gt;whole spec&lt;/td&gt;
&lt;td&gt;whole spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelism&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;implementer subagents in worktrees&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;coder&lt;/code&gt; subagents in worktrees, up to N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/code-review&lt;/code&gt; at the end&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/code-review&lt;/code&gt; at the end&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reviewer&lt;/code&gt; per ticket, then &lt;code&gt;code-review&lt;/code&gt; + &lt;code&gt;final-reviewer&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix loop&lt;/td&gt;
&lt;td&gt;same session&lt;/td&gt;
&lt;td&gt;unspecified&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SendMessage to the same coder&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four Claude Code features carry the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent tool&lt;/strong&gt; with &lt;code&gt;subagent_type: implement-orchestrated:coder&lt;/code&gt; (the namespaced form a plugin install gives you; hand-copied into &lt;code&gt;~/.claude/agents/&lt;/code&gt; it is the bare &lt;code&gt;coder&lt;/code&gt;) and &lt;code&gt;isolation: worktree&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SendMessage&lt;/strong&gt; to a finished subagent by name: it resumes from its transcript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;worktree.baseRef&lt;/code&gt;&lt;/strong&gt;: which commit subagent worktrees branch from&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.worktreeinclude&lt;/code&gt;&lt;/strong&gt;: copy gitignored files into each worktree&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read the spec and tickets. Build a state table (ticket, blockers, status, coder, branch, worktree) in a file outside the repo, because it has to survive context compaction.&lt;/li&gt;
&lt;li&gt;Create the feature branch, record a test baseline, open a draft PR.&lt;/li&gt;
&lt;li&gt;Dispatch the &lt;strong&gt;frontier&lt;/strong&gt; (every ticket whose blockers are closed) to &lt;code&gt;coder&lt;/code&gt; subagents, up to N at once, each in its own worktree.&lt;/li&gt;
&lt;li&gt;When a coder reports, spawn a &lt;code&gt;reviewer&lt;/code&gt;. On "Needs fixes", save the findings to a file and send the same coder the path. It already knows the ticket, the code, and what it did; the pointer is enough. Two failed rounds escalate to the human.&lt;/li&gt;
&lt;li&gt;Merge with &lt;code&gt;--no-ff&lt;/code&gt;, run the full suite, close the ticket, remove the worktree, recompute the frontier, go to 3.&lt;/li&gt;
&lt;li&gt;At the end: Matt's &lt;code&gt;code-review&lt;/code&gt; skill, a &lt;code&gt;final-reviewer&lt;/code&gt; pass, push, mark the PR ready.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The brief each coder gets is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ticket: &amp;lt;ticket pointer&amp;gt;. Spec: &amp;lt;spec pointer&amp;gt;. Notes: &amp;lt;notes&amp;gt;/notes.md (read it if it exists).

You are in your own git worktree on your own branch; every command and edit stays inside it. It holds tracked files plus any local config listed in `.worktreeinclude`, so run the project's install step (for example `npm ci`) before the first test. The glossary is `CONTEXT.md` in your worktree; if it is missing, read &amp;lt;main checkout&amp;gt;/CONTEXT.md and &amp;lt;main checkout&amp;gt;/docs/agents/. Build this ticket.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else, the TDD discipline, the report format, lives in the agent definition, not in the message.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coder agent
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;coder&lt;/code&gt; I had was five generic lines. Rewritten to fit Matt's &lt;code&gt;tdd&lt;/code&gt; skill, it now carries the two ideas that skill runs on: &lt;strong&gt;pre-agreed seams&lt;/strong&gt; (decide where tests go before writing any) and &lt;strong&gt;reporting by context pointer&lt;/strong&gt; (branch, SHAs, test command) instead of pasting diffs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&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;coder&lt;/span&gt;
&lt;span class="na"&gt;description: Use for coding tasks delegated to a subagent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;implementing a ticket or spec slice, fixing a bug, refactoring, writing tests. Works test-first at pre-agreed seams and reports by context pointers. Runs on Opus 5 with max reasoning effort.&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opus&lt;/span&gt;
&lt;span class="na"&gt;effort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are an implementer executing one scoped coding task from a brief. The plan was settled upstream: build what the brief says, and raise a design objection in your report rather than redesigning as you go.

&lt;span class="gu"&gt;## Before writing code&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Read the ticket or brief, then the code at the seams it names. When &lt;span class="sb"&gt;`CONTEXT.md`&lt;/span&gt; or &lt;span class="sb"&gt;`docs/adr/`&lt;/span&gt; exist, read the parts that touch this area: use the glossary's terms in names and tests, and flag any ADR your change would contradict.
&lt;span class="p"&gt;-&lt;/span&gt; Seams are pre-agreed: the brief or ticket names them. When it names none, test at the public boundary where the acceptance criteria are observable, and name that boundary in your report.

&lt;span class="gu"&gt;## Building&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Work as one vertical slice, red → green: call the Skill tool with "mattpocock-skills:tdd" and follow its loop, one failing test then the minimal code, seam by seam. When the interface shape itself is in question, call the Skill tool with "mattpocock-skills:codebase-design".
&lt;span class="p"&gt;-&lt;/span&gt; Typecheck often and run single test files as you go; run the full suite once at the end. Done means every acceptance criterion has a passing test at a seam, or a stated reason it cannot.
&lt;span class="p"&gt;-&lt;/span&gt; Commit on your current branch with a message that references the ticket. Merging, pushing, and the issue tracker belong to whoever dispatched you, unless the brief says otherwise.

&lt;span class="gu"&gt;## Report&lt;/span&gt;

Your final message is the return value; the caller saw none of your tool calls. Report by context pointer: branch, worktree path, commit SHAs, the test command and its result, the seams you tested at, and anything the ticket left ambiguous. Under 200 words; the diff speaks for itself.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isolation: worktree&lt;/code&gt; is deliberately not in the frontmatter. The orchestrator passes it per call, so the same agent still works for a plain bug fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two settings that make it work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Worktree base ref.&lt;/strong&gt; By default a new subagent worktree branches from the &lt;em&gt;remote&lt;/em&gt; default branch (&lt;code&gt;origin/main&lt;/code&gt;). The orchestrator merges each finished ticket into the feature branch and then dispatches the tickets that depend on it, so with the default, ticket 03's worktree branches from &lt;code&gt;origin/main&lt;/code&gt; and never sees ticket 01's merged code: the coder builds on stale code, or re-implements ticket 01 and conflicts at merge. With &lt;code&gt;head&lt;/code&gt;, every worktree branches from the current tip of the feature branch:&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;"worktree"&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;"baseRef"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"head"&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;Letting Claude edit &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for you may get refused by the permission classifier (it was in my run), so run this yourself once, then start a fresh session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json;p='&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/settings.json';d=json.load(open(p));d.setdefault('worktree',{})['baseRef']='head';json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gitignored files.&lt;/strong&gt; A worktree holds tracked files only. My glossary (&lt;code&gt;CONTEXT.md&lt;/code&gt;) and tracker config (&lt;code&gt;docs/agents/&lt;/code&gt;) are gitignored, so a &lt;code&gt;.worktreeinclude&lt;/code&gt; file (gitignore syntax) tells Claude Code to copy them into every worktree it creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONTEXT.md
docs/agents/
docs/adr/
CLAUDE.local.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;List specific paths. Do not list &lt;code&gt;.claude/&lt;/code&gt; wholesale: &lt;code&gt;.claude/worktrees/&lt;/code&gt; holds your other worktrees and would be copied into every new one, recursively, gigabytes at a time. Name the subfolders you need instead (&lt;code&gt;.claude/hooks/&lt;/code&gt;, &lt;code&gt;.claude/skills/&lt;/code&gt;, &lt;code&gt;.claude/settings.json&lt;/code&gt;). And the Agent tool's worktree isolation does not run your &lt;code&gt;PostToolUse&lt;/code&gt; hooks, so &lt;code&gt;.worktreeinclude&lt;/code&gt; is the only thing that copies untracked files into a coder's worktree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bit me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User-invoked skills can't be called by the orchestrator.&lt;/strong&gt; Skills with &lt;code&gt;disable-model-invocation: true&lt;/code&gt; (Matt's &lt;code&gt;/implement&lt;/code&gt;, &lt;code&gt;/to-spec&lt;/code&gt;) throw when called through the Skill tool. That is why the TDD discipline moved into the coder's prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A plugin's &lt;code&gt;agents&lt;/code&gt; array is not loaded.&lt;/strong&gt; The manifest reference documents &lt;code&gt;"agents": [...]&lt;/code&gt; in &lt;code&gt;plugin.json&lt;/code&gt;, but on Claude Code 2.1.265 the version that declared it registered nothing (&lt;code&gt;claude plugin details&lt;/code&gt; said &lt;strong&gt;Agents (0)&lt;/strong&gt;), and the version that dropped the array and relied on the default &lt;code&gt;agents/&lt;/code&gt; directory registered all three (&lt;strong&gt;Agents (3)&lt;/strong&gt;), namespaced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE_CODE_SUBAGENT_MODEL&lt;/code&gt; retargets every agent that does not pin a model.&lt;/strong&gt; It sets the default model for subagents whose definition declares none, and it applies silently: with the env var set globally to &lt;code&gt;opus[1m]&lt;/code&gt;, &lt;code&gt;final-reviewer&lt;/code&gt; — the one gate I want on the session model — started running on Opus. Any agent that must stay on the conversation's model needs &lt;code&gt;model: inherit&lt;/code&gt; written out explicitly. That is what plugin v0.1.3 added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Workflow tool can't continue an agent.&lt;/strong&gt; Each &lt;code&gt;agent()&lt;/code&gt; call there is fresh. The "same coder, previous context" property needs Agent + SendMessage inside a skill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worktree cleanup.&lt;/strong&gt; Claude Code removes a subagent worktree that finished without changes, but keeps one with unpushed commits. Coders commit, so the fix round lands in the same place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bare skill name is not the skill you mean.&lt;/strong&gt; The skill called Matt's review as &lt;code&gt;"code-review"&lt;/code&gt;. That name resolves to Claude Code's own bundled &lt;code&gt;code-review&lt;/code&gt; — and if you have a personal skill at &lt;code&gt;~/.claude/skills/code-review/&lt;/code&gt;, to that instead. The final gate could quietly run a review with no Spec axis while the README promised Matt's two-axis one. Every call now uses the &lt;code&gt;mattpocock-skills:&lt;/code&gt; prefix, which fails loudly when the plugin is missing (fixed in 0.1.5 and 0.1.6).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't run parallel &lt;code&gt;/implement&lt;/code&gt; sessions in one checkout.&lt;/strong&gt; Matt's docs have a field report of one session's &lt;code&gt;git commit --amend&lt;/code&gt; landing on another session's commit. Worktrees are the insurance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What happened when I ran it
&lt;/h2&gt;

&lt;p&gt;After publishing I ran &lt;code&gt;/implement-orchestrated screenshot-source 3&lt;/code&gt; on the same spec (2026-09-08, on my Chrome extension &lt;a href="https://github.com/ryoshumei/add-to-calendar" rel="noopener noreferrer"&gt;ryoshumei/add-to-calendar&lt;/a&gt;; the result is &lt;a href="https://github.com/ryoshumei/add-to-calendar/pull/14" rel="noopener noreferrer"&gt;PR #14&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;All 8 tickets closed, zero escalations.&lt;/strong&gt; 01, 02 and 07 went out three-wide at 06:17; seven tickets were merged by 08:07. Active orchestration time was about 1h50 for tickets 01 to 07, plus about 50 minutes for the last ticket and the final gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same-coder fix loop works.&lt;/strong&gt; Ticket 07's first review came back Needs fixes (a new Deno test broke the flagless &lt;code&gt;deno test&lt;/code&gt; invocation). One SendMessage with the path to the findings file, and the coder committed the fix (&lt;code&gt;8b2a729&lt;/code&gt;) on the same branch in the same worktree; round two was Approved. The cwd question from the first version of this post is answered: it stayed put, three times out of three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two surprises.&lt;/strong&gt; Tickets 04 and 05 touched the same files and conflicted at merge; instead of resolving in the main checkout, the orchestrator resumed 04's coder and had it merge the feature branch first. A flaky test traced back to the signed-in fixture leaving a second Supabase client alive; ticket 04's reviewer found the root cause, ticket 06 fixed it, and a maintenance ticket was logged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What hurt most was the Claude rate limit.&lt;/strong&gt; &lt;code&gt;coder&lt;/code&gt; and &lt;code&gt;reviewer&lt;/code&gt; are pinned to Opus with max effort; the eighth coder died at spawn with a 429 session limit. The orchestrator reported instead of hanging, and once allowed to continue it built that docs-only ticket itself, a logged deviation from "the orchestrator writes no feature code".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numbers:&lt;/strong&gt; 30 commits on the branch, +4,373 / −299 lines across 42 files. Playwright tests 27 → 89, Deno 86 → 89, final suite green. Subagent tokens about 3.24M in total (coders 1.61M, per-ticket reviewers 0.68M, final gate 0.96M).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest footnote:&lt;/strong&gt; running Claude Code's built-in &lt;code&gt;/code-review max&lt;/code&gt; on the branch afterwards produced 23 more findings, 3 of them flagged as security. They are fixed now (5 commits; the suite went 89 → 103 Playwright and 89 → 91 Deno, each finding verified by a separate reviewer), but the orchestrator's final gate should have caught them. The gap is structural rather than bad luck: that gate runs a Standards + Spec review and a whole-branch review, and neither is a correctness bug hunt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;The skill, the three agents, and install notes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ryoshumei/implement-orchestrated" rel="noopener noreferrer"&gt;https://github.com/ryoshumei/implement-orchestrated&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add ryoshumei/implement-orchestrated
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;implement-orchestrated@ryoshumei
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Claude Code 2.1.263 or newer; the current plugin version is 0.1.6. Installed as a plugin the agents are namespaced (&lt;code&gt;implement-orchestrated:coder&lt;/code&gt;, &lt;code&gt;implement-orchestrated:reviewer&lt;/code&gt;, &lt;code&gt;implement-orchestrated:final-reviewer&lt;/code&gt;); copied by hand into &lt;code&gt;~/.claude/agents/&lt;/code&gt; they are the bare &lt;code&gt;coder&lt;/code&gt; / &lt;code&gt;reviewer&lt;/code&gt; / &lt;code&gt;final-reviewer&lt;/code&gt;, and the orchestrator handles both. &lt;code&gt;claude plugin details implement-orchestrated@ryoshumei&lt;/code&gt; should list &lt;strong&gt;Agents (3)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You also need Matt's plugin installed and &lt;code&gt;/setup-matt-pocock-skills&lt;/code&gt; run once per repo; the orchestrator reads the tracker config that step writes.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Shipping an Expo app to the App Store: the landmines nobody warns you about</title>
      <dc:creator>day</dc:creator>
      <pubDate>Fri, 31 Jul 2026 20:14:43 +0000</pubDate>
      <link>https://dev.to/day_b3fa2204948ded3059f6f/shipping-an-expo-app-to-the-app-store-the-landmines-nobody-warns-you-about-2b4o</link>
      <guid>https://dev.to/day_b3fa2204948ded3059f6f/shipping-an-expo-app-to-the-app-store-the-landmines-nobody-warns-you-about-2b4o</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://apps.apple.com/app/id6772644308" rel="noopener noreferrer"&gt;Add to Calendar: AI Events&lt;/a&gt; — an iOS app that turns screenshots and text into calendar events — built with Expo SDK 52 and React Native's new architecture. The code is &lt;a href="https://github.com/ryoshumei/add-to-calendar-rn" rel="noopener noreferrer"&gt;open source (MIT)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The happy path in the Expo docs is genuinely good. This post is about everything that &lt;em&gt;isn't&lt;/em&gt; on the happy path: the things that cost me hours, in the order they'll probably cost you hours too.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An iOS Share Extension will dominate your build config
&lt;/h2&gt;

&lt;p&gt;The single hardest feature to ship was also the app's core flow: share a screenshot from any app → extension hands it to the main app → AI extracts the event. I used &lt;a href="https://github.com/achorein/expo-share-intent" rel="noopener noreferrer"&gt;&lt;code&gt;expo-share-intent&lt;/code&gt;&lt;/a&gt;, which is excellent, but a share extension is a &lt;em&gt;second binary target&lt;/em&gt;, and EAS Build needs to be told about it explicitly.&lt;/p&gt;

&lt;p&gt;Three things have to agree with each other or the build dies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.json (abridged)&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;"plugins"&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="s2"&gt;"expo-share-intent"&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;"disableExperimental"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;// ← don't let the plugin manage appExtensions&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"iosActivationRules"&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;"NSExtensionActivationSupportsImageWithMaxCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;"iosAppGroupIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"group.com.addtocalendar.rn"&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;"extra"&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;"eas"&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;"build"&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;"experimental"&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;"ios"&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;"appExtensions"&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="c1"&gt;// ← you declare the target yourself, once&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"targetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AddtoCalendarShare"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"bundleIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.addtocalendar.rn.share-extension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"entitlements"&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;"com.apple.security.application-groups"&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="s2"&gt;"group.com.addtocalendar.rn"&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;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;The landmine: if the plugin &lt;em&gt;also&lt;/em&gt; injects an &lt;code&gt;appExtensions&lt;/code&gt; entry (the default), EAS sees a duplicate and the build fails at the &lt;strong&gt;"Read app config"&lt;/strong&gt; step — before compiling anything, with an error that says nothing about share extensions. &lt;code&gt;disableExperimental: true&lt;/code&gt; plus exactly one hand-written &lt;code&gt;appExtensions&lt;/code&gt; block was the stable combination. Once it works, commit it and never touch it.&lt;/p&gt;

&lt;p&gt;Also budget a second provisioning profile and bundle ID (&lt;code&gt;.share-extension&lt;/code&gt;) — EAS handles the signing, but App Store Connect will show the extension as its own "app" in some screens, which is normal and alarming in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;patch-package&lt;/code&gt; is not a code smell on mobile — it's a release valve
&lt;/h2&gt;

&lt;p&gt;Two weeks after launch, users shipped me a bug: sharing a screenshot &lt;strong&gt;from the iOS markup editor&lt;/strong&gt; (the pencil icon after you take a screenshot) crashed the share extension. The fix existed upstream in &lt;code&gt;expo-share-intent&lt;/code&gt; v4 — but v4 required Expo SDK 53, and I was on 52 with a working, submitted build.&lt;/p&gt;

&lt;p&gt;Upgrading an entire SDK to ship a five-line fix is a bad trade the week after launch. Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# hand-edit node_modules/expo-share-intent/…&lt;/span&gt;
npx patch-package expo-share-intent
git add patches/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;patch-package&lt;/code&gt; re-applies the diff on every &lt;code&gt;npm install&lt;/code&gt; (via &lt;code&gt;postinstall&lt;/code&gt;). The backported fix shipped as v1.0.2 days later, and the SDK upgrade stayed a calm, separate decision. On web I'd call vendoring a dependency a smell; on mobile, where a "dependency upgrade" can mean re-testing every native module, it's how you ship a bug fix this week instead of next month.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your App Store listing should live in git
&lt;/h2&gt;

&lt;p&gt;App Store Connect's web forms are where listing quality goes to die — you can't diff them, review them, or restore them. EAS Metadata fixes this and almost nobody uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eas metadata:pull    &lt;span class="c"&gt;# download the live listing into store.config.json&lt;/span&gt;
&lt;span class="c"&gt;# edit store.config.json: description, keywords, release notes, screenshot sets&lt;/span&gt;
eas metadata:push    &lt;span class="c"&gt;# upload — creates the new version's metadata in ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;store.config.json&lt;/code&gt; and screenshots are committed next to the code. Release notes are written in the same PR as the feature. When I pulled the live listing for the first time, I discovered the description had &lt;strong&gt;corrupted line breaks&lt;/strong&gt; from an earlier copy-paste into the web form — it had been live like that for weeks. Listing-as-code caught what eyeballing the form never did.&lt;/p&gt;

&lt;p&gt;Two caveats so you don't over-trust it: &lt;code&gt;metadata:push&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; attach a build or press "Submit for Review" — those final clicks are still manual (or fastlane/ASC API if you want to script everything).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The review prompt: where you call it matters more than that you call it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;expo-store-review&lt;/code&gt; wraps &lt;code&gt;SKStoreReviewController&lt;/code&gt;. Every guide says "throttle it." Fewer mention the two things that actually bit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It silently does nothing on TestFlight.&lt;/strong&gt; By design, Apple suppresses the dialog there. If you're smoke-testing "does my rating prompt fire?", you must test a release build on a simulator/device &lt;em&gt;outside&lt;/em&gt; TestFlight, or you'll ship believing it's broken (or worse, that it's not, when it is).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It will collide with your own dialogs.&lt;/strong&gt; My prompt fires after a successful "event added" alert. Fire both and iOS will drop one on the floor. The fix is sequencing, not timing hacks — ask only after the user dismisses your alert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Added&lt;/span&gt;&lt;span class="dl"&gt;'&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" added to your calendar.`&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="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onPress&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;recordSuccessfulAddAndMaybeAskForReview&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;p&gt;The throttle itself: require ≥2 successful adds (proven value), ask at most once per app version, persist both in AsyncStorage, and never let the review path throw into your core flow. Apple additionally caps the system prompt at ~3 shows per year — treat &lt;code&gt;requestReview()&lt;/code&gt; as a suggestion, not a call you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Multiline &lt;code&gt;TextInput&lt;/code&gt; + a submit button = a keyboard you can't dismiss
&lt;/h2&gt;

&lt;p&gt;A classic that every RN form eventually hits: a multiline input has no "done" affordance, the keyboard covers your primary button, and iOS users — who expect tap-outside-to-dismiss from native apps — get stuck. Three props, all needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt;
  &lt;span class="na"&gt;multiline&lt;/span&gt;
  &lt;span class="na"&gt;returnKeyType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"done"&lt;/span&gt;        &lt;span class="c1"&gt;// Return key says Done…&lt;/span&gt;
  &lt;span class="na"&gt;submitBehavior&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"blurAndSubmit"&lt;/span&gt;  &lt;span class="c1"&gt;// …and dismisses instead of inserting \n&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// on the enclosing ScrollView:&lt;/span&gt;
&lt;span class="nx"&gt;keyboardDismissMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;on-drag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;// scrolling tucks the keyboard away&lt;/span&gt;
&lt;span class="nx"&gt;keyboardShouldPersistTaps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;handled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is honest: Return no longer inserts newlines. For a paste-mostly input that's right; pasted text keeps its line breaks anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Dark mode will look wrong in exactly one place: your modal sheets
&lt;/h2&gt;

&lt;p&gt;If you build semantic colors (&lt;code&gt;label&lt;/code&gt;, &lt;code&gt;separator&lt;/code&gt;, grouped backgrounds) into a theme hook from day one, dark mode is nearly free — except for modals. iOS grouped backgrounds are &lt;strong&gt;pure black&lt;/strong&gt; in dark mode, and a modal sheet rendered in pure black on a pure-black screen has no visible edge. Native apps solve this with &lt;em&gt;elevated&lt;/em&gt; variants; do the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in the modal screen only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&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;baseTheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;groupedBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elevatedGroupedBackground&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;elevatedCard&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;One remap at the top of the modal, and the sheet reads as a sheet again.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Privacy architecture is an App Store feature, not a legal chore
&lt;/h2&gt;

&lt;p&gt;The app has a bring-your-own-OpenAI-key mode. Two implementation decisions made App Review painless and became the listing's selling point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key lives in the iOS Keychain via &lt;code&gt;expo-secure-store&lt;/code&gt;, is sent &lt;strong&gt;only&lt;/strong&gt; to &lt;code&gt;api.openai.com&lt;/code&gt;, and never touches my server. That sentence goes verbatim into the privacy labels, the reviewer notes, and the description — and it's true, which is what makes Guideline 2.3 (accurate metadata) easy.&lt;/li&gt;
&lt;li&gt;Account deletion is in-app (Apple requires it if you have accounts at all — budget for it &lt;em&gt;before&lt;/em&gt; your first submission, it's a server-side feature, not a button).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more pre-flight item that surprises people: &lt;code&gt;ITSAppUsesNonExemptEncryption: false&lt;/code&gt; in &lt;code&gt;Info.plist&lt;/code&gt; saves you an export-compliance question on every single upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring meta-lesson
&lt;/h2&gt;

&lt;p&gt;Every one of these was discoverable only by shipping. The gap between "runs in Expo Go" and "approved on the App Store" is real but bounded — roughly: one build-config fight (share extension), one process discovery (metadata as code), and a handful of platform behaviors (review prompt, keyboard, dark mode) that each cost an afternoon.&lt;/p&gt;

&lt;p&gt;If you're one &lt;code&gt;eas submit&lt;/code&gt; away and hesitating: the reviewer is not your enemy, the checklist is finite, and the second submission takes a tenth the time of the first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The app: &lt;a href="https://apps.apple.com/app/id6772644308" rel="noopener noreferrer"&gt;Add to Calendar: AI Events&lt;/a&gt; — screenshots/text → calendar events. Source: &lt;a href="https://github.com/ryoshumei/add-to-calendar-rn" rel="noopener noreferrer"&gt;github.com/ryoshumei/add-to-calendar-rn&lt;/a&gt;. Questions about any of these landmines welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>ios</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
