Skip to content

Spike: revisit multi-host transport decision — TCC diagnosis and Developer ID signing feasibility #196

Description

@deverman

User outcome

A Claude Desktop (or other desktop-app-hosted MCP client) user gets either a
documented, supported FocusRelay setup path, or a documented, evidence-backed
"not supported yet" with a tracked follow-up — instead of a silent bridge
timeout. The spike's deliverable is a decision, not code: an updated
transport decision document and, if warranted, a follow-up implementation
issue.

Validation impact

docs — the spike produces diagnostic evidence and an updated decision
document only. No production code changes; any implementation happens under
a follow-up issue with its own (likely transport-reliability) impact.

Current evidence

  • FocusRelay works under terminal-launched MCP hosts (Claude Code, OpenCode)
    and fails under Claude Desktop on macOS 26/27: the server starts and
    handshakes, but bridge round trips never complete.
  • Verified on a failing machine: Claude Desktop's app bundle is not
    sandboxed (no com.apple.security.app-sandbox entitlement), while
    OmniFocus 4 is. The failure layer is therefore not app-sandbox
    inheritance but macOS TCC App Data protection
    (kTCCServiceSystemPolicyAppData, macOS 14+) over another app's
    ~/Library/Containers — which is exactly where the FocusRelay IPC
    rendezvous lives, because Omni Automation URL-scheme scripts can only
    write inside OmniFocus's own container.
  • The grant follows the requesting binary's code identity, and Full Disk
    Access does not substitute (SystemPolicyAllFiles
    SystemPolicyAppData).
  • Direct architectural analog: omnifocus-operator (same
    container-rendezvous design) has this exact bug open —
    HelloThisIsFlo/omnifocus-operator#2.
    Their diagnosis: ad-hoc or self-signed identities get the grant recorded
    with auth_value=5 (not honored), causing endless re-prompts or denials;
    their planned fix is a Developer ID-signed + notarized launcher.
  • Corroboration from Claude Desktop MCP issues:
    anthropics/claude-code#27557,
    #36832,
    #63032,
    #66216
    App Data prompts fire but grants don't persist through unsigned helper
    chains; Apple Events (osascript) remains promptable and functional.
  • Competitors using osascript/JXA with stdout return paths
    (themotionmachine/OmniFocus-MCP, jqlts1/omnifocus-mcp-enhanced,
    vitalyrodnenko/OmnifocusMCP) report zero permission-wall issues:
    their data path never crosses the OmniFocus container boundary.
  • Constraint: the roadmap Standing Decision (finalized in Consolidate on plugin-url and move pure JXA out of the shipped runtime #80,
    docs/transport-decision-2026-03-13.md) says plugin URL dispatch through
    the Bridge plugin is the only architecture. This spike explicitly
    revisits that decision with new evidence; the decision doc is the
    deliverable that either reaffirms or amends it.

Prerequisite

Options 1 and 2 below require an Apple Developer Program membership
(US$99/year)
for Developer ID signing and notarization. Task 1 (the TCC
diagnostic) is free and must run first — its result determines whether
signing can help at all, before any money is spent. Option 3 requires no
signing.

Proposed scope (ordered, with kill/keep gates)

  1. TCC attribution diagnostic (free, mandatory first). Under Claude
    Desktop, trigger a bridge call while capturing
    log stream --predicate 'subsystem == "com.apple.TCC"' and classify the
    recorded auth_value for the requesting identity. The critical unknown:
    is the focusrelay child process attributed to its own code identity or
    to the host app's responsible process? This gates everything after it.
  2. Option 1 — Developer ID sign + notarize the existing Homebrew binary.
    Keep if the App Data grant is honored (auth_value=2, prompt names
    FocusRelay): zero architecture change, zero hot-path cost. Kill if
    responsible-process attribution routes the grant to the host app.
    Includes pipeline scoping: codesign + notarytool + stapling in
    scripts/package-release.sh, and whether the tap tarball
    (deverman/homebrew-focus-relay) preserves signature and notarization
    through download/quarantine.
  3. Option 2 — signed LaunchAgent broker (only if Option 1 dies). A
    launchd-managed singleton owns the IPC client under its own TCC
    identity; focusrelay serve becomes a thin stdio shim. Also solves
    multi-host contention (cf.
    themotionmachine/OmniFocus-MCP#80
    arriving at the same launchd-singleton idea). Installer/uninstaller UX
    folds into Add guided setup for Homebrew-installed FocusRelay #92 guided setup — no second installer path. Kill on
    unacceptable setup friction; a background agent users don't understand
    is a real adoption cost.
  4. Option 3 — osascript/JXA stdout return path (Desktop-only degraded
    mode).
    Competitor-proven under Claude Desktop; still executes OmniJS
    inside OmniFocus (documented-API contract holds); requires no signing.
    Conflicts with the Consolidate on plugin-url and move pure JXA out of the shipped runtime #80 Standing Decision and adds per-request osascript
    spawn cost — evaluate strictly as a documented fallback for hosts where
    the container is unreachable, never the terminal-client path.
  5. Decision doc + follow-up. Update docs/transport-decision-2026-03-13.md
    (or successor) with: verbatim TCC log evidence (usernames stripped),
    codesign -dv/spctl -a output per tested identity, a host × option
    result matrix, terminal-path latency proof-of-no-change, and an explicit
    stands-or-amended verdict on the Standing Decision. File the follow-up
    implementation issue if any option survives.

Performance budget

The terminal-client hot path (Claude Code, OpenCode, Codex CLI) must be
code-path-identical before and after any multi-host change. Zero
per-request regression is tolerated; Desktop enablement may only add cost
to a Desktop-specific path. Canary-profile before/after evidence on the
unchanged terminal path is required in the follow-up issue.

Safety and compatibility

  • No production code changes in this spike; no release invalidation.
  • Diagnostic steps read TCC state and logs only; no TCC.db modification.
  • Depends on the IPC hardening issue landing first: hardening defines the
    on-disk contract and permissions any broker or signed binary would own.
  • Add guided setup for Homebrew-installed FocusRelay #92 guided setup should not finalize its installer flow until this
    spike's signing verdict lands, since a Developer ID pipeline (or a
    LaunchAgent) directly shapes it.

Acceptance criteria

  • TCC diagnostic evidence captured and classified (service, identity,
    attribution, auth_value), with usernames stripped from paths.
  • Each option has an explicit keep/kill verdict backed by recorded
    evidence; untested options state why.
  • Transport decision doc updated with the evidence list above and an
    explicit verdict on the Standing Decision.
  • Follow-up implementation issue filed (or an explicit "no viable path;
    revisit when X changes" recorded).
  • No code merged from this spike.

Non-goals

  • Implementing any transport change.
  • Changing the default plugin-URL architecture for terminal clients.
  • Installer/uninstaller work (belongs to Add guided setup for Homebrew-installed FocusRelay #92).
  • Revisiting JXA for terminal clients.
  • README changes beyond linking this issue (handled separately).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions