Hunting iOS Memory Leaks: S1E1
Welcome to the first edition of Hunting Memory Leaks.
Another wild week, mostly battling with XCTestLeaks, the tool I've been building on the side. I'll release it once I'm confident the workflow holds up across at least 5 repos.
For context, I've written 4 case studies on iOS memory leaks (part 1, part 2, part 3, part 4) that's where this series picks up.
During my blog series, I ran XCTestLeaks against Firefox-iOS and got 3 fixes upstream 🎉. This week, I wanted to stress-test it somewhere not-a-browser:
Turns out they have a few. I fixed two this week: HTMLProcessor and GutenbergCoverUploadProcessor.
What's Available so far
Before we get into how I found and fixed them, here's where XCTestLeaks is today and how it works:
xctestleaks agent run \
--workspace "./WordPress.xcworkspace" \
--scheme "WordPress"
--html-outputWhat this does:
- Runs your unit test scheme,
- After each test it invokes leaks(1) to check for retain cycles.
- At the end, you get an HTML report, per leak traces.

Setting myself up!
The tool does the job but it still has a long way to go. I'll admit, none of this would have been possible without all the love I've been getting from Claude Code.
I've been feeling like Professor X, fostering a small team of agents on my machine, just to get into a position where the wild expectation of "catch leaks across OSS repos every week (and maybe raise PRs)" is even achievable.
To pull that off, you have to seriously step up the automation game. After the launch announcement earlier this week, I took a step back and asked myself what I actually needed:
- GitHub Actions Workflow: This runs XCTestLeaks against any OSS repo on demand.
- Claude skill: Uses the audit artifacts from XCTestLeaks to verify a leak (🔴 red), apply a fix, and re-verify (🟢 green).
- Issues and PRs (optional): Filed upstream once a fix is confirmed.
I made the classic AI-pilled mistake, try to automate everything in one shot. My first stab was a single universal GHA workflow that would work against any OSS repo. Far-fetched.
Every OSS project has its own quirks, some pin to a specific Xcode version, others to a Swift toolchain, others have weird pre-build setup steps. Trying to encode all of that into one workflow would be a nightmare. Learning:
Humans are underrated in today’s AI world. The best systems are not the most automated ones, but the ones that keep the workflow simple.
Simplest thing here I did was to have one workflow for each project, and thats it worked:
- name: WordPress setup
working-directory: target
run: |
set -euxo pipefail
brew tap FelixHerrmann/tap
brew install swift-package-list imagemagick ghostscript
bundle exec rake dependencies
cp WordPress/Credentials/Secrets-example.swift WordPress/Credentials/Secrets.swift
- name: Run xctestleaks agent
continue-on-error: true
run: |
./build/install/xctestleaks/bin/xctestleaks agent run \
--workspace "$GITHUB_WORKSPACE/target/WordPress.xcworkspace" \
--scheme WordPress \
--output-dir "$GITHUB_WORKSPACE/xctestleaks-artifacts"
--html-outputSo we've got the workflow. The next piece I needed was a Claude skill to drive the red-green loop on top of it, and the user prompt had to be dead simple, something like:
Hey Claude, fix these two leaks xctestleaks is flagging CachedAnnouncementsStore and WordPressFlux.Dispatcher<()> from Wordpress-iOS project.
Time for Action!
Ran the leak found the HTML report, here you go:

Then I let the skill take it from there. Here's what the conversation actually looked like with Claude Code:
Me: Hey Claude, fix these two leaks XCTestLeaks is flagging — CachedAnnouncementsStore and WordPressFlux.Dispatcher<()>. Separately please.
Claude: Starting with CachedAnnouncementsStore. Found the test, ran it in isolation… 0 leaks. Pre-verify says it doesn't reproduce — likely a flake or test-pollution artifact. Same story for the Dispatcher one in
AppIconListViewModelTests. Both come back clean alone.
Me: How'd you narrow down the culprits?
Claude: Two options — static grep for the cycle shape, or dynamic bisection on the test order from the audit's logs.
Me: I like the dynamic one. Please do that and tell me the test name to run for both.
Claude: Pulled the execution order from leaks_raw.log (860 tests). The Dispatcher<()> first surfaces at test #267 — the very test that was flagged. Nothing earlier showed it. So I tried running the full test class (4 tests) instead of one. Both leaks reproduced. Bisected down: pairs were clean, trio surfaced the dispatcher, full class surfaced both. Run WordPressTests/AnnouncementsDataStoreTests. Isolation hid it because XCTest holds test instances longer when more tests are queued.
Me: Wait so it's all leaks in one test file?
Claude: Yes — same bug, four times. Every test does subscription = store.onChange { … store.x … }, captures store strongly into the closure the dispatcher retains, no tearDown. The two leak names are just two roots of the same cycle.
Me: OK do it but keep it staged. Can you also try fixing this HTMLProcessor leak?
Claude: Done. Added tearDown { subscription = nil } — 4 leaks → 0, staged. On to HTMLProcessor. This one's production code-GutenbergVideoUploadProcessor has three lazy var closures capturing self strongly, classic 2-node cycle. [weak self] in all three → 1 leak → 0, staged.
Me: Show me the diff for GutenbergVideoUploadProcessor — why was it a cycle?
Claude: GutenbergVideoUploadProcessor strongly owns videoHtmlProcessor (lazy stored property). HTMLProcessor stores the replacer: closure. The closure body references self.remoteURLString — Swift captures self strongly by default. Two strong arrows pointing at each other, ARC can't collect either side. [weak self] breaks the back-arrow; the chain unwinds normally.What's next?
- Right now XCTestLeaks flags anything that leaks, including inside the test files themselves.Often these are intentional or noise, and there's no clean way to ignore them. Adding per-test/per-class ignore directives is high on the list.
- I'd like to fix the Xcode 26.4 leaks(1) regression itself.
- Benchmark the overhead: how long the suite runs natively vs. with XCTestLeaks attached, so people know what they're signing up for.
Finally 🎉
And our goals have being achieved for this newsletter: One PR is already merged into WordPress-iOS: 570a229. Another is in review:
Enjoyed the read? Share it with your friends.
Subscribe to Memory Leak Hunt here. Reach out on LinkedIn or X if you know any iOS OSS repo that I should audit next week. If your team works on iOS performance, memory, or reliability, I’d love to chat.