Handee: my own iPhone file transfer app for the Mac

A native Mac app I built with Claude Code to replace AnyTrans for iOS: it opens when I plug in an iPhone or iPad, copies photos and app files both ways, and deletes nothing it hasn't verified.

The Handee app icon: a white iPhone with a red edelweiss on an amber gradient square, with the name Handee under it

Handee copies and moves files between my Mac and an iPhone or iPad. It replaces AnyTrans for iOS by iMobie, which I ran through Setapp: macOS app subsciption. Handee waits in the background without a Dock icon and opens its window when a device connects by cable. The Mac is on the left, the devices in the middle, and the device's camera roll or app folder on the right. I drag, or press ⌘↩, to copy in either direction.

I built it overnight with Claude Code, on 25th October 2026. The name follows the -ee family of my tools, like my file manager, see Filee: my own file manager for macOS.

The Handee window on a test device: a Mac folder with 2 files on the left, a sidebar with the device, Photos and 2 app folders in the middle, and the camera roll on the right grouped by month, with filter pills for All, Photos, Videos and New

Why I replaced AnyTrans

The agent read a copy of AnyTrans's own logs and data before the build. It was a bad experience, and the numbers say why:

What I found Value
Use on record 2 days in 19 months, 4 launches
Connect to ready 69 s with 420 photos, 5 min 47 s with 3,965 photos
Crashes 7, caught by its own crash guard, in 4 launches
Transfers logged 1: a music track, which took 15 min 15 s
Wireless backup helper 0 backups since July 2022, 10,256 connection errors
Data it kept on my Mac 470 MB, mostly copies of the devices' photo databases

I need this kind of tool a few times a year. When I need it, I want it ready in seconds, not minutes, and I want to trust that a move doesn't lose a photo.

What Handee does

Feature How it works
Opens on plug-in A device on USB opens the window with that device selected. Wi-Fi never does
Camera roll A grid by month, newest first, with All, Photos, Videos and New filters
Originals HEIC, HEVC, ProRAW, both files of a Live Photo and the edit files. No conversion
"New" filter Photos never copied to this Mac, from Handee's own copy log
Verified move Copy, verify, then delete from the device. Refused when iCloud Photos is on
App folders The folder of any app with file sharing: copy both ways, new folder, rename
Safe delete A delete in an app folder keeps a copy on the Mac for 30 days. ⌘Z restores it
AirDrop Sends Mac files to the device, the only public route into its Photos library
Several devices Each paired on its own, with a clear "Tap Trust on the device" step

Photos get a name that sorts: YYMMDD-HHMMSS-IMG_1234.heic, the capture time then the original name. A file with the same name and the same SHA-256 already in the folder is skipped, not copied twice. There is no automatic import. I copy by hand, on purpose.

App folders are where video players and document apps keep their files. Here is a test device with a video player's folder open, and 2 finished transfers:

Handee with a video player's app folder open on a test device: test photos copied to the Mac pane on the left, the app folder with a folder and 2 files on the right, and a transfer panel at the bottom with 2 green bars, 6 items copied and 6 items already there

Keys

Key Action
⌘↩ Copy the selection to the other side
⌥⌘↩ Move it: confirm, copy, verify, delete
Tab Switch side
⌘⌫, ⌘Z Delete with a safety copy; undo the last delete
⇧⌘N, ⌘R New folder, rename
⇧⌘A AirDrop the Mac selection
⌃⌘A/P/V/N Photo filters: All, Photos, Videos, New
⌘K Command centre: every command with its shortcut
⌘. Cancel the running transfers

No global hotkey. Handee opens on plug-in, by name from my launcher, or from the CLI.

A move that can't lose a photo

Reliability was goal 2: nothing leaves a device without a verified copy on the Mac. A move runs in 5 steps:

  1. Download the item and its sidecar files into a hidden folder next to the target.
  2. Check the size and Apple's fingerprint of the copy against the device's.
  3. Record the SHA-256 of the copy in a journal, state verified.
  4. Move the files to their real names.
  5. Delete the verified items from the device in batches of 50.

The edge cases are where the care goes:

  • iCloud Photos on: Move is refused, even from the CLI with --yes. There a delete reaches iCloud and every device, the Mac included.
  • A crash between steps 3 and 5: at the next launch, Handee marks those items kept and says the move was interrupted. It never deletes them without a new check.
  • Cancel: stops the copies, never the deletes of items already verified.
  • Never through AFC: deleting a photo file through the file protocol breaks the Photos library on the device. Photos are deleted only through ImageCaptureCore, after the checks.
  • App folder files arrive under a hidden partial name and get their real name only when the size matches. A pulled cable never leaves a half file under a real name.

Speed

Goal 1 was the time from plug-in to the first copy. Nothing in Handee waits for the whole device to load. Measured with QA's fake device:

Metric Budget Measured AnyTrans
Cold launch to ready < 400 ms 143 to 258 ms -
Window shown again < 50 ms 1 ms -
Grid of 4,000 photos on screen < 500 ms 156 ms -
Select all 4,000 - 25 ms -
Memory with 4,000 thumbnails < 200 MB 67 MB -
Memory after a 200 MB transfer < 200 MB 108 MB -
Handee's own cost per MB > 150 MB/s 1,800 to 6,800 MB/s -
Idle, no window ~ 0% CPU 0.0% CPU, 59 MB -
Plug-in to ready, real device < 15 s not measured yet 69 s to 5 min 47 s

The last row is the one that matters most. I'll add the real number after the first big copy from a phone.

How it's built

Stack

  • Swift 6.3 and AppKit, built with XcodeGen. The transfer rules live in a package without AppKit, so they're unit tested without a device.
  • ImageCaptureCore for photos: Apple's public API behind Image Capture. It gives originals, sidecars, a fingerprint and a proper delete.
  • MobileDevice.framework for device info and app folders, loaded with dlopen. It's Apple's private framework that Finder and Xcode use, and AnyTrans too. It ships on every Mac and Apple updates it for each new iOS.
  • An own AFC client for the file protocol, plus an own usbmux fallback in case a macOS update breaks the framework.
  • SQLite in WAL mode for the copy log, the move journal and the safety copies.
  • Nothing to install. The agent rejected libimobiledevice and pymobiledevice3 (installs that follow new iOS versions late) and Xcode's devicectl (it needs Developer Mode).

Rules that keep it from hanging

  • The main thread never touches the disk, a device or the database.
  • Each device has its own serial queue. Transfers use their own connection, so browsing stays quick during a copy.
  • Every blocking wait has a timeout: 30 s on a device socket, 120 s plus 1 s per 4 MB for a photo download.
  • Transfers stream in 1 MB chunks, each in its own autorelease pool. Without the pools, 200 MB in meant 200 MB of memory.

Gotchas worth knowing

  • App folders open at the container, not the files. Only /Documents is readable, and listing / was refused on iOS 27. Handee shows Documents as the top.
  • Image Capture sees only what's on the device. With "Optimise iPhone Storage", older photos live in iCloud only and don't show up in Handee, Image Capture or Photos import.
  • The Files app's "On My iPhone" folder has no documented route. Handee doesn't try.
  • A background app that opens a window must insist. The resident app switches from accessory to regular, and a plain activate() was declined: the window opened behind the front app. Handee now calls activate(ignoringOtherApps:) twice, 0.1 s apart.
  • Return values from a private framework. MobileDevice functions return int or long. Declared as Int32, the low 32 bits are right for both.

The agent interface

Every command takes --json, so NicAI can use Handee inside a task (see What is NicAI). The CLI opens a handee:// URL in the background, which starts the app when needed, and reads the app's reply from a file:

handee status --json                    # devices, iCloud Photos, locked, running jobs
handee photos list --new --json         # photos never copied to this Mac
handee photos copy --to ~/Pictures/Trip --since 2026-10-01 --wait --json

Destructive commands refuse without --yes. An agent never gets a dialog: --yes is the confirmation.

How it was built

Handee followed my appee process for apps:

  1. Usage review: AnyTrans's logs and data, Apple's device stack on my Mac, and the framework's exports, checked with nm.
  2. Questions: 35 in 1 round, each with a recommended answer. I changed 2: no automatic import, and open the window on plug-in.
  3. Spec and build in the same pass: 9 commits between 01:02 and 07:29 on 25th October 2026.

QA ran against fake devices: a folder that acts as a camera roll, and app folders served by an in-memory AFC server through the real client. At version 1.0:

  • 26 unit tests.
  • 81 end-to-end checks in 6 suites, 3 runs in a row.
  • An independent review by another agent found 15 defects in the data paths, for example a Live Photo's second file not checked before a move. All 15 are fixed.

The screenshots in this note come from those QA runs, on test data.

Left for later

  • Version 2: a local device backup, Messages export from that backup, HEIC to JPEG on export.
  • Dropped: automatic import, writing to the Music library, contacts and calendars (iCloud does those).

Further reading

The app Handee replaces:

Apple's framework for photos on a connected device:

The tool I built it with:

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.