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:

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

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 CopiedOur Rebuild
AppKit NIBs / StoryboardsSwiftUI
Core Data / RealmSwiftData
ObservableObject / @Published@Observable (Observation framework)
Manual CKRecord managementCloudKit via SwiftData
No concurrency modelSwift 6 strict concurrency
Manual buildsGitHub 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:

Code Review

We ran Claude Code's code review on the entire codebase. It found 25 issues, including:

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.

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.