March 2025 · 12 min read
How We Reverse-Engineered a Dead Mac App in 48 Hours with AI
The original Copied was a beloved clipboard manager for macOS. Then its developer stopped maintaining it, macOS updates broke it, and thousands of users lost a tool they depended on daily. We rebuilt it from scratch using Claude Code — not just the code, but the architecture, debugging, CI/CD, and project management. Here's how.
The Problem
If you used a Mac between 2015 and 2020, you might remember Copied. It was a clipboard manager that felt native — a menu bar popover that tracked everything you copied, organized it into lists, and synced across devices via iCloud. It wasn't flashy, but it was the kind of utility that became invisible infrastructure in your workflow.
Then the developer went silent. No updates, no responses to support emails, no acknowledgement that macOS Ventura had broken everything. The app icon still sat in your menu bar, but it was a ghost — crashing on launch, losing data, unable to sync.
The alternatives weren't great. Some were Electron apps that used 200MB of RAM. Others were subscription-based. None of them matched Copied's specific workflow: the popover UI, the quick keyboard shortcuts, the way it just stayed out of the way.
So we decided to rebuild it.
The Approach: AI as the Primary Developer
This wasn't a project where we used AI to autocomplete a few lines of code. We used Claude Code as the primary development tool for the entire project — from the first architecture decision to the final signed installer.
Here's what that actually looked like:
- Architecture planning — deciding on SwiftData over Core Data, structuring the app as a Swift Package (CopiedKit) shared between macOS and iOS targets
- Reverse engineering— understanding how the original app's clipboard monitoring, content type detection, and UI patterns worked
- Implementation — writing every service, model, and view
- Debugging — tracking down gesture conflicts, provisioning profile issues, iCloud sync race conditions
- Code review — finding 25 bugs including critical concurrency issues
- Project management — creating 38 Linear issues automatically from code analysis
- Build pipeline — signed DMG, PKG installer, GitHub Actions CI/CD
The total development time was roughly 48 hours. Not 48 hours of human typing — 48 hours wall clock from “let's do this” to a signed, distributable app with CI/CD.
What We Reverse-Engineered
Clipboard Monitoring
macOS doesn't have a clipboard change notification API. The standard approach — and the one the original Copied used — is polling NSPasteboard.general.changeCount on a timer. Our ClipboardService polls every 0.5 seconds, compares the change count, and captures new content when it changes:
@Observable
@MainActor
public final class ClipboardService {
private var pollingTask: Task<Void, Never>?
private var lastChangeCount: Int = 0
public func startMonitoring() {
pollingTask = Task {
while !Task.isCancelled {
let current = NSPasteboard.general.changeCount
if current != lastChangeCount {
lastChangeCount = current
captureClipboard()
}
try? await Task.sleep(for: .milliseconds(500))
}
}
}
}The tricky part: you need to avoid capturing your own pastes. When the user pastes a clip from our app, the app itself writes to the pasteboard, which triggers the poll. We handle this with a skipNextCapture flag that gets set before writing and cleared after the next poll cycle.
Content Type Detection
The pasteboard can contain multiple representations of the same content simultaneously — plain text, RTF, HTML, and a URL might all be present for a single copy operation. We detect the “best” type by checking pasteboard types in priority order: image, RTF, HTML, URL, then plain text.
Global Hotkey System
Global keyboard shortcuts on macOS are surprisingly hard to get right. The modern approach (CGEvent tap) works everywhere including fullscreen apps, but requires accessibility permissions. The legacy approach (Carbon RegisterEventHotKey) is more reliable but doesn't work in all contexts.
Our GlobalHotkeyManager tries the CGEvent tap first and falls back to Carbon:
/// Uses CGEvent tap (HID level) as primary method — this works
/// in fullscreen apps across all Spaces. Falls back to Carbon
/// RegisterEventHotKey if the event tap can't be created.
@MainActor
final class GlobalHotkeyManager: NSObject {
private var eventTap: CFMachPort? // CGEvent tap
private var hotkeyRef: EventHotKeyRef? // Carbon fallback
}Data Model
SwiftData models that match the original's structure — a Clipping model for individual items, a ClipList for organizing them into collections, and an Assetmodel for binary data (images, files). All three are CloudKit-compatible, which means they use optional properties and avoid unique constraints (CloudKit doesn't support them).
New Features the Original Never Had
Code Snippet Detection
The CodeDetector uses heuristic scoring to identify code snippets across 25+ languages. It looks for structural signals — shebang lines, indentation patterns, bracket ratios, language-specific keywords — and assigns a confidence score:
public static func detect(in text: String) -> Result {
var score: Double = 0
var language: String?
// Shebang — immediate strong signal
if text.hasPrefix("#!") {
score += 0.5
language = detectShebangLanguage(text)
}
// Negative: prose-like (high ratio of spaces)
let spaceRatio = Double(spaceCount) / Double(nonSpaceCount)
if spaceRatio > 0.35 { score -= 0.2 }
// ... 20+ more heuristics
return Result(isCode: score > 0.4, language: language, confidence: score)
}It also detects config formats (YAML, JSON, TOML) as a separate pass, since those have distinct patterns that general code heuristics miss.
Fuzzy Search
The FuzzyMatcherimplements Sublime Text-style matching: characters must appear in order but don't need to be contiguous. Consecutive matches, word boundary matches, and camelCase boundary matches score higher. It returns matched ranges for highlighting:
public struct FuzzyMatch: Sendable {
public let score: Int
public let matchedRanges: [Range<String.Index>]
}
// "fzm" matches "FuzzyMatcher" with ranges highlighting F, z, M
FuzzyMatcher.match(query: "fzm", in: "FuzzyMatcher")
// → FuzzyMatch(score: 42, matchedRanges: [0..<1, 4..<5, 9..<10])Smart Text Transformations
Built-in transforms that the original never had: JSON pretty-print, URL encode/decode, Base64, strip Markdown formatting, case conversions, and more. These work inline — select a clip, apply a transform, paste the result.
Everything Else
- Content type visual icons — each clip shows its type (text, URL, image, code) with a distinctive icon and optional language badge
- Real-time iCloud sync status — a
SyncMonitorthat tracks CloudKit account status, import/export events, and shows sync state in the UI - Keyboard-first navigation — ⌘1–9 quick paste, arrow key navigation, Enter to paste, Tab to preview
- Image thumbnail caching — uses
CGImageSourceCreateThumbnailAtIndexfor memory-efficient thumbnails with LRU cache eviction
Modern Apple Standards
One of the goals was to build with the latest Apple frameworks, not the ones available when the original was written:
| Original Copied | Our Rebuild |
|---|---|
| AppKit NIBs / Storyboards | SwiftUI |
| Core Data / Realm | SwiftData |
| ObservableObject / @Published | @Observable (Observation framework) |
| Manual CKRecord management | CloudKit via SwiftData |
| No concurrency model | Swift 6 strict concurrency |
| Manual builds | GitHub Actions CI/CD |
The codebase is structured as a Swift Package (CopiedKit) that contains all models, services, and shared views. The macOS app (CopiedMac) imports this package and adds platform-specific UI — the menu bar popover, settings window, and main window. An iOS target (CopiedIOS) shares the same package.
The AI Workflow
Here's where it gets interesting. We didn't use Claude Code as a fancy autocomplete. It was the architect, implementer, debugger, and project manager.
Architecture Decisions
The first conversation was about structure: should we use Core Data or SwiftData? How should we handle CloudKit sync? What's the right way to share code between macOS and iOS? Claude Code analyzed the tradeoffs — SwiftData's CloudKit integration is simpler but has constraints (no unique constraints, optional properties only), Core Data offers more control but more boilerplate. We went with SwiftData for the simpler mental model.
Service Implementation
Each core service was built through conversation: ClipboardService for monitoring, CodeDetector for language detection, FuzzyMatcher for search, ThumbnailCache for image handling, SyncMonitor for iCloud status. The pattern was consistent: describe the requirement, let Claude Code propose an implementation, review it, iterate on edge cases.
Debugging
Some of the hardest bugs were platform-specific issues that are poorly documented:
- Gesture conflict — SwiftUI's
.onTapGesturewas eating keyboard events in the popover, making arrow key navigation impossible. Fixed by switching toButtonwith custom styling - Menu bar popover positioning —
NSPopoverattached to a status item has quirks with multi-monitor setups and fullscreen spaces - iCloud sync race conditions— SwiftData's CloudKit integration can trigger merge conflicts when the same device reads and writes in quick succession
Code Review
We ran Claude Code's code review on the entire codebase. It found 25 issues, including:
- A concurrency bug where
@MainActor-isolated state was accessed from a background task - A memory leak in the thumbnail cache where evicted entries weren't releasing their
CGImagereferences - A race condition in the clipboard polling where two captures could fire for a single change
Project Management
Claude Code created 38 issues in Linear automatically — analyzing the codebase, identifying TODO comments, missing features, potential improvements, and filing them with proper descriptions, labels, and priority levels.
Build Pipeline
The final piece: a complete CI/CD pipeline. GitHub Actions builds, signs, and notarizes the app. The release artifacts include both a DMG (drag-to-Applications) and a PKG installer. The build script handles code signing, entitlements, and the notarization wait loop.
What We Learned
AI is better at breadth than depth.Claude Code excels at tasks that require knowing a lot of things at once — how SwiftData models interact with CloudKit constraints, how CGEvent taps relate to accessibility permissions, how to structure a GitHub Actions workflow for macOS code signing. It's less good at deep debugging where you need to hold a complex mental model of state across many execution paths.
The 80/20 rule applies differently.AI gets you to 80% faster than you've ever gotten there before. The last 20% — the platform edge cases, the UX polish, the “it works but doesn't feel right” — still requires human judgment and iteration.
Code review is an underrated AI use case. Having an AI review the entire codebase — not just the diff, but all the code — caught issues that incremental review would have missed. The concurrency bug it found would have been a production crash.
Open Source
The entire project is open source. You can clone it, build it, modify it, and distribute it.
- GitHub: MagnetonIO/copied-app
- Download: Latest release (DMG & PKG)
- Requirements: macOS 15 Sequoia or later
Contributions welcome — whether it's bug fixes, new features, or just filing issues. The Linear project is public too if you want to see what's planned.
Built with SwiftUI, SwiftData, CloudKit, and Claude Code.
If you have questions or want to contribute, open an issue on GitHub.