●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Retiring Dormant SDKs with Claude Code — A 12-Year Indie Developer's Pipeline for Safely Auditing Inactive Dependencies
Twelve years of indie iOS/Android development leaves a quiet sediment of SDKs that nobody calls anymore — but the dependencies remain. Here is the Claude Code pipeline I built to audit and safely retire them across four wallpaper apps, with the actual code and four weeks of operational metrics.
"This app still has that A/B testing SDK from 2018 in its Podfile" — when Claude Code surfaced that in a session, my hands stopped. The app has been running since 2014. The version of me from back then evaluated and adopted that SDK on purpose. But somewhere along the way I stopped opening its dashboard, and the only thing left was the small, recurring cost of keeping up with its major version bumps on every release.
When you stay with indie development for twelve years, this kind of "dormant SDK" sediment is unavoidable. Ad networks, analytics, push notifications, A/B testing frameworks, crash reporters, remote config — each one had a reason at the time of adoption. The reason is gone now, but the SDK lives on inside Podfile, Package.swift, and build.gradle.
I run four wallpaper apps, and their cumulative downloads have passed 50 million. Each app carries a few dozen SDKs, which makes more than a hundred dependencies in total to maintain. Auditing that by hand is no longer realistic. So I asked Claude Code to run a decommissioning pipeline against all four apps for about four weeks, and the result was a 18.3% binary size reduction overall. Cold builds also got 31 seconds faster on average, and I no longer had to refresh the Privacy Manifest for any of the four apps.
This article documents the "dormant audit → safe removal → regression detection" workflow I converged on during those four weeks, with the actual code and the operational numbers. If you are also fighting a twelve-year sediment of dependencies as an indie developer, I hope it is useful.
Why "unused" SDKs keep costing you every month
The cost of dormant SDKs sits in places that are easy to miss. Across four weeks of observation, I categorized it into four buckets.
Binary size: 200KB–2.5MB per SDK. Four apps × five dormant SDKs on average added up to 12–60MB of wasted bytes
Cold build time: Each SDK adds 3–8s to a clean Xcode build through dependency resolution and initialization. Every wait pulls me out of the flow Claude Code is keeping me in
Privacy Manifest / Data Safety: Since May 2024, Apple recursively aggregates the Privacy Manifests of your dependencies. Data collection declarations from SDKs you do not even call anymore stay in your manifest and drag your review through them
Security alerts: GitHub Dependabot and Snyk keep firing CVE alerts for SDKs you never call. The signal-to-noise ratio of "which PR actually matters" gets quietly destroyed
The third one is especially uncomfortable for me. Since 2019, when I watched a ring of light over Kichijoji station and started making visual work, I have been treating the privacy hygiene of my wallpaper apps as part of the integrity I expect from my own art. Carrying data-collection declarations from SDKs I no longer use feels like a small lie against that integrity.
A three-axis score for "removable" vs "not removable" SDKs
The difficulty of removal does not show up in the manifest. pod 'XyzSDK' in Podfile could mean the SDK is never called at all, or that one file initializes it and never touches the API. Conversely, a single file may import an SDK through a Bridging Header in a way that no naive grep will catch.
Before letting Claude judge anything, I defined three axes that can be scored mechanically.
Reach Score: how reachable the module is from the current entrypoints, traced through the AST. 0–100
Symbol Density: density of the SDK's public symbols in the source tree. A rough grep gets you most of the way
Bridge Risk: presence of paths that static analysis cannot see — Objective-C Bridging Header, Kotlin/Java interop, method swizzling, KVC/KVO, reflection, JNI
With these three axes, an SDK with Reach=0, Density=0.0001, Bridge=Low is almost certainly removable, while Reach=0, Density=0, Bridge=High is dangerous and needs dynamic verification before removal. Without this pre-bucketing, asking Claude to "evaluate every SDK" leads to Claude flagging everything as risky, and you end up removing nothing.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A complete, copy-and-run set of scripts for scanning dependencies across iOS (CocoaPods/SPM) and Android (Gradle), scoring removal risk, and auto-generating decommissioning PRs
✦A hybrid AST + grep + symbol-resolution design for letting Claude judge what can and cannot be removed, with a measured 80% reduction in false positives compared to naive grep
✦Four weeks of operational results across four wallpaper apps: 18.3% binary size reduction, 31s faster cold builds on average, and complete removal of three discontinued ad SDKs
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Step 1: A cross-manifest scanner for iOS and Android
The first step is to gather every dependency manifest from the four apps and build a frequency map of SDKs. The script below reads Podfile.lock, Package.resolved, build.gradle, and settings.gradle and aggregates SDK name, version, and the number of projects each one appears in.
# tools/scan_dependencies.pyimport json, refrom pathlib import Pathfrom collections import defaultdictPROJECT_ROOTS = [ Path("~/repos/wallpaper-app-a").expanduser(), Path("~/repos/wallpaper-app-b").expanduser(), Path("~/repos/wallpaper-app-c").expanduser(), Path("~/repos/wallpaper-app-d").expanduser(),]def parse_podfile_lock(path: Path): text = path.read_text(encoding="utf-8") pods = [] for m in re.finditer(r"^\s+- ([\w\d/\-]+) \((\d[^)]*)\)", text, re.MULTILINE): pods.append((m.group(1).split("/")[0], m.group(2))) return podsdef parse_spm_resolved(path: Path): data = json.loads(path.read_text(encoding="utf-8")) pins = data.get("pins", []) or data.get("object", {}).get("pins", []) return [(p.get("identity") or p.get("package", ""), p.get("state", {}).get("version", "")) for p in pins]def parse_gradle(root: Path): deps = [] for f in root.rglob("build.gradle*"): for line in f.read_text(encoding="utf-8", errors="ignore").splitlines(): m = re.search(r"(implementation|api)\s*['\"]([\w\.\-:]+):([\w\.\-]+)['\"]", line) if m: deps.append((f"{m.group(2)}", m.group(3))) return depsdef scan(root: Path): found = [] for lock in root.rglob("Podfile.lock"): found.extend(parse_podfile_lock(lock)) for spm in root.rglob("Package.resolved"): found.extend(parse_spm_resolved(spm)) found.extend(parse_gradle(root)) return founddef main(): inventory = defaultdict(lambda: {"versions": set(), "projects": set()}) for root in PROJECT_ROOTS: for sdk, version in scan(root): inventory[sdk]["versions"].add(version) inventory[sdk]["projects"].add(root.name) rows = [] for sdk, info in sorted(inventory.items()): rows.append({ "sdk": sdk, "versions": sorted(info["versions"]), "project_count": len(info["projects"]), "projects": sorted(info["projects"]), }) Path("inventory.json").write_text(json.dumps(rows, indent=2, ensure_ascii=False)) print(f"detected {len(rows)} unique SDKs across {len(PROJECT_ROOTS)} apps")if __name__ == "__main__": main()
Run against my four apps, this surfaced 137 unique SDKs. The number was much higher than my mental model of "about 80," and seeing it for the first time was a little sobering.
Step 2: Reach Score — letting the compiler tell you what is actually called
Next, judge whether each SDK is reachable from the current entrypoints. The orthodox approach is SwiftSyntax on iOS and the Kotlin Compiler PSI on Android, but maintaining that kind of heavyweight analysis stack as an indie developer is not realistic.
Instead, I reused the Index Store that Xcode produces. The DerivedData/Index directory contains symbol reference graphs that the compiler emits during the build. Tools built on index-import can read it without you having to hand-build an AST, and that is enough to compute a Reach Score.
# tools/reach_score.sh — extract a reachability graph from Xcode's Index Storeset -euo pipefailPROJECT_ROOT="${1:?usage: reach_score.sh <project_root>}"DERIVED="$HOME/Library/Developer/Xcode/DerivedData"INDEX_DIR=$(find "$DERIVED" -maxdepth 3 -type d -name "DataStore" \ -path "*$(basename "$PROJECT_ROOT")*" | head -1)if [ -z "$INDEX_DIR" ]; then echo "Index Store not found. Build the project once in Xcode first." >&2 exit 1fi# For each SDK module, count source files that reference the public headers/modulespython3 - <<'PY'import os, json, subprocesssdks = [r["sdk"] for r in json.load(open("inventory.json"))]results = []for sdk in sdks: out = subprocess.run( ["grep", "-rIl", "-e", f"import {sdk}", "-e", f"@import {sdk}", os.environ.get("PROJECT_ROOT", ".")], capture_output=True, text=True ) refs = [l for l in out.stdout.splitlines() if l.endswith((".swift", ".m", ".mm", ".h"))] results.append({"sdk": sdk, "import_files": len(refs), "files": refs[:5]})json.dump(results, open("reach.json", "w"), indent=2, ensure_ascii=False)PY
import_files == 0 is a first cut at "Reach Score = 0". Do not stop there — always run Step 3's Bridge Risk check next. In my first week I skipped Bridge Risk and removed an SDK that was being called through an Objective-C category, and I broke the build for half a day. That was the lesson that pushed me to put the three-axis design in place.
Step 3: Bridge Risk — let Claude evaluate the paths static analysis cannot see
Bridge Risk is the part that mechanical analysis struggles with. Method swizzling, Objective-C categories, KVC/KVO, Kotlin reified inline functions, reflection, JNI — these are the areas where Claude Code earns its keep.
The trick is to narrow down the candidates before involving Claude. Throwing all 137 SDKs at it pollutes the context and the accuracy drops. I narrowed down to the 28 SDKs that came out of Step 2 with import_files == 0, then asked Claude per SDK: "does any file in this repo that contains swizzle / +load / @objc dynamic / KVC / reflection / JNI indirectly call this SDK?"
# tools/bridge_risk.shfor sdk in $(jq -r '.[] | select(.import_files == 0) | .sdk' reach.json); do echo "===== $sdk =====" rg -l --type swift -e "swizzle" -e "+ *load" -e "@objc dynamic" \ -g '!Pods/*' -g '!Carthage/*' | head -20 > /tmp/candidates.txt claude -p "Evaluate whether the following Swift files might indirectly call SDK '$sdk' through method swizzling, KVC, or reflection. Output JSON: {risk: 'Low'|'Medium'|'High', reason: '...'}" \ --allow-tool=read \ < /tmp/candidates.txtdone
Always force Claude to output JSON. Free-form natural language responses break the downstream parser too often. In my operational rule, any SDK rated Medium or higher is handed back to a human for a second look, and only Low-rated SDKs are auto-PR'd.
After four weeks, this three-stage filter narrowed the original 137 SDKs down to 14 marked as "safe to auto-generate a decommissioning PR." Not delegating every judgment to Claude — using mechanical filters to coarse-filter first, then asking Claude to interpret the remainder — is the division of labor that kept this pipeline stable.
Step 4: Auto-generating decommissioning PRs with Plan Mode
When generating decommissioning PRs, use Claude Code's Plan Mode. Letting Edit rip directly into Podfile breaks the dependency graph and silently loses Build Settings that the SDK had implicitly added.
The system prompt I use for Plan Mode looks like this.
You are preparing a PR to remove ONE unused SDK from an indie iOS/Android repository.Inputs: SDK name / list of detected reference files / paths of manifest files.Output a Plan that includes:1. Manifest lines to remove (Podfile, Package.swift, build.gradle)2. List of import statements to delete3. Build Settings / Info.plist entries that may break when removed4. Verification steps with concrete simulator flows5. Rollback procedureDo NOT apply changes. Produce the Plan only. After human approval, switch to Apply mode.
It is important to always include Build Settings and Info.plist impact in the Plan. Ad SDKs add SKAdNetworkIdentifier to Info.plist. Analytics SDKs add exceptions to App Transport Security. These side effects need to surface during planning, before you discover them post-merge.
After a decommissioning PR is merged, I run a seven-day automated monitor to confirm nothing actually broke. It runs every morning on a Cloudflare Workers Cron Trigger, fetching Crashlytics and AdMob metrics and comparing them against the seven days before removal.
My thresholds fire on "Crash-Free Users drop by more than 0.2 points" and "eCPM drop by more than 8%". I adjusted the eCPM threshold from 5% to 8% in week three so that natural ad-network removal would not generate false alarms — short-term eCPM dips after removing an ad SDK are normal, and 3–5% drift is treated as noise.
The numbers after four weeks
Here are the actual results after four weeks of running the pipeline across the four apps.
SDKs detected: 137 (total across four apps, deduped)
SDKs narrowed to removal candidates: 14
SDKs actually removed: 11 (the remaining 3 had Bridge Risk Medium and were held back)
Categories removed: 3 discontinued ad networks, 2 old A/B testing frameworks, 4 old analytics SDKs, 1 old push provider, 1 old crash reporter
Binary size reduction: 18.3% average across four apps (max 24.1%, min 9.8%)
Cold build time saved: 31 seconds average (M3 MacBook Pro / Xcode 16)
Regressions observed: one (Crash-Free Users dipped 0.18 points, below threshold, recovered naturally in three days)
What mattered more than the numbers was the disappearance of Privacy Manifest maintenance load. Three discontinued ad SDKs that Apple no longer updates would otherwise have kept showing up in the Privacy Manifest compatibility report as "unsupported" on every release. Removing them cleaned up the audit surface across all four apps, and that alone was a noticeable improvement to my own peace of mind.
Operational recommendations for indie developers
Closing out, here are a few things I would recommend if you are running this kind of pipeline as an indie developer.
Run against one app per month. Running all apps at once balloons verification cost. One app per month, with one or two decommissioning PRs each, is the pace that matched my capacity
Do not remove anything with Bridge Risk Medium or higher. Accept the limits of static analysis. Keep them on the roadmap and remove them as part of a related feature refactor
Evaluate ad SDK removal per geo/device segment. Looking only at the overall average eCPM can hide that you are degrading the experience for users in a specific region
Paste the Plan Mode output straight into the PR description. The five sections — manifest lines, imports, Build Settings, verification, rollback — exist for the version of you reviewing this six months later. Twelve years of indie development has taught me that the most important investment is making the past readable to your future self
Removing dormant SDKs is not glamorous feature work. But it is an opportunity to re-understand the insides of an app you have been running for twelve years. Because Claude Code carries the mechanical inventory, I get to focus on the part that matters — whether this SDK truly should go. I hope this is useful to anyone who has been working on the same kind of long-running side project.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.