Local Code Signing
How run.sh signs a local dev or named test bundle, and why the entitlements it
generates depend on the signing identity's Team ID. The one-line rule lives in
../AGENTS.md → Development Workflow → Building & Running.
Identity resolution order
run.sh (resolve_signing_identity) picks the first that exists:
OMI_SIGN_IDENTITY— explicit override.Apple Development— preferred, because local permissions stay stable.Developer ID Application.Omi Local Dev Signing— the stable self-signed local identity, created automatically if it does not exist.- Ad-hoc (
-) — only with both a named bundle andOMI_ALLOW_ADHOC_SIGN=1.
Each candidate is probed, not merely found: the script signs a throwaway file
with it before committing. An identity can be listed and still be unusable — see
§errSecInternalComponent — and choosing one on the strength of its existence
turns a two-second fallback into a build that fails a minute later, after the
compile and a 265 MB bundle copy.
Omi Local Dev Signing is created automatically
run.sh creates it on demand when no Apple identity is usable. Nothing to do, no
GUI, no password, and it works in CI and over ssh where the wizard below cannot.
It is generated with /usr/bin/openssl (LibreSSL, stock on every Mac) into its
own keychain — ~/Library/Keychains/omi-local-dev-signing.keychain-db, whose
password the script owns, which is what lets it grant codesign a partition
without a prompt. Set OMI_SKIP_LOCAL_SIGN_IDENTITY=1 to opt out.
Two details worth knowing if you touch that code:
- The PKCS#12 must use the legacy encoding (
-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1). OpenSSL 3's AES/SHA256 default imports asMAC verification failed during PKCS12 import (wrong password?), which is not a password problem at all. - The certificate needs
extendedKeyUsage = codeSigningandCA:false, orcodesigndeclines the identity.
A self-signed identity does not appear in security find-identity -v -p codesigning — that list filters on policy trust — yet codesign -s "Omi Local Dev Signing" works fine. Check usability by signing something, not by listing:
cp /usr/bin/true /tmp/probe && codesign --force --sign "Omi Local Dev Signing" /tmp/probe
To create it by hand instead (Keychain Access → Certificate Assistant → Create a
Certificate…): name Omi Local Dev Signing exactly, Identity Type Self Signed
Root, Certificate Type Code Signing.
The entitlement decision keys on the Team ID
Hardened-runtime library validation only lets a process load third-party code
whose signature carries the same Team ID. The Team ID comes from the signing
certificate's Organizational Unit. A self-signed certificate has no OU, so
codesign -dvv reports TeamIdentifier=not set — putting it in exactly the same
position as ad-hoc signing, even though it is a real named identity.
scripts/prepare-local-dev-entitlements.sh therefore classifies identities by
resolved Team ID, not by identity string:
| Mode | When | com.apple.security.cs.disable-library-validation |
|---|---|---|
teamless |
ad-hoc (-), or any identity whose signature has no Team ID |
granted |
team-scoped |
Apple Development, Developer ID Application: Based Hardware INC (9536L8KLMP) |
never granted |
--validate-identity is the single authority: it rejects configurations that
must not be used and prints the mode that then selects the entitlements, so a
configuration that validates is exactly the one that gets signed. Resolve a Team
ID with --identity-team-id "<identity>", which reads it back off a real
signature rather than guessing from the identity's name.
Production is unaffected. The release pipeline signs in Codemagic with the
Developer ID above (Team ID 9536L8KLMP) against the checked-in
Desktop/Omi.entitlements, and never invokes this script. Even if it did, a
Team-ID identity classifies team-scoped and keeps full library validation.
Symptom when it is wrong
Signing a bundle with a teamless identity but team-scoped entitlements produces an app that cannot launch:
dyld: Library not loaded: @rpath/Sparkle.framework/Versions/B/Sparkle
… mapping process and mapped file (non-platform) have different Team IDs
The same rule rejects the bundled Sentry and onnxruntime frameworks.
The failure hides from the usual checks:
codesign --verifypasses — the signature really is valid. What fails is a launch-time Team ID match, not signature integrity.open -aexits 0 and nothing starts.
The only way to see the real reason is to run the executable directly:
"/Applications/omi-<feature>.app/Contents/MacOS/Omi Computer" 2>&1 | head
Confirm a good bundle instead with:
codesign -dvv "/Applications/omi-<feature>.app" 2>&1 | grep -E 'Authority|TeamIdentifier|flags'
codesign -d --entitlements :- "/Applications/omi-<feature>.app" | grep -o 'disable-library-validation'
errSecInternalComponent: the session, not the certificate
An agent, a CI job, or any shell started outside the GUI login session hits this, and it is the single most misdiagnosed failure in this file:
build/<app>.app/Contents/Frameworks/Sparkle.framework: replacing existing signature
build/<app>.app/Contents/Frameworks/Sparkle.framework: errSecInternalComponent
The certificate is fine. The keychain is unlocked. You are the right user. What is missing is authorization to use the private key from a process that cannot be shown a dialog.
macOS normally resolves an unlisted caller by having SecurityAgent ask — "codesign
wants to use key X. Allow / Always Allow". A process whose launchctl managername
reports Background rather than Aqua has no window server connection, so that
dialog cannot be drawn and the Security framework fails the call immediately rather
than hanging. security reports the same condition as exit 36, and
security show-keychain-info says it outright: User interaction is not allowed.
Diagnose it in one line before assuming anything about the identity:
launchctl managername # Aqua = can prompt; Background = cannot
Two checks that will mislead you if you skip the one above:
security find-identity -v -p codesigninglists the identity — presence is not permission.security unlock-keychain -p <deliberately-wrong-password> login.keychain-dbreturns 0 when the keychain is already unlocked, so a locked keychain is not the explanation.
The fix, once, permanently
Grant codesign a standing partition on the key, from a terminal in the GUI session:
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
-l "Apple Development: YOUR NAME (XXXXXXXXXX)" ~/Library/Keychains/login.keychain-db
Omit -k so it prompts for the keychain password rather than recording it in shell
history. After this, signing works from any session, including background ones.
run.sh detects this failure and prints this remedy with your identity already
substituted, so you should never have to find this section from a raw error.
Auth dump/seed for developer bundles does not use the login keychain
Developer bundles (Omi Dev, named omi-* apps, ad-hoc / Apple Development local
builds) store Firebase tokens in a JSON file under Application Support
(developer-secrets/<bundle-id>.json). scripts/omi-auth-dump.sh and
scripts/omi-auth-seed.sh read and write that file, so they work from a
Background session with no login-keychain access. Rebuilds never prompt
SecurityAgent for those tokens.
Shipped bundles (com.omi.computer-macos and com.omi.computer-macos.beta)
still use the login keychain. Dumping a production-family source still calls
security find-generic-password and therefore still needs an Aqua session or a
partition-list grant on that item.
Do not "fix" a launch failure with ad-hoc signing
OMI_ALLOW_ADHOC_SIGN=1 looks like it works, and it is worse. An ad-hoc
signature has no stable designated requirement, so it invalidates that bundle's
own Screen Recording TCC grant: Rewind silently stops capturing and the app
reports 0 screen moments while still appearing in the Settings list. A stable
self-signed identity pins its certificate in the designated requirement, so
permissions survive rebuilds — that is the whole reason to prefer it.
Tests
cd desktop/macos && bash tests/test-prepare-local-dev-entitlements.sh
Registered as manifest check desktop-local-dev-entitlements (macOS, local
and ci lanes). It drives the real classification through the script's CLI and
runs run.sh's own resolve_signing_identity and
local_entitlements_fallback_reason function bodies against injected identity
metadata.