ADR-190: macOS Code Signing and TCC Compliance
Status
Accepted | February 13, 2026
Related ADRs
- ADR-066 — Context Automation Services (LaunchAgent architecture)
- ADR-134 — Unified Multi-LLM Watcher (codi-watcher v3.0)
- ADR-182 — File Integrity Registry (hash-based verification)
- ADR-186 — Protected Installation / User Data Separation
Context
CODITECT ships codi-watcher, a Rust binary that runs as a macOS LaunchAgent (every 5 minutes via --once mode). It monitors LLM session files, detects hash changes, and triggers incremental context extraction (/cx).
The Problem
macOS enforces TCC (Transparency, Consent, and Control) permissions on per-binary basis. When a LaunchAgent-spawned process accesses TCC-protected directories, macOS presents permission dialogs to the user:
| Dialog | TCC Category | Trigger |
|---|---|---|
| "would like to access files in your Documents folder" | kTCCServiceSystemPolicyDocumentsFolder | glob crate or Python subprocess traversing into ~/Documents via symlinks |
| "would like to access Apple Music" | kTCCServiceMedia | osascript display notification calls (since removed) |
| "would like to access data from other apps" | kTCCServiceSystemPolicyAllFiles | Python rglob traversing ~/Library/Containers/ and ~/Library/Group Containers/ via symlinks |
These dialogs appear repeatedly (every 5-minute poll cycle) and cannot be dismissed permanently without granting Full Disk Access (FDA).
Why This Happens
-
LaunchAgents don't inherit terminal TCC grants. Interactive terminal sessions inherit the terminal app's TCC permissions (e.g., Terminal.app or iTerm2 typically have FDA). LaunchAgent-spawned processes have no inherited TCC — they must be granted permissions individually.
-
macOS FDA UI rejects raw Mach-O binaries. The System Settings → Privacy & Security → Full Disk Access panel only accepts
.appbundles in its file picker. Raw command-line binaries cannot be added through the GUI. -
Ad-hoc/linker-signed binaries may not persist in TCC. macOS identifies binaries by their code signature for TCC tracking. Ad-hoc signatures (the default from
cargo build) have unstable identifiers that change on each build.
Constraints
- SIP is enabled on all supported machines — direct TCC.db modification is not possible
- No MDM enrollment — PPPC (Privacy Preferences Policy Control) profiles only work via MDM, not local install
tccutilcan only reset permissions, not grant them- There is no programmatic way to grant FDA on macOS with SIP enabled
Decision
1. Code Signing with Stable Identity
All CODITECT native binaries are signed with a proper code signing identity using a stable bundle identifier.
Development/Self-Hosted (current):
- Self-signed certificate: "CODITECT Local Signing" in login keychain
- Created via
openssl→ PKCS12 →security import→security add-trusted-cert -p codeSign - Identifier:
ai.coditect.codi-watcher
Customer Distribution (required for launch):
- Apple Developer ID certificate (requires Apple Developer Program membership, $99/year)
- Identifier:
ai.coditect.codi-watcher - Notarized via
notarytoolfor Gatekeeper trust - Enables installation without "unidentified developer" warnings
2. .app Bundle Wrapper
Native binaries that require FDA are packaged in minimal .app bundles so macOS FDA UI accepts them.
Bundle structure:
CoDiWatcher.app/
Contents/
Info.plist # Bundle metadata with stable identifier
MacOS/
codi-watcher # The actual Rust binary
Info.plist properties:
CFBundleIdentifier = ai.coditect.codi-watcher
CFBundleExecutable = codi-watcher
LSBackgroundOnly = true <!-- No dock icon -->
LSUIElement = true <!-- No menu bar -->
Location: ~/.coditect/bin/CoDiWatcher.app/ (inside protected installation)
3. LaunchAgent References .app Bundle Binary
The LaunchAgent plist executes the binary inside the .app bundle:
<key>ProgramArguments</key>
<array>
<string>/path/to/CoDiWatcher.app/Contents/MacOS/codi-watcher</string>
<string>--once</string>
</array>
macOS associates the process with the .app bundle's code signature, inheriting the FDA grant.
4. Build Pipeline Integration
The /sync workflow and install scripts must:
- Copy the compiled binary into
CoDiWatcher.app/Contents/MacOS/ - Re-sign the
.appbundle with the available signing identity - Preserve the existing FDA grant (macOS tracks by bundle identifier, not binary hash — re-signing with the same identifier retains the grant)
Customer Impact Analysis
Will This Work for Customers?
Yes, with the right signing strategy. The approach has three tiers depending on distribution method:
| Tier | Signing | Notarization | FDA Grant | User Experience |
|---|---|---|---|---|
| Tier 1: Developer ID | Apple Developer ID certificate | Yes (via notarytool) | One-time GUI toggle | Best — no Gatekeeper warnings, clean FDA UI entry |
| Tier 2: Self-Signed | Local self-signed cert (automated by installer) | No | One-time GUI toggle | Good — Gatekeeper warning on first launch, then clean |
| Tier 3: Ad-hoc | codesign -s - | No | May not persist across rebuilds | Poor — repeated Gatekeeper warnings, FDA may not stick |
Recommendation: Tier 1 for GA, Tier 2 as fallback.
What Customers Must Do (Unavoidable)
Regardless of signing tier, customers must manually grant FDA once through System Settings. There is no programmatic alternative on macOS with SIP enabled (which is the expected configuration). This is an Apple platform constraint, not a CODITECT limitation.
One-time setup (guided by installer):
- System Settings → Privacy & Security → Full Disk Access
- Click "+"
- Select
CoDiWatcher.app(installer opens the correct pane and copies path to clipboard) - Toggle on
Automated Installer Flow
The CODITECT installer (coditect-onboarding) should:
1. Build/copy codi-watcher binary
2. Create .app bundle with Info.plist
3. Sign with available identity:
a. Apple Developer ID (if available in keychain) → Tier 1
b. Self-signed "CODITECT Local Signing" (auto-create if missing) → Tier 2
4. Install .app to ~/.coditect/bin/CoDiWatcher.app/
5. Install LaunchAgent plist pointing to .app binary
6. Open System Settings FDA pane: open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
7. Print: "Please add CoDiWatcher.app to Full Disk Access to enable automated context extraction."
8. Wait for user confirmation
9. Load LaunchAgent
What Doesn't Work (Evaluated and Rejected)
| Approach | Why It Fails |
|---|---|
| Modify TCC.db directly | SIP blocks writes to TCC database |
| PPPC configuration profile | Only honored when deployed via MDM; locally-installed profiles are ignored |
tccutil grant | tccutil only supports reset, not grant |
osascript to click UI | Requires Accessibility permission (circular dependency) |
| Embedding in Terminal.app | Terminal already has FDA, but we need a standalone daemon |
| Running as root | FDA still required; running as root introduces security risks |
Linux Compatibility
Linux has no TCC equivalent. The codi-watcher binary runs as a systemd user service or cron job without any special permissions. The .app bundle and code signing are macOS-only concerns. The build pipeline should:
- macOS: Build → .app bundle → sign → install LaunchAgent
- Linux: Build → install to
~/.coditect/bin/→ install systemd service
Implementation
Certificate Management
Self-signed certificate creation (automated):
# Generate certificate
openssl req -x509 -newkey rsa:2048 \
-keyout /tmp/coditect-sign.key \
-out /tmp/coditect-sign.crt \
-days 3650 -nodes \
-subj "/CN=CODITECT Local Signing/O=AZ1.AI INC" \
-addext "keyUsage=critical,digitalSignature" \
-addext "extendedKeyUsage=critical,codeSigning" \
-addext "basicConstraints=critical,CA:false"
# Export to PKCS12 (use -legacy for macOS Keychain compatibility)
openssl pkcs12 -export -legacy \
-out /tmp/coditect-sign.p12 \
-inkey /tmp/coditect-sign.key \
-in /tmp/coditect-sign.crt \
-passout pass:$RANDOM_PASSWORD
# Import into login keychain
security import /tmp/coditect-sign.p12 \
-k ~/Library/Keychains/login.keychain-db \
-P $RANDOM_PASSWORD \
-T /usr/bin/codesign
# Trust for code signing
security add-trusted-cert -p codeSign \
-k ~/Library/Keychains/login.keychain-db \
/tmp/coditect-sign.crt
# Clean up
rm -f /tmp/coditect-sign.key /tmp/coditect-sign.crt /tmp/coditect-sign.p12
PKCS12 -legacy flag is required. OpenSSL 3.x defaults to PKCS12 v2 encryption which macOS Keychain cannot import. The -legacy flag forces PKCS12 v1 compatibility.
.app Bundle Creation
APP_DIR="$HOME/.coditect/bin/CoDiWatcher.app"
mkdir -p "$APP_DIR/Contents/MacOS"
# Copy binary
cp "$BUILD_DIR/codi-watcher" "$APP_DIR/Contents/MacOS/codi-watcher"
# Write Info.plist
cat > "$APP_DIR/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>CFBundleIdentifier</key>
<string>ai.coditect.codi-watcher</string>
<key>CFBundleName</key>
<string>CoDiWatcher</string>
<key>CFBundleDisplayName</key>
<string>CODITECT Context Watcher</string>
<key>CFBundleExecutable</key>
<string>codi-watcher</string>
<key>CFBundleVersion</key>
<string>3.1.0</string>
<key>CFBundleShortVersionString</key>
<string>3.1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSBackgroundOnly</key>
<true/>
<key>LSUIElement</key>
<true/>
</dict>
</plist>
EOF
# Sign
codesign -fs "CODITECT Local Signing" \
--identifier "ai.coditect.codi-watcher" \
"$APP_DIR"
Re-signing After Binary Updates
When /sync rebuilds the binary:
# Copy new binary into existing .app
cp "$NEW_BINARY" "$APP_DIR/Contents/MacOS/codi-watcher"
# Re-sign (same identifier preserves FDA grant)
codesign -fs "CODITECT Local Signing" \
--identifier "ai.coditect.codi-watcher" \
"$APP_DIR"
FDA persists across re-signs because macOS tracks FDA by bundle identifier (ai.coditect.codi-watcher), not by binary hash.
Consequences
Positive
- No more TCC permission dialogs from LaunchAgent
- Stable code signature identity across builds
- FDA grant persists across binary updates (same bundle ID)
- Foundation for Apple Developer ID signing and notarization at GA
.appbundle pattern reusable for other CODITECT daemons (context-indexer, backup-scheduled)
Negative
- One-time manual FDA grant required per machine (Apple platform constraint)
- Self-signed certificates trigger Gatekeeper warning on first launch (Tier 2)
.appbundle adds a layer of indirection to the binary path/syncmust re-copy and re-sign the binary inside the.app- Apple Developer Program costs $99/year for Tier 1 signing
Risks
- FDA revocation: macOS updates may reset TCC grants (observed in major OS upgrades). Mitigation: the installer can detect missing FDA and re-prompt.
- Certificate expiry: Self-signed certs expire after 10 years (3650 days). Developer ID certs expire after 5 years. Build pipeline should check cert validity.
- Notarization requirements tightening: Apple may require notarization for all binaries in future macOS versions. Tier 1 signing with notarization is forward-compatible.
Action Items
| Priority | Item | Owner | Target |
|---|---|---|---|
| P0 | Integrate .app bundle creation into /sync workflow | Framework | Next sprint |
| P0 | Integrate .app bundle creation into CODITECT-CORE-INITIAL-SETUP.py | Framework | Next sprint |
| P1 | Obtain Apple Developer ID certificate for AZ1.AI INC | Operations | Pre-GA |
| P1 | Add notarization step to release pipeline | DevOps | Pre-GA |
| P2 | Extend .app bundle pattern to context-indexer and backup-scheduled | Framework | Post-GA |
| P2 | Add FDA detection to health-check / /orient | Framework | Post-GA |
| P3 | MDM PPPC profile for enterprise customers | Enterprise | Post-GA |
Appendix: macOS TCC Categories Encountered
| Service Key | Dialog Text | When Triggered |
|---|---|---|
kTCCServiceMedia | "would like to access Apple Music" | osascript display notification from daemon context |
kTCCServiceSystemPolicyDocumentsFolder | "would like to access files in your Documents folder" | glob traversing symlinks into ~/Documents |
kTCCServiceSystemPolicyAllFiles | "would like to access data from other apps" | File operations in ~/Library/Containers/ |
Full Disk Access (kTCCServiceSystemPolicyAllFiles) is the superset permission that covers all of these.