diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml
--- a/.github/workflows/packaging-lint.yml
+++ b/.github/workflows/packaging-lint.yml
@@ -21,3 +21,13 @@ jobs:
   lint:
     # Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
     uses: storytold/craft-common-actions/.github/workflows/packaging-lint.yml@main
+
+  macos-dmg:
+    timeout-minutes: 15
+    name: macOS DMG app passes codesign --strict
+    runs-on: macos-15
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
+      - run: bash packaging/macos/dmg-test.sh
diff --git a/docs/releasing.md b/docs/releasing.md
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -76,8 +76,11 @@ deliberately, in every job at once.
   `entitlements.plist`. The Developer ID certificate is imported into a temporary keychain by
   `packaging/macos/import-cert.sh`.
 - **Notarization:** the app is sent with `xcrun notarytool submit`, then the ticket is stapled
-  and checked with `stapler validate`. The app goes on a DMG (`hdiutil makehybrid`), which is
-  signed and notarized too. The universal `filmcraft-cli` is signed the same way, zipped, and the zip is notarized.
+  and checked with `stapler validate`. The app goes on a DMG (`hdiutil create`), which is
+  signed and notarized too. Before notarizing it, the script mounts the DMG and checks the app in
+  it, and a copy installed from it, with `codesign --verify --strict --deep` (an image made with
+  `makehybrid` failed that, #882; `packaging/macos/dmg-test.sh` tests it).
+  The universal `filmcraft-cli` is signed the same way, zipped, and the zip is notarized.
   Its Finder window (background, icon size and positions) comes from
   [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `FilmCraft`
   without the version, which the window's background needs; the DMG file name keeps the version.
diff --git a/packaging/macos/dmg-test.sh b/packaging/macos/dmg-test.sh
new file mode 100755
--- /dev/null
+++ b/packaging/macos/dmg-test.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+# The app in the macOS DMG, and a copy installed from it, pass `codesign --verify --strict --deep`
+# (#882). Runs package.sh end to end, signed ad hoc, around a stand-in executable (no Rust build,
+# no secrets), with a font licence that carries Finder info as the input. Then shows the check
+# rejects the image `hdiutil makehybrid -hfs` made of the same files. macOS only;
+# CI: .github/workflows/packaging-lint.yml.
+set -euo pipefail
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+fail() { echo "FAIL: $*" >&2; exit 1; }
+if [[ "$(uname -s)" != Darwin ]]; then
+  echo "skip: needs macOS (hdiutil, codesign)"
+  exit 0
+fi
+case "$(uname -m)" in
+  arm64) ARCH=aarch64 TRIPLE=aarch64-apple-darwin CC_ARCH=arm64 ;;
+  x86_64) ARCH=x86_64 TRIPLE=x86_64-apple-darwin CC_ARCH=x86_64 ;;
+  *) fail "unknown machine $(uname -m)" ;;
+esac
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+# Ad hoc, not notarized, and no Actions annotations from the script under test.
+unset MACOS_SIGN_IDENTITY MACOS_KEYCHAIN APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID GITHUB_ACTIONS
+export CARGO_TARGET_DIR="$TMP/target" DIST="$TMP/dist" CRAFT_FONTS_DIR="$TMP/craft-fonts"
+# shellcheck source=../env.sh
+. "$HERE/../env.sh"
+# shellcheck source=dmg.sh
+. "$HERE/dmg.sh"
+
+# package.sh only joins, signs and runs (`filmcraft-cli --version`) the binaries, so any
+# executable stands in for them.
+bin="$CARGO_TARGET_DIR/$TRIPLE/release"
+mkdir -p "$bin"
+printf 'int main(void) { return 0; }\n' | cc -arch "$CC_ARCH" -x c -o "$bin/filmcraft" -
+cp "$bin/filmcraft" "$bin/filmcraft-cli"
+# A licence with Finder info (what makehybrid gave every file): `cp` brings it into the bundle, so
+# the strip before signing is tested too.
+mkdir -p "$CRAFT_FONTS_DIR/fonts/test-sans"
+echo "test licence" >"$CRAFT_FONTS_DIR/fonts/test-sans/OFL.txt"
+/usr/bin/xattr -wx com.apple.FinderInfo 00000000000000000000FFFFFFFF000000000000000000000000000000000000 \
+  "$CRAFT_FONTS_DIR/fonts/test-sans/OFL.txt"
+
+"$HERE/package.sh" --arch "$ARCH" --skip-build || fail "package.sh failed"
+DMG="$DIST/filmcraft-$VERSION-macos-$ARCH.dmg"
+check_dmg_app "$DMG" FilmCraft.app || fail "the app in $(basename "$DMG") fails codesign --strict"
+
+# Interrupted while the image is mounted (SIGTERM, as a cancelled job gets it: a stand-in codesign
+# sends it to the check), the check still detaches the image and removes its scratch directory.
+mkdir "$TMP/stub" "$TMP/interrupted"
+cat >"$TMP/stub/codesign" <<'EOF'
+#!/bin/sh
+kill -TERM "$PPID"
+EOF
+chmod +x "$TMP/stub/codesign"
+if (
+  PATH="$TMP/stub:$PATH"
+  export RUNNER_TEMP="$TMP/interrupted"
+  check_dmg_app "$DMG" FilmCraft.app
+) >/dev/null 2>&1; then
+  fail "check_dmg_app passed though it was interrupted"
+fi
+left="$(ls -A "$TMP/interrupted")"
+if [[ -n "$left" ]]; then
+  hdiutil detach -force "$TMP/interrupted/$left/mnt" >/dev/null 2>&1 || true
+  fail "an interrupted check_dmg_app left $left behind, its image still attached"
+fi
+
+# The check must catch the defect it guards against: the same staged files, imaged the old way.
+OLD="$TMP/makehybrid.dmg"
+hdiutil makehybrid -quiet -hfs -hfs-volume-name FilmCraft -o "$OLD" "$CARGO_TARGET_DIR/macos-package/dmg"
+if out="$(check_dmg_app "$OLD" FilmCraft.app 2>&1)"; then
+  fail "check_dmg_app accepted a makehybrid image"
+fi
+case "$out" in
+  *"Finder information, or similar detritus"*) ;;
+  *) fail "check_dmg_app rejected the makehybrid image for another reason: $out" ;;
+esac
+
+echo "ok: the app in the DMG passes codesign --strict; an interrupted check detaches; a makehybrid image of it is rejected"
diff --git a/packaging/macos/dmg.sh b/packaging/macos/dmg.sh
new file mode 100644
--- /dev/null
+++ b/packaging/macos/dmg.sh
@@ -0,0 +1,70 @@
+# shellcheck shell=bash
+# DMG helpers shared by package.sh and dmg-test.sh. Source it after ../env.sh (`warn`).
+
+# `hdiutil create` and `attach` attach a device, which fails now and then on CI runners ("Resource
+# busy"), so they get a few tries. A retry can't end a hang: where a sandbox blocks the disk-image
+# helper (some agent sandboxes do), hdiutil can wait forever, so build and check DMGs outside one.
+# Usage: hdiutil_retry <what> <hdiutil args…>; tries five times with growing pauses.
+hdiutil_retry() {
+  local what="$1" attempt
+  shift
+  for attempt in 1 2 3 4 5; do
+    if hdiutil "$@"; then
+      return 0
+    fi
+    if [[ "$attempt" = 5 ]]; then
+      echo "error: hdiutil $what failed 5 times" >&2
+      return 1
+    fi
+    warn "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s"
+    sleep $((attempt * 10))
+  done
+}
+
+# The app must pass `codesign --verify --strict --deep` as users get it: in the mounted image, and
+# copied out of it with ditto, which keeps extended attributes the way a Finder drag to Applications
+# does. Checking the bundle before it goes on the image is not enough: `hdiutil makehybrid -hfs`
+# gave every file in the image a com.apple.FinderInfo, which --strict rejects as "resource fork,
+# Finder information, or similar detritus not allowed" (#882), while the DMG's own signature and
+# Gatekeeper still passed.
+# Usage: check_dmg_app <dmg> <App.app>
+#
+# The check runs in a subshell whose EXIT trap detaches the image and removes the scratch directory
+# however it ends: a failed step, `set -e`, Ctrl-C, or the SIGTERM a cancelled job gets (bash skips
+# the EXIT trap when a signal kills it, so HUP, INT and TERM are turned into exits). The traps are
+# the subshell's own, so a caller's EXIT trap is left alone.
+check_dmg_app() (
+  dmg="$1" app="$2" rc=0
+  dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/dmg-check.XXXXXX")"
+  trap 'exit 129' HUP
+  trap 'exit 130' INT
+  trap 'exit 143' TERM
+  trap 'dmg_check_cleanup "$dir" "$?"' EXIT
+  mkdir "$dir/mnt" "$dir/copy"
+  hdiutil_retry attach attach -readonly -nobrowse -noautoopen -mountpoint "$dir/mnt" "$dmg" >/dev/null || exit 1
+  echo "==> codesign --verify --strict --deep: $app in $(basename "$dmg"), then a copy installed from it"
+  if ! codesign --verify --strict --deep --verbose=2 "$dir/mnt/$app"; then
+    rc=1
+  elif ! ditto "$dir/mnt/$app" "$dir/copy/$app" || ! codesign --verify --strict --deep --verbose=2 "$dir/copy/$app"; then
+    rc=1
+  fi
+  if [[ "$rc" != 0 ]]; then
+    echo "error: $app in $(basename "$dmg") fails codesign --verify --strict --deep" >&2
+  fi
+  exit "$rc"
+)
+
+# check_dmg_app's EXIT trap. Whatever is mounted on <dir>/mnt is detached (found by its device, not
+# a flag, so an image attached just before a signal goes too), then <dir> is removed. Exits with
+# <status>, or 1 when the image won't detach (then <dir> stays, with the mount in it).
+# Usage: dmg_check_cleanup <dir> <status>
+dmg_check_cleanup() {
+  local dir="$1" rc="$2" dev
+  dev="$(stat -f %d "$dir/mnt" 2>/dev/null)" || dev=""
+  if [[ -n "$dev" && "$dev" != "$(stat -f %d "$dir")" ]] && ! hdiutil_retry detach detach "$dir/mnt" >/dev/null; then
+    echo "error: could not detach $dir/mnt" >&2
+    exit 1
+  fi
+  rm -rf "$dir"
+  exit "$rc"
+}
diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md
--- a/packaging/macos/dmg/README.md
+++ b/packaging/macos/dmg/README.md
@@ -2,8 +2,8 @@
 
 What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the
 `Applications` link side by side. `package.sh` copies these files into the image; nothing here is
-generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no
-Finder scripting on CI, nothing extra installed in the signing job).
+generated at build time, so the DMG builds with a plain `hdiutil create -srcfolder` (no Finder
+scripting on CI, nothing extra installed in the signing job).
 
 | File | What |
 |---|---|
@@ -19,6 +19,20 @@ through an alias that includes the volume name, and Finder resolves it by that n
 versioned name the window keeps its size and icon positions but shows no background (tested).
 The DMG file name (`filmcraft-<version>-macos-<arch>.dmg`) still carries the version.
 
+## `hdiutil create`, not `makehybrid`
+
+`hdiutil makehybrid -hfs` gives every file and folder on the volume a `com.apple.FinderInfo` (a
+Finder location of -1,-1), even when the staged files have none. `codesign --verify --strict`
+rejects it on the app in the image and on every copy installed from it ("resource fork, Finder
+information, or similar detritus not allowed", #882), though Gatekeeper accepts the app.
+`hdiutil create -srcfolder` writes the files as they are. `package.sh` checks the app in the
+finished image (`check_dmg_app` in `../dmg.sh`), and `../dmg-test.sh` tests both ways.
+
+The one thing `create` can't do is `makehybrid -hfs-openfolder`, which listed the volume's root as
+its first open folder. Opening the window doesn't depend on it: double-clicking a read-only disk
+image opens its volume in the Finder (hdiutil(1), `auto-open-ro-root`). The window's background,
+icon size and positions come from `.DS_Store`, as before.
+
 ## Rules
 
 - **Finder draws the icon labels in black in light and dark mode** when a window has a background,
diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh
--- a/packaging/macos/package.sh
+++ b/packaging/macos/package.sh
@@ -16,6 +16,8 @@ set -euo pipefail
 # shellcheck source=../env.sh
 . "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
 HERE="$ROOT/packaging/macos"
+# shellcheck source=dmg.sh
+. "$HERE/dmg.sh"
 
 ARCH=universal
 SKIP_BUILD=0
@@ -110,6 +112,10 @@ plutil -lint "$APP/Contents/Info.plist"
 printf 'APPL????' >"$APP/Contents/PkgInfo"
 # licences of the embedded craft-fonts fonts (builds with CRAFT_FONTS_DIR)
 copy_font_licences "$APP/Contents/Resources"
+# `codesign --verify --strict` rejects Finder info and resource forks in a bundle (#882), and `cp`
+# copies extended attributes, so a local checkout can bring them in on the icon or a licence.
+# Apple's xattr: a Homebrew `xattr` earlier on PATH has no -r.
+/usr/bin/xattr -cr "$APP"
 
 # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature).
 # Today the only nested code is the main executable; frameworks/helpers would be signed here too.
@@ -135,15 +141,18 @@ ln -s /Applications "$STAGE/Applications"
 mkdir -p "$STAGE/.background"
 cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff"
 cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store"
-rm -f "$DMG" "$WORK/raw.dmg"
-# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`,
-# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions.
+rm -f "$DMG"
+# `hdiutil create -srcfolder` writes the files as they are. `makehybrid -hfs` gave every file a
+# com.apple.FinderInfo (a Finder location of -1,-1), which `codesign --verify --strict` rejects on
+# the app in the image and on a copy installed from it (#882; the sibling apps made the same
+# change in storytold/pdfcraft#823 and storytold/vectorcraft#1121). `create` attaches a device
+# while it copies: see hdiutil_retry in dmg.sh for CI and sandboxes.
 # The volume name has no version: .DS_Store finds the background through an alias that includes it.
-hdiutil makehybrid -hfs -hfs-volume-name "FilmCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE"
-hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG"
-rm -f "$WORK/raw.dmg"
+hdiutil_retry create create -srcfolder "$STAGE" -volname "FilmCraft" -fs HFS+ -format UDZO -imagekey zlib-level=9 -ov "$DMG"
 sign "$DMG"
 codesign --verify --strict --verbose=2 "$DMG"
+# Before notarizing: the app in the image must still pass --strict, so this can't regress unseen.
+check_dmg_app "$DMG" FilmCraft.app
 if [[ "$NOTARIZE" = 1 ]]; then
   notarize "$DMG"
   xcrun stapler staple "$DMG"
