
This appears when you double-click a .app that was downloaded from the internet and is not recognized by Apple. Despite the alarming wording, the app is not actually damaged. Keep reading.
Why does macOS block the app?
macOS includes a security system called Gatekeeper that checks whether an application has been reviewed and digitally signed by Apple. Apps distributed outside the Mac App Store must be signed with a paid Apple Developer certificate and submitted to Apple for notarization. Because most open-source and research tools skip this step (it costs $99/year and requires an Apple account), Gatekeeper may block them.
If you encounter a dialog like the one displayed above (and if you trust the software!)
If the application is provided as an .app bundle:*
.app file.You only need to do this once. From then on, the app will open normally with a double-click.
If the application is a command-line tool (not a .app bundle), the right-click trick does not apply. Instead, remove the quarantine attribute that macOS attached when you downloaded the file:
xattr -d com.apple.quarantine ./retinotopy
Then make sure the binary is executable and run it normally:
chmod +x ./retinotopy
./retinotopy -s 0 -r 1
You only need to run xattr once. If the file is inside a zip you extracted, run the command on the binary after extraction.

This dialog appears via System Settings > Privacy & Security after a blocked launch attempt. macOS offers you an “Open Anyway” button there.
To open it this way:
Again, you only need to do this once.
For graphical apps:
| Situation | Fix |
|---|---|
| “Damaged … move to Bin” after double-click | Right-click → Open → Open |
| Blocked via Privacy & Security dialog | System Settings → Privacy & Security → Open Anyway |
When macOS downloads a file, it attaches an invisible quarantine attribute (com.apple.quarantine) to it. When the user tries to open it, Gatekeeper checks whether the app is signed and notarized by Apple. If it is not, Gatekeeper shows the “damaged” error.
There are three levels of signing:
| Level | Cost | User experience |
|---|---|---|
| Not signed | Free | “Damaged” error — very alarming, right-click workaround needed |
Ad-hoc signed (codesign -s -) |
Free | First-launch warning only — right-click workaround still needed, but no “damaged” message |
| Apple-notarized | $99/year Apple Developer account | No warnings at all |
For open-source projects, ad-hoc signing is a good middle ground: it eliminates the most alarming “damaged” dialog while costing nothing.
.app bundle with ad-hoc signingA macOS .app is just a folder with a specific structure:
MyApp.app/
├── Contents/
│ ├── Info.plist ← metadata (bundle ID, version, icon name…)
│ ├── MacOS/
│ │ └── myapp ← the compiled binary
│ └── Resources/
│ └── AppIcon.icns ← optional icon
The script package-macos.sh (included in this repository) builds this structure from a compiled binary, ad-hoc signs it, and packages the result as a .zip ready for distribution. You can download it directly:
curl -O https://raw.githubusercontent.com/chrplr/note-about-macos-unsigned-apps/refs/heads/main/package-macos.sh
chmod +x package-macos.sh
#!/usr/bin/env bash
# package-macos.sh — build, sign and zip a macOS .app bundle
# Usage: bash package-macos.sh <binary> <AppName> <bundle-id> <version> [icon.icns]
#
# Example:
# bash package-macos.sh ./myapp MyApp com.example.myapp 1.2.3 assets/icon.icns
set -euo pipefail
BINARY="$1"
APP_NAME="$2" # e.g. MyApp (the .app will be MyApp.app)
BUNDLE_ID="$3" # e.g. com.example.myapp
VERSION="$4" # e.g. 1.2.3
ICON="${5:-}" # optional path to .icns file
APP_BUNDLE="${APP_NAME}.app"
ZIP_NAME="${APP_NAME}.zip"
echo "→ Creating ${APP_BUNDLE} ..."
rm -rf "${APP_BUNDLE}"
mkdir -p "${APP_BUNDLE}/Contents/MacOS"
mkdir -p "${APP_BUNDLE}/Contents/Resources"
# Copy binary
cp "${BINARY}" "${APP_BUNDLE}/Contents/MacOS/${APP_NAME}"
chmod +x "${APP_BUNDLE}/Contents/MacOS/${APP_NAME}"
# Copy icon (optional)
if [[ -n "${ICON}" && -f "${ICON}" ]]; then
cp "${ICON}" "${APP_BUNDLE}/Contents/Resources/AppIcon.icns"
ICON_KEY="<key>CFBundleIconFile</key><string>AppIcon</string>"
else
ICON_KEY=""
fi
# Write Info.plist
cat > "${APP_BUNDLE}/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
${ICON_KEY}
</dict>
</plist>
EOF
echo "→ Ad-hoc signing ${APP_BUNDLE} ..."
# '-' means ad-hoc (no certificate). --deep signs nested binaries and frameworks too.
codesign --force --deep --sign - "${APP_BUNDLE}"
echo "→ Zipping into ${ZIP_NAME} ..."
zip -r "${ZIP_NAME}" "${APP_BUNDLE}"
echo "Done: ${ZIP_NAME}"
- name: Build macOS App Bundle (zip)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Compile
run: GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o myapp-arm64 ./cmd/myapp
- name: Package and sign
run: |
bash package-macos.sh myapp-arm64 MyApp com.example.myapp $ assets/icon.icns
mv MyApp.zip myapp-macos-arm64.zip
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macos-arm64
path: myapp-macos-arm64.zip
codesign --deep recurses into nested .framework and .dylib files. For a simple Go binary this makes no difference, but it is good practice.xcrun notarytool..zip, not .dmg. A plain .zip avoids the extra “eject the disk image” confusion and lets users drag the .app wherever they want.codesign --force --sign - ./myapp-cli
xattr -d com.apple.quarantine ./myapp-cli # if testing locally
Official Apple documentation on these topics: