export(
appdir = "my-app",
destdir = "build",
sign = TRUE
)Signing tells the operating system who built this. Notarization tells the OS that Apple has seen it. Without either, macOS Gatekeeper blocks your app as an “unidentified developer” and Windows SmartScreen warns on every download. This guide covers what you need, what it costs, and how to wire it up. For the wider security picture, see Security Considerations.
Users can click past warnings, but the friction is real. Three reasons to sign anyway:
electron-updater verifies signatures on macOS and Windows before applying an update, so unsigned builds lose the update path entirely.The concrete outcomes differ per platform:
| macOS | Windows | Linux | |
|---|---|---|---|
| Required? | Strongly recommended | Strongly recommended | Optional |
| Cost | $99/year (Apple Developer) | $200 to $700/year (CA cert) | None (planned) |
| Certificate | Developer ID Application | OV or EV code signing | Not yet supported |
| Notarization | Yes (macOS 10.15+) | N/A | N/A |
Pass sign = TRUE to export() and electron-builder does the rest:
export(
appdir = "my-app",
destdir = "build",
sign = TRUE
)sign = TRUE tells electron-builder to use whatever credentials it finds in the environment. It writes the signing configuration into package.json, sets the macOS identity, enables notarization (when configured), and points Windows at the certificate. It does not create certificates. If the credentials are missing, it warns and hands off to electron-builder, which then fails at the signing step with a specific error.
The function argument overrides the config file. export(..., sign = TRUE) signs even when _shinyelectron.yml says sign: false. You can also enable signing purely through the config:
signing:
sign: true
mac:
identity: "Developer ID Application: Your Name (TEAMID)"
team_id: "XXXXXXXXXX"
notarize: trueCredentials themselves (certificate password, Apple ID password, etc.) must stay in environment variables. Never commit them to the config file.
Before you kick off a full build, verify your setup with app_check():
app_check("my-app", sign = TRUE)That catches missing certificates or environment variables cheaply, without going through the full build.
Cost: $99/year for Apple Developer Program membership, plus the time to generate an app-specific password.
What you need:
Wiring it up. Export the Developer ID Application certificate from Keychain Access as a .p12 file, then set these environment variables:
# Certificate (path to .p12 file, or base64-encoded content)
export CSC_LINK="/path/to/certificate.p12"
export CSC_KEY_PASSWORD="your-certificate-password"
# Notarization credentials
export APPLE_ID="your@apple.id"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="XXXXXXXXXX"Then run export(..., sign = TRUE). electron-builder drives the full signing and notarization flow. You do not call codesign or xcrun notarytool yourself.
Without signing: Gatekeeper blocks the app on first launch with “App can’t be opened because Apple cannot check it for malicious software.” To open an unsigned .app during local development, strip the quarantine flag:
xattr -cr /path/to/YourApp.appOr right-click the app in Finder and choose Open to bypass Gatekeeper once.
Cost: $200 to $700/year depending on validation level. An EV certificate builds SmartScreen trust immediately; an OV certificate builds reputation over several weeks, during which users still see warnings.
What you need: An OV or EV code signing certificate from a commercial CA. Common options:
EV certificates ship on a hardware token (USB dongle or HSM) and require a different signing workflow when used from CI; plan for that up front.
Wiring it up.
export CSC_LINK="/path/to/certificate.pfx"
export CSC_KEY_PASSWORD="your-certificate-password"Then:
export(
appdir = "my-app",
destdir = "build",
platform = "win",
sign = TRUE
)Without signing: unsigned Windows installers trigger SmartScreen on every download. There is no workaround short of signing the build.
Cost: none today; free (GPG key) when implemented.
Status: not yet implemented. The gpg_sign: true flag and GPG_KEY environment variable are reserved for a future release. Setting them has no effect today: no signature is produced, and a console warning is emitted if GPG_KEY is not set. Linux AppImages run without any OS-enforced signature check, so the absence of GPG signing has no effect on whether the app launches.
Configuration (reserved for future use).
export GPG_KEY="your-gpg-key-id"signing:
sign: true
linux:
gpg_sign: trueWithout signing: nothing visibly changes. AppImage files do not require a signature to run, so users can open the app without any additional steps.
CI is where signing usually lives in the long run: no human holds the keys on a laptop, and every tagged release is signed automatically. Store credentials as GitHub Actions secrets (Settings → Secrets and variables → Actions), then pass them to the coatless-actions/shiny-to-electron action and flip sign on. The action reads credentials from the job environment, so set them on the job, not on the step:
jobs:
build:
runs-on: ${{ matrix.os }}
env:
CSC_LINK: ${{ secrets.CSC_LINK }} # base64 .p12 certificate
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
steps:
- uses: actions/checkout@v7
- uses: coatless-actions/shiny-to-electron@v1
with:
appdir: app
app-name: MyApp
platform: ${{ matrix.platform }}
arch: ${{ matrix.arch }}
sign: 'true'With sign: 'true' and those secrets present, macOS builds are signed with your Developer ID and notarized, taking the team id from APPLE_TEAM_ID.
Important
macOS and Windows certificates use different files.
CSC_LINKandCSC_KEY_PASSWORDmust point at the right one for the runner. In a matrix build, set them per-platform in a conditionalenvblock or split the build into separate jobs so each runner gets only its own credentials.
For the full workflow template (toolchain setup, matrix jobs, artifact upload), see Building with GitHub Actions.
A working rhythm:
sign = FALSE (the default). Use xattr -cr on macOS when you need to launch your own unsigned build.sign = TRUE for release builds, either in the export() call or through signing: sign: true in _shinyelectron.yml.| Development | Production | |
|---|---|---|
sign |
FALSE (default) |
TRUE |
| Credentials needed? | No | Yes |
| Build speed | Fast | Slower: notarization takes 1 to 5 minutes |
| OS warnings? | Yes (use xattr -cr on macOS) |
No |
One canonical table for every variable the signing pipeline reads.
| Variable | Platform | Purpose |
|---|---|---|
CSC_LINK |
macOS, Windows | Path to .p12 or .pfx certificate (or base64-encoded content) |
CSC_KEY_PASSWORD |
macOS, Windows | Password for the .p12 or .pfx file |
APPLE_ID |
macOS | Apple ID email used for notarization |
APPLE_APP_SPECIFIC_PASSWORD |
macOS | App-specific password from appleid.apple.com |
APPLE_TEAM_ID |
macOS | 10-character Apple Developer Team ID |
GPG_KEY |
Linux | GPG key ID for AppImage signing (reserved; not yet implemented) |
_shinyelectron.yml reference, signing included.