Skip to content

Fantastical "Couldn't communicate with a helper application" After an App Store Update

Symptom

Fantastical shows this on every sync attempt:

Sync error (Google)
Fantastical encountered an error while communicating with the server.
Try your operation again. If that fails, quit and relaunch the application and try again.
Error: Couldn't communicate with a helper application.
https://apidata.googleusercontent.com/caldav/v2/…

Retry fails every time. Closing the main window and reopening it does not help. Calendars stop updating silently — nothing surfaces until a window is opened or a sync is forced.

Despite the wording and the Google URL, this is not a server, network, or account problem. Nothing is wrong with the calendar service.

Cause

Fantastical runs as three cooperating processes:

  • the app itself (Fantastical),
  • a login-item helper that owns the menu bar icon and keeps running after the app is quit (…com.flexibits.fantastical2.mac.helper),
  • an XPC sync service the helper talks to (com.flexibits.fbcaldav.FBCalDAV-XPC).

The sync service checks the code signature of whatever connects to it. When the Mac App Store updates Fantastical it rewrites the app bundle on disk — including the embedded helper — while the old helper keeps running. macOS can no longer match that running process to code on disk, the identity lookup fails, and the sync service refuses every connection:

com.flexibits.fbcaldav.FBCalDAV-XPC  (XPCSupport) xpc_support_check_token: … status: -67065
…helper  [FBSyncQueue.m] Prompting to retry command for error: … NSCocoaErrorDomain Code=4097
         "connection to service named com.flexibits.fbcaldav.FBCalDAV-XPC"

security error -67065 → host has no guest with the requested attributes.

Three things make this persist rather than self-correct:

  1. The helper is designed to outlive the app. It is a launchd-managed login item, deliberately decoupled from the app's lifecycle so background sync continues while the app is closed. Quitting the app does not quit it.
  2. macOS does not kill a process when its executable is replaced. The process keeps running from what it already loaded; replacing files underneath it changes nothing about its execution.
  3. The App Store's "quit to update" prompt only targets the visible app. If the app is closed when the update installs, nothing is prompted and nothing is restarted — yet the helper is still running and is now stale.

Because the helper is a login item, it can run for days and outlive several updates, which makes the problem look like something that "builds up with uptime". It does not — it is one discrete event.

A reboot always clears it, since everything restarts from the new bundle. The machine most at risk is therefore the one rebooted least.

Diagnosis

The decisive test is a timestamp comparison, not a signature check:

# 1. When did the helper start?
ps -axo pid,lstart,comm | grep -i -E 'fantastical2.mac.helper|FBCalDAV-XPC' | grep -v grep

# 2. When was the app last rewritten on disk?  Later than (1) = stale helper.
stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' \
  /Applications/Fantastical.app/Contents/_CodeSignature/CodeResources

# 3. Can the running helper still be validated?  (<pid> from step 1)
codesign --verify -v +<pid>
#   stale:   "host has no guest with the requested attributes"
#   healthy: "dynamically valid"

Count attempts, not just errors. Filtering the log to error level showed "one failure in three hours", which read as a rare glitch. Including info-level lines showed the helper had made only three sync attempts in that window and all three failed — sync had been dead the whole time. A low error count on a quiet process means few attempts, not a low failure rate.

log show matches its own invocation. A verification pass that counts matching lines will count the log show command itself, because its arguments contain the very keywords being searched for. Read the matched lines; do not trust a bare count.

Fix

Quit both the app and the menu bar helper, then reopen:

  • Menu bar icon → Quit Fantastical (hold Option for "Quit Completely" if offered), or from a shell:
osascript -e 'tell application id "com.flexibits.fantastical2.mac" to quit'
pkill -TERM -f 'fantastical2.mac.helper|FBCalDAV-XPC|libical-XPC'
open -a /Applications/Fantastical.app

If the app is not running, killing the helper alone is enough — launchd respawns it from the new bundle within seconds.

Verify: new PIDs with start times after the bundle's modification time, and sync connections with no -67065.

Restarting only the sync service does not fix it. The stale process is the helper.

Prevention

The helper should notice its own bundle was replaced and exit, letting launchd restart it from the new code — an invisible restart. It does not. Until it does, a small periodic guard can do that check externally.

The logic is the same comparison used in diagnosis: take the newest mtime among the bundle's signature-bearing files (Info.plist, _CodeSignature/CodeResources, Contents/MacOS/*, and the bundle directory), compare it against each process's start time from ps -o lstart=, and restart anything older.

# core of the check
bundle=$(stat -f %m /Applications/Fantastical.app/Contents/_CodeSignature/CodeResources)
for pid in $(pgrep -f 'com.flexibits.fantastical2.mac.helper'); do
  started=$(date -j -f '%a %b %e %H:%M:%S %Y' \
    "$(ps -o lstart= -p "$pid" | tr -s ' ' | sed 's/^ *//;s/ *$//')" +%s)
  [ "$started" -lt "$bundle" ] && kill "$pid"   # launchd respawns it
done

Run it from a LaunchAgent with RunAtLoad and a StartInterval of a few minutes.

Two design points worth keeping:

  • Compare against the newest of several bundle files, not just the bundle directory. A single mtime is a guess; a maximum across the signature-bearing files means a partial or unusual update still trips the check.
  • Be asymmetric about what you restart. A stale helper can be killed silently — it holds no user-visible state. A stale main app should only be reported, never killed, because that can discard an open event editor or a half-typed entry. Same staleness, very different blast radius.

Other options, for completeness:

  • Relaunch Fantastical fully after it updates (reliable, but you have to notice).
  • Turn off App Store automatic updates (affects every App Store app).
  • Report it to the vendor — this is properly the helper's job.

Notes for anyone testing this

Two sandbox behaviours will mislead you while building or verifying a fix, and neither reflects the real bug:

  • Modifying a running binary gets it SIGKILLed (Killed: 9) — whether you touch it in place or swap it for a new inode. The real app survives having its bundle rewritten, which is the whole reason this bug exists; a test that kills its own fake process is testing the sandbox, not the behaviour.
  • Executing a copy of a system binary from a temp directory is killed by the kernel on Apple Silicon, because the copy loses its platform-binary standing. Build fake processes with exec -a <name> sleep … instead, which also keeps a test's fake helper from matching the real process pattern.

Confirmation

Observed on two Macs on the same day, which is what established the mechanism rather than leaving it a plausible story:

helper started bundle rewritten result
Machine A 08:59 09:11 broken; relaunch at 12:39 cleared it
Machine B two days earlier 13:20 broken; killing the stale helper cleared it

On machine B the main app was not even running at the time — the orphaned helper alone was enough to guarantee the next sync would fail, which is why the error appeared to come out of nowhere.

One loose end: after a relaunch, codesign --verify -v +<pid> prints dynamically valid and a second line invalid signature (code or signature have been modified) for the new processes, although the on-disk app verifies clean (--deep --strict; spctl accepts it as Mac App Store). Unexplained, and sync is unaffected.