A family member called with the classic iPhone complaints: storage full, battery draining fast, and no idea why. They’d installed apps over the years, used them a few times, then forgot about them. “Why did I even install this?” The problem wasn’t finding apps to delete—it was figuring out which apps to delete. iOS Settings shows you a list sorted by size, but that’s it. No install dates. No “last updated 3 years ago” warnings. No way to see which apps are abandoned.
I thought: this can’t be that hard. Why doesn’t this already exist?
This is iPhone Explorer—a Node.js web app that connects to iPhones via USB, lists every installed app with App Store metadata, calculates privacy concern scores, and helps identify the apps worth keeping. One of my first “vibe coded” complete applications—not a script or code snippet, but a full working app built in a day because someone needed it and the tool didn’t exist.
Why Did We Do This?
The problem: Someone needed to clean up their iPhone but had no good way to decide what to delete. You install an app, use it twice, forget about it. Two years later you’re scrolling through Settings wondering “what even is this?” with no context to make a decision.
Existing solutions didn’t fit:
- iOS Settings → General → iPhone Storage: Shows apps sorted by size with storage breakdown. Useful for freeing space, but no ratings, no update dates, no privacy info.
- App Store privacy labels: Available on each app’s product page—but you have to check each app individually. With 200+ apps, that’s not realistic.
- Third-party iOS management apps: Mostly focused on file transfer and backups, not app analysis.
The idea: Connect to the iPhone directly, pull the complete app list, enrich it with App Store metadata, and present it in a searchable web UI with filters that actually help decide what to keep.
I had no idea how to talk to an iPhone from a computer. That’s where the coding agent earned its keep—Claude found libimobiledevice, an open-source library that implements Apple’s proprietary protocols. The same tools jailbreak communities use for legitimate device communication. I’d never heard of it before this project.
What Problems Needed Solving?
Device Communication
macOS doesn’t expose iPhone app lists through any public API. I described the problem to Claude: “I need to get a list of installed apps from an iPhone connected via USB.” The agent came back with libimobiledevice—an open-source library I’d never heard of that implements Apple’s proprietary protocols. Specifically:
idevice_idto detect connected devicesidevicepairto handle trust/pairingideviceinstallerto list installed apps with bundle IDs
The catch: these are CLI tools, not a Node.js library. So I needed to spawn processes and parse their output. But the agent found the right tool for the job—that’s the vibe coding workflow working as intended.
App Store Metadata
Bundle IDs like com.spotify.client aren’t human-readable. Needed to:
- Look up app names, icons, ratings from the App Store
- Get version history and last update dates
- Handle apps that aren’t on the App Store (enterprise apps, TestFlight)
Apple’s iTunes Search API handles most lookups, but it rate-limits aggressively.
Privacy Label Scraping
Once the app list worked, an obvious question emerged: “I installed this app years ago—is it collecting data I didn’t realize?” The App Store shows privacy labels for each app, but checking 150 apps manually? No chance.
Apple added “App Privacy” labels in iOS 14, but there’s no API. The data only exists on App Store web pages. So the natural extension:
- Scrape the App Store web pages for privacy sections
- Parse the data collection categories (location, contacts, health, etc.)
- Calculate a “concern score” based on what’s collected and how it’s used
Bad privacy = bad app, in my opinion. This feature turned a “what can I delete” tool into a “what should I delete” tool.
Performance at Scale
With 200+ apps, fetching metadata and scraping privacy labels for each would take forever sequentially. Needed:
- Batch processing with parallelism
- Multi-level caching (memory + disk)
- Request deduplication to avoid hitting the same endpoint twice
The Plan
Here’s the plan I gave Claude. One-day sprint, structured as a Claude Code implementation plan.
Plan: iphone-explorer.md
Implementation Plan: iPhone Explorer Web App
Overview
Build a web app that connects to iPhone via USB, lists installed apps with full App Store metadata, analyzes privacy practices, and provides tools for identifying problematic apps.
Goals
- Connect to iPhone via USB using libimobiledevice CLI tools
- List all apps with bundle IDs and installation info
- Enrich with metadata from App Store (name, icon, rating, updates)
- Analyze privacy by scraping App Store privacy labels
- Identify problems (outdated apps, poor ratings, privacy concerns)
- Export for analysis in JSON and LLM-friendly text formats
Architecture Decisions
Backend: Node.js + Express
- Node.js for async I/O (child_process spawning, HTTP requests)
- Express for REST API
- Socket.io for real-time device status updates
Device Connection: libimobiledevice CLI
- CLI wrapper approach over native bindings: More stable, easier to debug
- Polling for device detection (no native USB events in Node.js)
- Event-driven status updates via WebSocket
Metadata: Multi-source Enrichment
- iTunes Search API for basic metadata (name, icon, rating)
- App Store web scraping for privacy labels
- Graceful degradation when sources fail
Caching: Two-Level Strategy
- Memory cache for hot data (current session)
- Disk cache with TTL for persistence (7 days for metadata, 24 hours for privacy)
- Request deduplication to prevent parallel identical requests
Implementation Tasks
Task 1: Project Setup
- Initialize Node.js project with Express
- Configure Socket.io for WebSocket communication
- Set up Bootstrap 5 frontend with responsive layout
Files:
package.json: Dependencies (express, socket.io, axios, cheerio)server.js: Express app entry pointpublic/index.html: Web UI shell
Verification:
npm start
# Check: Server runs on port 3000, UI loads
Task 2: Device Connection Module
- Implement device detection via
idevice_id - Handle pairing status via
idevicepair - Poll for connection status every 2 seconds
- Emit WebSocket events on connect/disconnect
Files:
src/device-manager.js: Device detection and pairingsrc/idevice-wrapper.js: CLI command wrappers
Key Commands:
idevice_id -l # List connected devices
idevicepair pair # Initiate pairing
idevicepair validate # Check pair status
ideviceinstaller -l # List installed apps
Verification: Connect iPhone, UI shows “Connected” status
Task 3: App List Retrieval
- Parse
ideviceinstaller -loutput for bundle IDs - Extract app types (user, system, hidden)
- Deduplicate and normalize bundle IDs
- Return structured app list
Files:
src/app-list-service.js: App list parsingsrc/models/app-info.js: App data model
Output format:
{
bundleId: "com.spotify.client",
appType: "user",
version: "8.9.2",
installDate: "2024-01-15"
}
Verification: App list displays in UI with bundle IDs
Task 4: App Store Metadata Service
- Batch lookups via iTunes Search API
- Rate limiting: max 5 concurrent requests
- Exponential backoff on 429 errors (1s → 2s → 4s)
- Cache responses to disk with 7-day TTL
Files:
src/app-store-service.js: iTunes API integrationsrc/metadata-manager.js: Caching and batchingsrc/cache/disk-cache.js: File-based cache
API endpoint:
https://itunes.apple.com/lookup?bundleId={bundleId}&country=us
Verification: Apps display names, icons, and ratings
Task 5: Privacy Label Scraper
- Scrape App Store web pages for privacy sections
- Parse data collection categories (17 types)
- Identify data linking and tracking practices
- Calculate concern score (0-100)
Files:
src/privacy-scraper.js: Web scraping with Cheeriosrc/privacy-analyzer.js: Score calculationsrc/models/privacy-data.js: Privacy data model
Categories tracked:
- Contact Info, Health & Fitness, Financial Info
- Location, Contacts, User Content
- Browsing History, Search History
- Identifiers, Usage Data, Diagnostics
- Purchases, Sensitive Info
Scoring formula:
- Base: Number of data types collected
- Multipliers: Data linked to identity (2x), used for tracking (3x)
- Flags: Sensitive categories (health, financial) add extra weight
Verification: Privacy tab shows data collection breakdown
Task 6: Problem Detection
- “Worst Apps” filter: Low rating (<3.0) + no update in 6+ months
- “Privacy Concerns” filter: High concern score (>70)
- Sorting by update date to spot abandoned apps
Files:
src/filters/problem-detector.js: Filter implementationssrc/models/filter-result.js: Filter output model
Verification: Filter buttons return relevant app subsets
Task 7: Export Functionality
- JSON export with full metadata
- LLM-friendly text format (markdown tables, structured summaries)
- Copy-to-clipboard for quick sharing
- Download as file
Files:
src/exporters/json-exporter.js: JSON serializationsrc/exporters/llm-exporter.js: Text formatting
LLM export format:
## App Analysis: [Device Name]
Generated: 2025-05-26
### High Privacy Concern Apps (Score > 70)
| App | Score | Data Types | Notes |
|-----|-------|------------|-------|
| Facebook | 95 | 15 | Tracks across apps |
...
Verification: Export buttons produce valid outputs
Task 8: Web UI Polish
- Real-time connection status indicator
- Search with instant filtering
- Sort by name, rating, privacy score, last update
- Responsive design for desktop and tablet
Files:
public/index.html: Main layoutpublic/css/styles.css: Custom stylingpublic/js/app.js: Client-side logic
Verification: Full UI walkthrough on desktop and iPad
Critical Files
iphone-explorer/
├── package.json
├── server.js # Express + Socket.io setup
├── src/
│ ├── device-manager.js # USB connection handling
│ ├── idevice-wrapper.js # CLI command wrappers
│ ├── app-list-service.js # App list parsing
│ ├── app-store-service.js # iTunes API integration
│ ├── metadata-manager.js # Caching + batching
│ ├── privacy-scraper.js # Privacy label scraping
│ ├── privacy-analyzer.js # Concern score calculation
│ ├── cache/
│ │ └── disk-cache.js # File-based caching
│ ├── filters/
│ │ └── problem-detector.js # Worst/outdated/privacy filters
│ ├── exporters/
│ │ ├── json-exporter.js
│ │ └── llm-exporter.js
│ └── models/
│ ├── app-info.js
│ ├── privacy-data.js
│ └── filter-result.js
└── public/
├── index.html
├── css/styles.css
└── js/app.js
Verification
Local Testing
npm install
npm start
# Open http://localhost:3000
# Connect iPhone via USB
# Trust computer on iPhone when prompted
# Click "Refresh" to load app list
Metadata Test
- Connect iPhone
- Click “Refresh Apps”
- Wait for metadata enrichment (watch progress bar)
- Verify: Apps show names, icons, ratings (not just bundle IDs)
Privacy Test
- Click “Load Privacy Data”
- Wait for scraping (slower due to web requests)
- Verify: Privacy tab shows data categories
- Filter by “High Concern” - should show data-hungry apps
Export Test
- Load app list with metadata
- Click “Export JSON” - verify valid JSON file downloads
- Click “Export for LLM” - verify readable markdown
Trade-offs
CLI Spawning vs. Native Bindings
- Chose CLI spawning via child_process
- Trade-off: Slower, requires parsing text output
- Benefit: More stable, libimobiledevice native bindings are outdated
Web Scraping vs. No Privacy Data
- Chose web scraping for privacy labels
- Trade-off: Fragile (Apple can change HTML), slow, may hit rate limits
- Benefit: Only way to get privacy data programmatically
Disk Cache vs. Database
- Chose disk cache (JSON files) over SQLite
- Trade-off: No queries, slower for large datasets
- Benefit: Zero dependencies, human-readable cache files
Concern Score vs. Raw Data
- Chose calculated score for quick filtering
- Trade-off: Opinionated, may not match user’s priorities
- Benefit: Easy “worst offenders” identification
How Development Actually Went
The implementation plan above looks clean. Reality had more hiccups.
This was one of my first “vibe coded” complete applications—not a script or code snippet, but a full app with a web UI, real-time updates, caching, error handling. Before this, I’d built tools that did one thing in the terminal. This was different: an actual application someone else could use without me standing over their shoulder explaining things.
The rate limiting problem: About 30 apps into the metadata fetch, Apple started returning 429s. The iTunes API doesn’t document rate limits, so I had to figure them out empirically. Solution: batch processing (5 apps at a time), exponential backoff (1s delay after first 429, then 2s, then 4s), and aggressive caching so I never request the same app twice.
Privacy scraping fragility: The App Store web pages use client-side rendering. Initial scraping with Cheerio got empty privacy sections. Had to look at the page source more carefully—turns out the privacy data is in a JSON blob embedded in a script tag, not rendered HTML. Much easier to parse once I found it.
Device detection timing: The polling loop for device detection initially ran every 5 seconds. Too slow—you’d plug in your phone and wait, staring at “No device connected.” Dropped it to 2 seconds. Feels responsive now.
The satisfying moment was watching 200+ apps stream into the UI with names, icons, ratings, and privacy scores appearing progressively. That’s when it felt real—not a script, but an actual tool someone could use.
Fun Challenges Encountered
The 429 Dance
First run through my app list, everything worked until about app #30. Then every request failed with HTTP 429. Apple’s rate limiting kicked in hard.
The fix wasn’t just adding delays—the agent had to implement batch processing (5 apps at a time), exponential backoff (wait longer after each failure), and aggressive caching so we never request the same app twice. I described what was happening, Claude figured out the solution.
Lesson: Apple’s APIs are hostile to bulk operations. Cache aggressively, batch conservatively, back off gracefully.
Finding the Privacy JSON
Initial privacy scraping returned empty results. The App Store page showed privacy labels in the browser, but the scraper saw nothing in those DOM locations.
Turns out the privacy data wasn’t rendered HTML—it was embedded in a script tag as JSON that the page hydrates on load. Once Claude found where the data actually lived, parsing it was straightforward.
Lesson: Modern web pages often embed data in JSON blobs for client-side rendering. When scraping, look for data islands, not just rendered content.
Device Trust Timing
First-time iPhone connections require trusting the computer. The flow: plug in phone → phone shows “Trust this computer?” → user taps Trust → device becomes accessible.
Problem: the app detected the device immediately, tried to list apps, and failed because the trust prompt was still pending. Had to add a pairing validation step that waits for the user to tap Trust before proceeding.
Lesson: USB device detection and authorization are separate steps. Don’t assume connection means access.
The “Unknown App” Problem
About 10% of apps returned no metadata from the iTunes API. Turns out:
- Some apps were removed from the App Store but still installed
- Some were enterprise distribution (no public listing)
- Some were regional mismatches
- One was a renamed app with a new bundle ID
For these, the app shows “Unknown - [bundle ID]” with a note about why metadata might be missing. Not every app has a story Apple will tell you.
Results
Build time: One day (May 26, 2025)
Scale: Successfully analyzed 242 apps with full metadata and privacy scores for most of them. About 10% were enterprise apps, removed apps, or regional edge cases where metadata wasn’t available.
What I discovered:
- Privacy surprises: Several “simple” utility apps collected more data than expected. The privacy scores made it easy to spot the outliers.
- Forgotten apps: Plenty of apps I didn’t remember installing or hadn’t used in ages. Sorting by update date surfaced the abandoned ones.
- The “why do I have this?” moment: That’s really what the tool is for—seeing everything in one place with enough context to decide what to keep.
The payoff: The family member who started all this? They exported their app list and pasted it into ChatGPT to help decide what to keep and what to delete. I don’t know what got deleted—I provided a tool, they used it. That’s the best outcome: built something someone actually needed, they used it their own way.
Still using it: I run it every few months to audit my own apps. The LLM export turned out to be the killer feature—paste your app list into Claude or ChatGPT and ask “which of these apps should I reconsider keeping?”
Limitations:
- Requires USB connection (can’t run remotely)
- Privacy scraping can break if Apple changes their page structure
- No iOS usage data (can’t tell when you last opened an app)
- macOS only (libimobiledevice works on Linux too, but I only tested Mac)
What’s next: Nothing. It works for my use case. If Apple exposes privacy labels via an API someday, I might update the scraper. But for now, it’s a solved problem.
Tech stack: Node.js, Express, Socket.io, Cheerio, libimobiledevice CLI Build time: 1 day Lines of code: ~4,500 across 18 modules