Unwait

The invisible tar entries that broke our auto-updater

· 3 min read macos tauri code signing release engineering

macOS tar quietly stores extended attributes as ._ files, bsdtar hides them again on the way out, and a Rust tar parser turns them into real files that break your code signature. How our 0.1.6 update failed and the check that would have caught it.

Unwait 0.1.6 shipped, and auto-update broke. The tarball we published was signed, notarized, stapled, and verified. We listed its contents and saw nothing wrong. We extracted it and got a working app. Every check we knew how to run said the artifact was fine, and every user who updated got a broken install.

The bug is a three-way disagreement between macOS tar, macOS tar again, and the Rust tar crate. If you build update artifacts on a Mac with anything other than your framework's own bundler, you probably have the same hole.

macOS tar smuggles metadata as ._ files

Mac filesystems attach extended attributes to files. Our freshly notarized binary carried one called com.apple.provenance. The tar format has no native slot for xattrs, so Apple's tar stores each one as a separate archive entry: a sibling file named ._Info.plist next to Info.plist, in a format called AppleDouble.

That alone would be visible and harmless. The trap is the second half: bsdtar (which is what tar is on macOS) reassembles AppleDouble entries back into xattrs when reading the archive, and hides them while doing it. tar tzf did not show us the ._ entries. Extracting with tar xzf produced a clean bundle with the metadata folded back where it came from. From inside macOS tooling, the archive is indistinguishable from a clean one.

Rust tar takes the archive literally

The Tauri updater does not extract with bsdtar. It extracts with the Rust tar crate, which does no AppleDouble reassembly and materializes exactly what the archive contains. On the user's machine, ._Info.plist and friends became real files inside Unwait.app.

A macOS code signature seals the bundle's exact file inventory. Files that are not in the seal make verification fail:

$ codesign --verify --strict Unwait.app
Unwait.app: file added

So every updated install was an app whose signature no longer verified. The update pipeline had signed the tarball correctly, and the tarball reproduced a broken bundle, and nothing between us and the user ever looked at it the way the updater would.

Why the hole was ours

Tauri's own bundler produces the update tarball in Rust, symmetric with the Rust extractor, so this failure cannot happen to a stock pipeline. We rebuilt the tarball by hand in our release script, because the app inside it needs the notarization ticket stapled after the bundler runs, and we used the tool every macOS release script uses: tar.

The fix is one environment variable. Apple's tar checks COPYFILE_DISABLE and skips the AppleDouble packing entirely:

COPYFILE_DISABLE=1 tar czf Unwait.app.tar.gz Unwait.app

The check that actually catches it

The one-line fix is not the lesson. The lesson is that we verified the artifact with the wrong parser. Our release gate extracted the tarball with bsdtar and checked the result, which meant the gate could not see what the updater would see, by construction.

The gate now unpacks with Python's tarfile, which, like the Rust crate, takes the archive literally:

python3 -c "import tarfile,sys; tarfile.open(sys.argv[1]).extractall(sys.argv[2])" \
  "$TARBALL" "$CHECK_DIR"
xcrun stapler validate "$CHECK_DIR/Unwait.app"
codesign --verify --strict "$CHECK_DIR/Unwait.app" || exit 1

If a ._ file sneaks back in, codesign --strict fails on the extracted copy and the release script refuses to publish. This exact check, run against the 0.1.6 tarball, fails immediately. That is the property you want from a release gate: it must reproduce the consumer's conditions, not the producer's.

To inspect an archive you already have, the same idea works as a listing:

python3 -c 'import tarfile,sys
for m in tarfile.open(sys.argv[1]): print(m.name)' Unwait.app.tar.gz | grep '/\._'

Any output means AppleDouble entries are present. GNU tar on a Linux box shows them too, which is the accidental way most people eventually discover them.

The general shape of this bug

Strip the macOS specifics and the pattern is: a producer writes platform metadata into a portable format, the platform's own tools hide that metadata on the way back out, and a cross-platform consumer renders it literally. Checking the artifact with the producer's tools tells you nothing. The archive that looks identical on your machine and behaves differently on the consumer's is the worst kind of artifact, because every local verification gives you false confidence.

If your updater, CI, or install script extracts archives with anything that is not bsdtar, build those archives with COPYFILE_DISABLE=1, and verify them with the extractor your consumer uses. It is one environment variable and four lines of gate script, and it is the difference between finding this in CI and finding it in your users' dock.

Further reading

The AppleDouble behavior lives in Apple's copyfile(3) man page, which documents COPYFILE_DISABLE. The Tauri updater flow is documented in the Tauri updater guide. The overlay app this pipeline ships is Unwait, which shows learning cards while your AI coding agent works.

Unwait does this for you

A macOS menu bar app that watches your Claude Code and Codex sessions, shows a short card while they work, and puts a strip on screen the moment one finishes. Free for two weeks, no card and no sign up.

Try for free
← All posts