A native macOS menu-bar app that auto-mounts your Android phone as a real Finder volume over MTP, using libmtp + macFUSE.
No window. No drag-and-drop app. Plug your phone in, pick "File
Transfer" on the device, and use the menu bar item → Open in
Finder. The mount lives under ~/.AndroidMount/<device-name>/
(a hidden folder in your home directory; volname still labels the
volume in Finder).
┌──────────────────────┐ USB ┌─────────────┐
│ AndroidMount.app │◄─────────►│ Android │
│ (Swift, menu bar) │ IOKit │ phone │
│ │ │ │ (MTP mode) │
│ ▼ │ └─────────────┘
│ spawns `mtpfuse` │
│ (C, libmtp + FUSE) │──► ~/.AndroidMount/... (macFUSE volume)
└──────────────────────┘
- Xcode command line tools –
xcode-select --install - Homebrew + libmtp –
brew install libmtp - macFUSE – download from https://osxfuse.github.io, install, and reboot (it needs to load a kernel extension).
- Android device with MTP / "File Transfer" mode enabled (not PTP or charge-only).
From the directory that contains this Makefile (repository root):
make check-deps # verifies macFUSE + libmtp are reachable
make # builds build/AndroidMount.app
make run # rebuild bundle, kill AndroidMount/mtpfuse/Finder, open the app
./scripts/run.sh # clean + full build + openUse ANDROIDMOUNT_SKIP_KILL=1 make run to open without killing processes first.
Using it: unlock the phone, set USB to File Transfer / MTP, then launch the app (or keep it running). When the menu shows Connected, choose Open in Finder (⌘O). Eject before unplugging.
Debug mtpfuse: to see [LOAD] / libmtp logs on stderr, run the
helper by hand after the phone is connected (adjust paths if needed):
mkdir -p "$HOME/.AndroidMount/Debug"
./build/mtpfuse -f -o volname=Debug "$HOME/.AndroidMount/Debug"The Makefile compiles two binaries:
| Binary | Source | Purpose |
|---|---|---|
AndroidMount |
AndroidMount/*.swift |
menu-bar app, USB watcher |
mtpfuse |
MTPFuse/*.c |
FUSE daemon (libmtp bridge) |
Both are bundled into build/AndroidMount.app. The app finds the
helper via Bundle.main.bundleURL/Contents/MacOS/mtpfuse.
Developers & coding agents: see AGENTS.md (repo overview) and the
project Cursor skill .cursor/skills/droidmount-mtp/SKILL.md (MTP/FUSE
behavior, env vars, partial vs full reads, mutex rules). Update those when you
change bridge or FUSE semantics.
The build is ad-hoc signed (codesign -s -). No Apple Developer
account is required, and Gatekeeper will allow the app on the machine
that built it. macOS may still prompt the first time you launch.
USBWatcher.swift–IOServiceAddMatchingNotificationonkIOUSBDeviceClassName, filtering by known Android vendor IDs (Google 0x18D1, Samsung 0x04E8, etc.) and "MTP" interface strings.MountManager.swift– on connect, spawnsmtpfuse -f -o … volname=…on~/.AndroidMount/<device>/. On disconnect (or "Eject"), runsdiskutil unmount forcethenterminate()+ SIGKILL fallback.mtp_bridge.c– wraps libmtp:LIBMTP_Init,Detect_Raw_Devices/Open_Raw_Device_Uncached,Get_Storage,Get_Files_And_Foldersone folder level at a time (lazy tree underpath → object_id).fs_ops.c– FUSE 2.xgetattr / readdir / read / write / rename / unlink / mkdir / rmdir / create / release / truncate. Reads/writes are staged through a per-handle temp file because MTP is request/response and not seekable; onrelease()a dirty handle is streamed back to the device withLIBMTP_Send_File_From_File_Descriptor(no full-filemalloc).
- Several phones at once: the menu-bar app starts one
mtpfuseper USBlocationIDunder~/.AndroidMount/<name>_<locationHex>/. Each helper setsMTP_USB_BUS_LOCATIONso libmtp opens the matching raw device (bus_location). If a device is not found, setMTP_RAW_INDEXor check stderr for the listedbus_locationvalues. - Writes happen on
release(), so very large copies still need enough free disk space for the staging file under/tmp. - Rename / move uses
LIBMTP_Set_Object_FilenameandLIBMTP_Move_Object; moving a folder to another parent refreshes the in-memory tree. Some vendor MTP stacks may behave oddly. - There is no widely used “modern” replacement for libmtp on macOS for Android file transfer; this project stays on libmtp for device compatibility.
- macFUSE is required; if the kext is missing the app pops a one-shot alert with a link to the installer instead of crashing.
AndroidMount/
├── Makefile
├── README.md
├── AndroidMount/ ← Swift menu-bar app
│ ├── AppDelegate.swift
│ ├── USBWatcher.swift
│ ├── MountManager.swift
│ └── Info.plist ← LSUIElement = YES
└── MTPFuse/ ← C FUSE daemon
├── main.c
├── fs_ops.c
├── fs_ops.h
├── mtp_bridge.c
└── mtp_bridge.h
-
Action log (FUSE + MTP): the menu-bar app sets
MTPFUSE_LOG=1andMTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log(append, session banners per process). Each FUSE op is logged withop=…,path=…,rc=, anddt=…ms(andread/writeinclude size and offset). The bridge’s existingmtp_loglines (MTP I/O) go to the same file when enabled. Watch live:tail -F ~/.AndroidMount/mtpfuse.logA stable symlink is created at
/tmp/mtpfuse-debug-latest.logwhen the log file opens. Disable file logging:MTPFUSE_LOG=0. Enable from the shell without the app:MTPFUSE_LOG=1and optionallyMTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log(same rules asMTPFUSE_DEBUG_LOGfor a custom path).MTPFUSE_LOG_SYNC=1flushes every line (slower, safer if the process is killed).MTPFUSE_DEBUG=1also turns logging on. Hot-path volume queries: ifstatfslines are too noisy, we can add a filter in a follow-up. -
Finder stuck on “Loading…” – use the log above. Long gaps between
readdir_snapshotandGet_Files_And_Foldersmean the phone or MTP listing is slow for that folder; manyfuse op=read/ bridgemtp_readlines mean Finder is reading data (e.g. previews). -
“Mount point is already in use” – usually a leftover macFUSE mount after Finder or the app hung. On the next connect, the app force-unmounts that path and stops matching
mtpfuseprocesses automatically; if it still fails, run Eject in the menu ordiskutil unmount force ~/.AndroidMount/<device>. -
"MTPFuse helper binary not found" – run
make(ormake run); the app looks formtpfusenext to its own executable. -
fuse: device not found, try 'modprobe fuse' first– macFUSE kext was blocked. Open System Settings → Privacy & Security and click Allow next to "System software from developer …", then reboot. -
No raw devices foundwhen the daemon starts – the phone is still in Charging mode. Pull down the notification shade on the device and switch USB usage to File Transfer. -
LIBMTP_Open_Raw_Devicefails while the phone shows as connected – another app may already hold the MTP USB session (same class of issue SwiftMTP documents forLIBUSB_ERROR_NOT_FOUND): quit Preview, Image Capture, and Android File Transfer (including its background agent in Activity Monitor), then unplug/replug.mtpfuseretries detection/open a few times with a short delay to ride out USB settle. -
Optional environment (passed to
mtpfuse): setMTP_SKIP_HIDDEN=1orMTP_HIDE_DOTFILES=1to omit device entries whose names start with.(Finder-style “no dotfiles” listing; off by default). -
Mount appears but is empty – tap Allow on the phone the very first time the Mac connects; MTP requires per-host authorization.