The Deploy Problem Nobody Talks About

The word 'Launch' spelled with Scrabble tiles.

Photo by Markus Winkler on Unsplash

I can take a macOS app from source code to signed, notarised, published GitHub release in about three minutes. One click. Ten pipeline stages. My Release Manager application handles everything: build, sign, verify signing, notarise with Apple, staple the ticket, verify notarisation, package, and upload to GitHub with release notes and assets.

Three minutes to build and publish. Three days to get it running correctly on someone else’s machine.

That’s an exaggeration, but not by as much as you’d think. The gap between “released” and “correctly installed” on macOS is wider than it has any right to be, and almost nobody talks about it — because if you distribute through the Mac App Store, the gap doesn’t exist. Apple handles it. But if you distribute outside the store, as every independent developer who doesn’t want to give Apple 30% of nothing does, you’re on your own. And “on your own” involves quarantine flags, App Translocation, Gatekeeper, and a security model that — quite correctly — treats every downloaded application as a potential threat until proven otherwise.

The quarantine flag

When you download a file through a web browser on macOS, the browser adds an extended attribute to the file: com.apple.quarantine. This is a metadata tag stored in the filesystem that records where the file came from, when it was downloaded, and which application downloaded it.

The quarantine flag is invisible in Finder. You can’t see it in Get Info. You can’t see it in the file’s properties. The only way to know it’s there is to run xattr -l on the file in Terminal. It looks something like this:

com.apple.quarantine: 00c1;69da8b63;Safari;SOME-UUID-HERE

That semicolon-delimited string contains the quarantine flags, a timestamp, the downloading application’s name, and a unique identifier. It follows the file through extraction — when you unzip a downloaded archive, the quarantine flag propagates to every file inside it. Your app bundle, its executable, its resources, its code signature files — all quarantined.

This is by design. Apple wants to know that a file came from the internet, so that Gatekeeper can make informed decisions about whether to let it run. The problem isn’t that quarantine exists. The problem is what happens next.

Gatekeeper’s three-act play

When you double-click a quarantined app for the first time, Gatekeeper performs three checks:

  1. Signature verification. Is the app signed with a valid Developer ID certificate? If not, Gatekeeper blocks the launch entirely. The user sees a dialog saying the app “can’t be opened because it is from an unidentified developer.” This is the check that code signing and notarisation solve.

  2. Notarisation verification. Has the app been submitted to Apple’s notary service and approved? If not, Gatekeeper shows a warning dialog. The user can still open the app by right-clicking and choosing “Open,” but the default path is blocked. This is the check that notarisation and stapling solve.

  3. App Translocation. If the app passes the first two checks but is still quarantined, macOS may silently move it to a temporary read-only location before launching it. The app appears to run normally, but it’s not running from where you put it.

The first two acts are well-documented and well-understood. The third one is where things get strange.

App Translocation

App Translocation was introduced in macOS Sierra as a security measure against a specific attack: an app that modifies its own bundle contents at runtime. By relocating the app to a read-only temporary directory, macOS ensures that the running code can’t write to its own Resources, Frameworks, or any other part of the bundle.

The relocation is silent. The app isn’t told it’s been moved. Bundle.main.bundlePath returns the translocated path, not the original. If your app reads files from its own bundle — which most apps do — it reads them from the temporary copy. If your app tries to write to its bundle — which it shouldn’t, but some older apps do — the write fails with a permission error.

The translocated path looks something like:

/private/var/folders/xx/random/T/AppTranslocation/another-random/d/YourApp.app

That’s the real path. That’s what ps shows. That’s what the process table records. The app is running from a randomised temporary directory, not from /Applications.

This has consequences. If you check the running process’s path — as my system audit does — you’ll see the translocated path, not the install path. If you try to update the app by replacing the bundle in /Applications, the running copy in the temp directory isn’t affected. If you have any logic that depends on “where am I running from?” — and you’d be surprised how much software does — it will see a path it doesn’t expect.

The most insidious part: Translocation only happens on first launch of a quarantined app. If you move the app to a different directory and move it back — or if you remove the quarantine flag — subsequent launches run from the real path. This means the problem is intermittent by nature. It happens once, confuses someone, and then goes away without explanation.

The removal problem

You’d think removing the quarantine flag would be straightforward. It’s an extended attribute. macOS has a command for removing extended attributes: xattr -d. Run xattr -dr com.apple.quarantine /Applications/YourApp.app and the flag should be gone from the bundle and everything inside it.

On a freshly extracted app with default permissions, this works. But if the app has been launched — even once — macOS may have tightened the file permissions through TCC (Transparency, Consent, and Control), the system that manages access to protected resources like the camera, microphone, contacts, and files. Once TCC has seen the app, even sudo may not be able to modify its extended attributes.

I hit this exact problem. The Notes Editor had been downloaded via a browser, quarantined, translocated on first launch, and subsequently protected by TCC. Running sudo xattr -dr com.apple.quarantine on it returned “Operation not permitted” for every file in the bundle. The quarantine flag was irremovable through normal means.

The fix was to delete the app entirely and reinstall it from a source that doesn’t add quarantine. I used the gh CLI tool — gh release download — which downloads the release asset directly without involving a browser. No browser means no quarantine flag. No quarantine means no Translocation. The app runs from /Applications immediately, first launch, no surprises.

The browser tax

This is the part that frustrates me. I’ve done everything Apple asks:

The app is legitimate by every measure Apple defines. And yet, when a user downloads it through a browser — which is how most people download things from the internet — the browser adds a flag that causes macOS to treat the app with suspicion. Not because the app is suspicious, but because the download channel is.

The Mac App Store doesn’t have this problem. Apps installed through the store aren’t quarantined, aren’t translocated, and aren’t subject to the same Gatekeeper ceremony. They’re trusted by default because they came through Apple’s distribution channel.

If you distribute outside the store, you get the quarantine tax. Your users get a friction that Apple’s own distribution channel doesn’t impose. The playing field isn’t level, and it isn’t meant to be.

What independent developers can do

There are a few strategies for minimising the impact, none of which are ideal:

Document the right-click dance. On first launch, a notarised-but-quarantined app shows a Gatekeeper warning. The user needs to right-click, choose “Open,” and confirm. This is well-known among Mac power users and completely unknown to everyone else. Your README or installation guide needs to explain it.

Offer a CLI download option. If you host on GitHub, tell users they can install with gh release download or curl instead of clicking the download link in a browser. CLI downloads don’t get quarantined. This is a niche solution — most users aren’t going to open Terminal to install an app — but for a technical audience, it works.

Ship a .pkg installer. Installer packages (.pkg files) go through Gatekeeper differently. The installer process handles quarantine removal as part of the installation. When the app is placed in /Applications by the installer rather than by the user dragging and dropping, it isn’t subject to Translocation. This is the most user-friendly option, but it requires building an installer, which is additional complexity in your release pipeline.

Build a local install step. If you have an app that manages releases — like the Release Manager — you can add a step that downloads the release asset via API (no quarantine), removes any residual extended attributes, and copies the bundle to /Applications. This is what I’ve started doing for my own apps: build, release to GitHub, then deploy locally through the pipeline rather than downloading through a browser.

Strip quarantine in a post-install script. If you ship a .dmg with a drag-to-install workflow, you can include a hidden post-install script that strips the quarantine flag. This is technically possible but feels wrong — you’re circumventing a security feature, even if that feature is causing your specific app unnecessary friction.

The philosophical gap

The deeper issue is a tension between two legitimate goals.

Apple wants to protect users from malicious software. The quarantine, Gatekeeper, and Translocation systems are part of a defence-in-depth strategy that assumes downloaded software is dangerous until proven otherwise. This is a reasonable default for a platform that hundreds of millions of people use, many of whom can’t distinguish a legitimate app from a trojanised one.

Independent developers want to distribute software directly to users without an intermediary taking a cut, imposing rules, or adding friction. This is a reasonable desire for anyone who builds and ships their own tools.

These goals are in direct conflict when the “proof of legitimacy” is only frictionless through Apple’s own channel. Notarisation was supposed to be the answer — a way to prove your app is safe without going through the store. And it is, technically. But the user experience of installing a notarised app downloaded from a browser is still worse than installing the same app from the Mac App Store. The friction exists at the distribution layer, not the trust layer.

I don’t have a solution to this. I don’t think there is one, at least not one that satisfies both sides. Apple isn’t going to remove quarantine. Browsers aren’t going to stop adding quarantine flags. Users aren’t going to switch to CLI downloads.

What I can do is make the path from “published to GitHub” to “running correctly on my machine” as smooth as possible for my own apps. The Release Manager’s audit tells me when something is wrong. The pipeline handles signing and notarisation automatically. And when I need to install one of my own apps, I use the CLI, not the browser.

It’s a workaround for a problem that shouldn’t exist. But it works, and “it works” is sometimes the best you can do when the platform isn’t on your side.