Skip to main content

ADR-190: macOS Code Signing and TCC Compliance

Status

Accepted | February 13, 2026

  • 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:

DialogTCC CategoryTrigger
"would like to access files in your Documents folder"kTCCServiceSystemPolicyDocumentsFolderglob crate or Python subprocess traversing into ~/Documents via symlinks
"would like to access Apple Music"kTCCServiceMediaosascript display notification calls (since removed)
"would like to access data from other apps"kTCCServiceSystemPolicyAllFilesPython 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

  1. 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.

  2. macOS FDA UI rejects raw Mach-O binaries. The System Settings → Privacy & Security → Full Disk Access panel only accepts .app bundles in its file picker. Raw command-line binaries cannot be added through the GUI.

  3. 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
  • tccutil can 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 importsecurity 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 notarytool for 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:

  1. Copy the compiled binary into CoDiWatcher.app/Contents/MacOS/
  2. Re-sign the .app bundle with the available signing identity
  3. 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:

TierSigningNotarizationFDA GrantUser Experience
Tier 1: Developer IDApple Developer ID certificateYes (via notarytool)One-time GUI toggleBest — no Gatekeeper warnings, clean FDA UI entry
Tier 2: Self-SignedLocal self-signed cert (automated by installer)NoOne-time GUI toggleGood — Gatekeeper warning on first launch, then clean
Tier 3: Ad-hoccodesign -s -NoMay not persist across rebuildsPoor — 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):

  1. System Settings → Privacy & Security → Full Disk Access
  2. Click "+"
  3. Select CoDiWatcher.app (installer opens the correct pane and copies path to clipboard)
  4. 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)

ApproachWhy It Fails
Modify TCC.db directlySIP blocks writes to TCC database
PPPC configuration profileOnly honored when deployed via MDM; locally-installed profiles are ignored
tccutil granttccutil only supports reset, not grant
osascript to click UIRequires Accessibility permission (circular dependency)
Embedding in Terminal.appTerminal already has FDA, but we need a standalone daemon
Running as rootFDA 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
  • .app bundle 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)
  • .app bundle adds a layer of indirection to the binary path
  • /sync must 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

PriorityItemOwnerTarget
P0Integrate .app bundle creation into /sync workflowFrameworkNext sprint
P0Integrate .app bundle creation into CODITECT-CORE-INITIAL-SETUP.pyFrameworkNext sprint
P1Obtain Apple Developer ID certificate for AZ1.AI INCOperationsPre-GA
P1Add notarization step to release pipelineDevOpsPre-GA
P2Extend .app bundle pattern to context-indexer and backup-scheduledFrameworkPost-GA
P2Add FDA detection to health-check / /orientFrameworkPost-GA
P3MDM PPPC profile for enterprise customersEnterprisePost-GA

Appendix: macOS TCC Categories Encountered

Service KeyDialog TextWhen 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.