SLT
ProjectsWritingContact
All posts
Product

Why We Built Porter

My sort-downloads script kept breaking every macOS update. Porter is the properly-built version — a menu bar app that files every download into the right NAS folder and actually survives an OS update.

July 7, 2026
7 min read
By Abdul Rafay

I've had some version of a "sort my Downloads folder" script running on my Mac for years. It started as a launchd job watching ~/Downloads and moving files by extension into folders on a NAS — PDFs here, installers there, screenshots somewhere else. It worked, in the sense that it ran, right up until the next macOS update, at which point something in the chain would quietly stop working and I wouldn't notice until my Downloads folder had four hundred files in it again.

Porter exists because I got tired of debugging the same script in a slightly different way every six months, and finally sat down to figure out why it kept breaking instead of patching around the symptom again.

Why the script kept dying

Three separate things kept failing, and each one taught me something about why this is a harder problem than "watch a folder, move files" sounds like on paper.

launchd's WatchPaths goes dead on network volumes. My script used WatchPaths to trigger on changes to the Downloads folder, then wrote the sorted files to an SMB share on the NAS. WatchPaths relies on FSEvents under the hood, and when the destination is a network mount, that mount going through a sleep/wake cycle, a Wi-Fi drop, or a NAS reboot would silently break the watch without any error — the job just stopped firing, with nothing in the logs to say why.

TCC permission resets. macOS periodically resets Full Disk Access grants — most reliably after a significant OS update, but sometimes for reasons that were never clear to me — and a script that isn't a stably-signed application loses its permission grant along with it. My script was a shell script invoked by launchd, which meant it had no code-signing identity of its own to hold onto; every reset meant re-granting permissions I'd already granted a dozen times.

SMB writes from a background daemon are unreliable in a way GUI apps aren't. Writing files to a NAS share from a launchd background job, with no active user session backing the mount, ran into authentication and reconnection issues that a normal logged-in app writing to the same share didn't have. The mount would be "there" from the Finder's perspective and not actually writable from the daemon's.

None of these are exotic problems. They're the specific, boring failure modes of a script trying to do continuously-running, permission-sensitive, network-writing work without being a real application. The fix wasn't a smarter script. It was building an actual app.

What we built

Porter is a macOS menu bar app that watches your Downloads folder and files each finished download into the right destination folder on your NAS — PDFs, screenshots, installers, movies, whatever categories you define — automatically, as soon as the download completes.

The things that make it actually reliable, versus what the script had:

An in-process FSEvents watcher, not launchd's WatchPaths. Porter runs as a long-lived app and owns its own FSEvents subscription directly, rather than depending on launchd to notice a change and spawn a process. This sidesteps the exact failure mode that killed the script — a watch that silently stops firing after a network hiccup, with no process left running to even notice.

A login item, so writes happen in a real user session. Porter runs as a proper login item rather than a headless background daemon, which means SMB writes to the NAS happen in the context of an actual logged-in session with an active mount, not a daemon guessing at whether a network share is currently reachable.

A stable code-signing identity, so Full Disk Access survives updates. Because Porter is a properly signed application (not a shell script wrapped in a launchd plist), macOS's permission system treats it consistently across updates. This was the single biggest quality-of-life fix — the number of times I'd opened System Settings to re-grant Full Disk Access to a shell script went to zero once there was a real, stably-identified app to grant it to.

It watches for whatever file types you configure and moves them to whatever NAS folders you map them to, with rules that are simple on purpose — extension and pattern-based, not a rules engine you need documentation to configure.

The honest tradeoffs

It's still dependent on the NAS being reachable, and we don't hide that. If your NAS is off or the network is down, Porter can't write anywhere, and we don't pretend otherwise with a fake success state or a silent queue that might grow unbounded. Failed moves are visible, not swallowed — the whole reason I built this was to stop discovering problems by finding a full Downloads folder days later, so hiding new failures the same way would have defeated the point.

Rules are intentionally simple, not a general automation engine. I thought about building something closer to Hazel's rule engine — conditions, actions, a whole scripting surface. I didn't, because that's a different, much bigger product, and the actual problem was narrower: "put this file where it goes," not "let me build arbitrary workflows." If you need conditional logic beyond file type and name pattern matching, Porter is the wrong tool and Hazel is probably the right one.

This started as a personal itch, and it shows in the defaults. The category set and folder conventions Porter ships with are close to what I use myself — PDFs, screenshots, installers, movies. It's configurable, but the out-of-box experience is shaped by one person's Downloads folder, which is an honest limitation of building the tool you personally needed first and generalizing after.

Where it's useful

If you've ever had a script like mine — something that watches Downloads and files things away, that worked fine until it quietly didn't — Porter is that script built the way it should have been built from the start: as a real, signed, permission-stable application instead of a launchd job held together with hope.

It's open source under GPL-3.0, and the repo's open for anyone who wants to add categories, sync destinations, or fix the inevitable next SMB edge case at rafay99-epic/porter.

porter.rafay99.com

On this page

  • Why the script kept dying
  • What we built
  • The honest tradeoffs
  • Where it's useful

Ask AI

Ask ChatGPTAsk Claude

From Syntax Lab Technology

envpilot.dev →wryte.xyz →ourtrail.xyz →lumocash.com →
All posts

On this page

  • Why the script kept dying
  • What we built
  • The honest tradeoffs
  • Where it's useful

Ask AI

Ask ChatGPTAsk Claude
Syntax Lab Technology

© 2026 · Independent software organization

ProjectsWritingContactPrivacyTerms
envpilot.dev — live
wryte.xyz — live
ourtrail.xyz — live
LUMO on Google Play — live
support@syntaxlabtechnology.org