Scaling Myself: How I Run 22 Claude Code Sessions for DS4 Migration

This article is the 15th entry in Merpay & Mercoin Tech Openness Month 2026.

In this age of AI, most of us have built our own work setups around it. This article is not a proclamation that mine is the best; other setups might work better for other people. But this is the one that works for me, and I am sharing it in case any part of it is useful to you.

I am an iOS engineer on the Mercoin (MC) Client team, and for the past several months I have been working on the migration of our screens from DesignSystem V3 (DS3) to DesignSystem V4 (DS4), our fourth-generation component library that defines how every screen in the app looks and behaves. Over the course of this migration, I have created around 140 Pull Requests (PRs), and at one point I had 18 of them open at the same time. This post is about the setup that made that sustainable rather than suffocating: git worktrees, a deliberate terminal layout across two screens, an Obsidian Vault that doubles as Claude Code’s knowledge base, and custom slash commands that encode the migration playbook so I do not have to remember it.

The problem: a PR flood

You cannot migrate from DS3 to DS4 with a single find-and-replace. As the MC team agreed, we had to separate the implementation into phases to keep each change reviewable and safe to merge independently. Each phase produces its own PR, and the pipeline looks like this:

With multiple modules going through this pipeline at the same time, the PR count grows rapidly. A single module can have four to six PRs in flight across different phases, and with several modules running in parallel, the total easily reaches twenty or more. Trying to context-switch across all those branches on a single working tree quickly becomes painful. Every branch switch means a rebuild, lost editor state, and mental overhead just to remember where things were left off.

This is the problem that led me to rebuild my environment around parallelism.

The phases are as follows: Phase 0 creates the feature flag (FF). Phase 1 copies the existing screen files into V4 variants. Phase 2 swaps DS3 components for their DS4 equivalents. Phase 2.1, which is optional, refactors the code on V4 variants. Phase 3 wires up the FF at the wireframe level so the app can switch users between the old and new implementations. Phase 4 is cleanup — remove the flag and delete the DS3 code.

The setup

The diagram below shows the full layout of my development environment, spread across two physical screens and nine macOS Desktops.

On my main screen, I keep three desktops. Two of them run brain sessions — Claude Code instances dedicated to discussion, translation, DS4 progress tracking, and writing experiential learnings. The third desktop runs two code review sessions for reviewing teammates’ PRs. None of these sessions touch migration code directly; they exist purely for thinking, planning, and reviewing.

On the secondary screen, I have six desktops with three terminal windows each, for a total of 18 worktree sessions. Each of these runs Claude Code inside a git worktree, and this is where the actual migration work happens. Every worktree session is equal — any of them can pick up any task. There is no fixed assignment; whichever worktree is free gets the next module or phase.

The split exists to separate thinking about the work from doing the work. When every terminal is an execution workstream, meta-work like improving the workflow itself, tracking progress, or writing documentation gets crowded out. Dedicated brain sessions force me to invest in the system, not just the output. Without it, running 22 sessions would tip from manageable into chaotic.

Another feature that helps sustain this across days is Claude Code’s /resume command, which restores a session’s full conversation context from the previous day. When I close my terminal at the end of the day and reopen it the next morning, /resume brings back the branch names, the progress so far, and what was planned next. This means I do not lose momentum between work sessions, and I do not have to re-explain the state of a migration to Claude each morning.

Git worktrees and shared caching

Everything in this setup rests on git worktrees. Even if your project uses a different build system or a different number of parallel sessions, the core principle applies broadly: keep multiple branches checked out simultaneously so you never have to context-switch. A worktree allows one repository to have multiple filesystem checkouts simultaneously. Unlike branch-switching, there is no rebuild and no state loss. Each worktree is its own directory with its own checked-out branch. This means 18 branches can be active at the same time, each in its own terminal, each building independently, though only one at a time, since they share a Bazel build queue (more on that below). Additionally, I have created a worktrees shell alias that displays all worktrees and their current branches at a glance. This makes it easy to find a free worktree or check the state of an in-progress migration. The output looks like this:

I follow a naming convention of worktree-X-Y, where X is the desktop number on the secondary screen and Y is the position within that desktop (top, middle, or bottom). This maps directly to the physical screen layout, so I always know where to find a given session without thinking about it.

All 18 worktrees share a single Bazel output_base through a .user.bazelrc configuration file. The first build in any worktree warms the cache for every other worktree, so subsequent builds across the board are significantly faster because they hit cached artifacts instead of rebuilding from scratch. The diagram below illustrates how this sharing works.

The tradeoff is that Bazel can only run one build at a time per output base. To handle this, I implemented a file-based lock protocol: before any build, the session claims a lock file. If another worktree holds it, the session waits and retries every 20 seconds until the lock is free. Builds effectively queue up — one runs while the others wait their turn. The session always releases the lock after the build finishes, whether it succeeds or fails.

This might sound slow, but in practice it works well. Only one worktree builds at a time, but the shared cache means each individual build is faster than it would be with isolated caches. That speed gain more than compensates for the occasional wait in the queue.

Obsidian as the single source of truth

With 22 sessions running, they all need to share the same configuration — the same rules, the same slash commands, the same knowledge base. Claude Code reads ~/.claude/CLAUDE.md and ~/.claude/commands/*.md for its global context. I wanted to manage that content in Obsidian, a note-taking application that provides a better editing experience with features like backlinks, tags, omnisearch, and a proper file tree.

The solution is filesystem symlinks. By pointing ~/.claude/CLAUDE.md and ~/.claude/commands/ at their corresponding locations inside my Obsidian Vault, I can edit everything in Obsidian’s interface and have Claude Code see the changes instantly across all 22 sessions. There is one source of truth and zero manual syncing required.

The Vault holds everything Claude Code needs to operate. CLAUDE.md contains global rules like the Bazel lock protocol and PR description conventions. The commands/ folder contains over 35 custom slash commands. The knowledge/ folder stores architecture documentation, screen-level specifications, and testing patterns. The plans/ folder tracks active migration status and findings.

Of everything in the Vault, the slash commands matter most. Each phase-specific command — from /do-ds4-migration-phase0 through /do-ds4-migration-phase4 — encodes the entire playbook for that phase: what files to create, which components to swap, and what patterns to check for. When I run /do-ds4-migration-phase2 in a worktree, Claude drives the migration while I verify the output. Updating a command in Obsidian instantly updates how every future session behaves. In this sense, the commands are the workflow.

Of course, at the beginning these custom commands had plenty of flaws: incorrect file patterns, missing edge cases, and incomplete checklists. But as I kept improving them throughout the course of the DS4 migration, they gradually matured to the point where I could run a phase command and it would handle the migration with minimal supervision.

With the workflow encoded in commands and shared across all sessions, the next challenge was managing the output of that workflow — the PRs themselves.

Managing stacked PR chains

Because migration phases are sequential, with Phase 1 building on Phase 0, Phase 2 on Phase 1, and so on, they naturally produce stacked PR chains. PR1 is based on master, PR2 is based on PR1’s branch, and PR3 is based on PR2’s. When PR1 gets merged, typically via squash-merge, every downstream PR needs rebasing. Doing this manually across three or four stacked PRs is tedious and error-prone, with risks including wrong base branches, conflict resolution mistakes, and accidentally dropped commits.

To handle this, I dedicate a single Claude Code session to own an entire chain for a given screen. When a base PR merges, I tell Claude to rebase each downstream PR in sequence and force-push with --force-with-lease. After the rebase cascade completes, I review the diffs to make sure nothing was lost, and then request reviews on the freshly rebased PRs.

The reason I keep this in one session rather than distributing it is that Claude Code’s conversation context remembers the branch names, PR numbers, and the PR chain’s order. If I split the work across sessions, I would have to re-explain the chain each time. Keeping one session mapped to one chain preserves the mental model of the full stack and reduces the chance of errors during rebasing.

Keeping the workspace clean

The parallel setup introduces its own maintenance challenge. With 18 worktrees and PRs constantly being merged, stale branches pile up fast. A worktree sitting on a merged branch is basically a blocked lane. It cannot take new work until someone manually detaches it and deletes the old branch. Making this worse, our repository uses squash-merge, which means git branch --merged does not detect them as merged, because squash-merge creates a new commit on the target branch rather than fast-forwarding, so the original branch commits are never ancestors of HEAD. Standard git tooling cannot help.

To solve this, I built a custom slash command called /prune-branches. It prunes stale remote-tracking references, finds local branches whose upstream has been deleted, cross-references them with worktrees, and checks each branch’s PR merge status via GitHub CLI. The output is two tables: branches that are safe to delete immediately, and branches that are checked out in worktrees and need detaching first.

The command is dry-run by default — running /prune-branches shows what would be cleaned up without making any changes. Running /prune-branches --delete performs the actual cleanup. I run it every morning as part of my routine. It turns the question "which worktrees are free?" from a manual investigation into a one-command answer, and keeps the workspace ready for new work instead of gradually piling up with stale branches.

The "remove and build" double-check

After migrating 21 screens across seven feature areas to DS4, I initially thought the work was done. The V4 code compiled and the screens looked correct. However, I learned the hard way that "it compiles" is not the same as "it is migrated."

The issue is subtle. During Phases 1 through 3, the V4 code sits alongside the V3 code, and the DS3 dependencies remain in the BUILD file. Everything compiles because DS3 provides certain things transitively — extensions, resource imports, and shared utilities that the V4 code unknowingly depends on. When Phase 4 arrives and removes those dependencies, the V4 code would break in ways that were invisible during earlier phases.

A teammate suggested a strategy: simulate Phase 4 locally. For each screen, one at a time, remove the DS3 dependencies from the BUILD file, delete the V3 screen files and V3-only views, build the single target, and then restore everything with git checkout -- . to discard all uncommitted changes before moving to the next screen. This restore step is essential. Without it, the DS3 removals from the previous screen would still be in effect and would contaminate the next screen’s build results. Testing one screen at a time in a clean state is critical. Building all targets together causes cascading failures from shared dependencies, which masks which screens have real issues versus which ones simply depend on a broken neighbor.

Out of 21 screens, three had hidden DS3 dependencies in their V4 code that would have broken in Phase 4. As shown in the results table above, the failures included dead references to JPYPriceFormat in shared entity files like AmountOption.swift, V3 atom references in NavigatorExtensions.swift, and dead atoms like MainColorAtom that were no longer needed but still imported. Claude Code made this verification practical — running 21 sequential modify-build-restore cycles took about a minute per screen, compared to the much longer manual process it would have required otherwise.

Conclusion

This setup took about two months of iteration to reach its current form. The worktree conventions, the slash commands, the lock protocol, the Obsidian structure: none of it was designed upfront. Each piece was added when the pain of not having it became obvious. However, the compounding payoff has been enormous. Each new module that goes through the pipeline is faster than the last, because the workflow is encoded in the commands rather than in my head.

Running 22 sessions does require a well-equipped development machine with sufficient memory — lighter machines would benefit from fewer sessions. Switching from Cursor to VS Code also helped reduce overhead, since Cursor bundles its own AI runtime on top of VS Code, which is unnecessary when Claude Code runs in the terminal. Naming discipline turned out to be important as well; drifting from the X-Y worktree convention quickly breaks the spatial mental map. And while symlinks make the single-source-of-truth model possible, they are fragile: if the Vault path changes, they break. I keep my Obsidian Vault backed up on Google Drive, which mitigates this, but the symlinks themselves still need to point to the right place.

The takeaway from this experience is that for agentic development, we should invest in building the workflow before the workload arrives. Git worktrees, a single-source knowledge base, and a deliberate terminal layout can turn what could be an overwhelming pile of PRs into a manageable pipeline. The investment in tooling pays for itself many times over.

The next article is by anzai. Stay tuned!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加