[BUG] Claude Desktop macOS: ~107s startup beachball — ReadMacOSKeychainCertificates blocks main thread

Open 💬 0 comments Opened Jul 9, 2026 by terry-murphy-zenith

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Desktop

What's Wrong?

So the main/UI thread synchronously enumerates the login keychain and runs SecTrustEvaluateWithError (revocation-enabled) on every cert via blocking XPC to trustd.

Root cause on my machine

  • Login keychain had 1,359 certificates (junk CN=Test Certificate entries leaked by a test suite over months).
  • 1,359 certs × ~80ms per synchronous trust evaluation ≈ 108s, matching the observed 107s stall almost exactly.
  • Removing the excess certs → startup dropped to ~instant. Confirmed by the event-loop-stall line disappearing from main.log.

Important: the CLI workaround does not fix the desktop app

  • CLAUDE_CODE_CERT_STORE=bundled (the CLI workaround from #53660) has no effect on the desktop app — the Electron/Node system-store enumeration isn't gated by it — so GUI users have no env-var mitigation, only manual keychain cleanup.

Requests

  1. Don't perform keychain enumeration / trust evaluation on the UI thread — move it off-main and make it async.
  2. Use a non-revocation policy (SecPolicyCreateBasicX509) or read anchors directly via SecTrustSettingsCopyCertificates, instead of SecTrustEvaluateWithError, since this is collecting trust anchors, not validating chains.
  3. Honor CLAUDE_CODE_CERT_STORE=bundled in the desktop app too.
  4. This scales badly: the original report is ~10s for 18 certs; here it's ~107s for ~1,359. A bloated (but otherwise legitimate) keychain shouldn't be able to freeze the app for nearly two minutes.

What Should Happen?

Claude Desktop application should not beachball on a mac for almost 2 minutes

Error Messages/Logs

### Error messages / logs

**1. `~/Library/Logs/Claude/main.log` — the freeze (a ~107s gap, then the stall is reported):**


22:08:13 [info] [remote-tools-device] connecting DO bridge with: get_device_info, device_list_dir, ... Filesystem__list_allowed_directories

   << ~107 seconds, UI fully frozen (beachball) >>

22:10:00 [warn] [event-loop-stall] main process blocked for 107153ms [likely sleep/wake] (total 0, cumulative 0ms, rss 409MB)
22:10:00 [warn] [my-access] fetch failed { error: DOMException [TimeoutError]: The operation was aborted due to timeout }
22:10:00 [warn] [buddy-ble] mainView not ready: Timed out waiting for mainView to become ready
22:10:00 [error] [RemotePluginManager] Sync failed: This operation was aborted (AbortError)
22:10:00 [info] [EventLogging] Flushing 38 events


The `[likely sleep/wake]` tag is a misattribution — the machine was awake and actively launching. The surrounding timeouts (`my-access`, `RemotePluginManager`) are collateral: their timers couldn't fire while the loop was blocked, so they all abort the instant it unblocks. The line that names the actual blocker is the `sample` below, not these.

**2. `sample` of the main process captured during the freeze — 14973/14973 samples on the same stack:**

<details>
<summary>Main-thread stack (com.apple.main-thread / CrBrowserMain)</summary>


Process:     Claude [68256]
Version:     1.19367.0
Code Type:   ARM64
OS Version:  macOS 26.5.1 (25F80)
Analysis Tool: /usr/bin/sample  (1ms interval, 14973 samples)

14973 CrBrowserMain
  ... Electron / Node ...
    node::crypto::GetSystemCACertificates
      node::crypto::GetOpenSSLSystemCertificates
        node::crypto::ReadMacOSKeychainCertificates
          node::crypto::IsCertificateTrustedForPolicy
            node::crypto::IsCertificateTrustValid
              SecTrustEvaluateWithError            (Security)
                SecTrustEvaluateInternal
                  SecTrustEvaluateIfNecessary
                    _dispatch_lane_barrier_sync_invoke_and_complete   (libdispatch)
                      securityd_send_sync_and_do   (Security)
                        security_fw_send_message_with_reply_sync_inner
                          xpc_connection_send_message_with_reply_sync (libxpc)
                            dispatch_mach_send_with_result_and_wait_for_reply
                              mach_msg -> mach_msg2_trap   [blocked, waiting on trustd]

</details>

100% of the freeze is the main/UI thread synchronously enumerating the login keychain and calling `SecTrustEvaluateWithError` (revocation-enabled) on each certificate via **blocking XPC to `trustd`**.

**3. Root cause confirmed — bloated login keychain:**


$ security find-certificate -a ~/Library/Keychains/login.keychain-db \
    | grep -o '"labl"<blob>="[^"]*"' | sort | uniq -c | sort -rn | head
1359 "labl"<blob>="Test Certificate"
   2 "labl"<blob>="localhost"


1,359 certs × ~80ms per synchronous trust evaluation ≈ **108s**, matching the observed 107,153ms stall almost exactly.

**4. Fix / confirmation:**


# after deleting the 1,359 certs by SHA-1 hash:
$ security find-certificate -a ~/Library/Keychains/login.keychain-db \
    | grep -c '"labl"<blob>="Test Certificate"'
0


Startup is now near-instant and `main.log` no longer contains any `[event-loop-stall] ... blocked` line.

**Note:** `CLAUDE_CODE_CERT_STORE=bundled` (tried via direct launch and `launchctl setenv`) had **no effect** on the desktop app — the Electron/Node path enumerates the system store via `--use-system-ca` regardless, so GUI users have no env-var mitigation, only manual keychain cleanup.

Steps to Reproduce

Steps to reproduce

The freeze scales with the number of certificates in the login keychain, because each one is trust-evaluated synchronously on the UI thread at startup. You can reproduce it by seeding the keychain and cold-launching.

⚠️ Back up your keychain first — this adds throwaway certs to your login keychain: cp ~/Library/Keychains/login.keychain-db ~/Desktop/login.keychain-db.bak

1. Record a baseline. Quit Claude Desktop fully, launch it, and note it starts quickly. Confirm no stall is logged:

tail -n 80 ~/Library/Logs/Claude/main.log | grep -i "event-loop-stall"   # expect: no output

2. Seed N distinct certificates into the login keychain (each openssl run makes a unique cert, so the keychain doesn't dedupe them):

for i in $(seq 1 500); do
  openssl req -x509 -newkey rsa:2048 -nodes -keyout /tmp/k.pem -out /tmp/c.pem \
    -days 1 -subj "/CN=Repro Test Certificate" >/dev/null 2>&1
  security add-certificates -k ~/Library/Keychains/login.keychain-db /tmp/c.pem >/dev/null 2>&1
done

# confirm the count
security find-certificate -a ~/Library/Keychains/login.keychain-db \
  | grep -c '"labl"<blob>="Repro Test Certificate"'

3. Cold-launch and measure. Quit Claude Desktop fully (Cmd+Q, confirm it's gone from Activity Monitor), relaunch, and observe the beachball. Then:

tail -n 80 ~/Library/Logs/Claude/main.log | grep -i "event-loop-stall"

You'll see a main process blocked for <N>ms line whose duration grows roughly linearly with the cert count.

4. (Optional) Capture the stack to confirm it's the same code path:

open -a Claude
sleep 15
sample "$(pgrep -xn Claude)" 20 -file ~/Desktop/claude-hang.txt

The main thread will sit in ReadMacOSKeychainCertificates -> SecTrustEvaluateWithError -> xpc ... -> mach_msg.

Observed scaling (per-cert cost is environment-dependent — corporate NetworkExtension filters make it far worse):
| Certs in login keychain | Startup freeze |
|---|---|
| ~0 (baseline) | none / instant |
| 18 (corporate, per #53660) | ~10s (~530ms/cert) |
| 1,359 (this report, plain home Mac) | ~107s (~80ms/cert) |

Cleanup — remove the seeded certs (-c name-delete fails on duplicate names, so delete by SHA-1 hash):

security find-certificate -a -c "Repro Test Certificate" -Z ~/Library/Keychains/login.keychain-db \
  | awk '/SHA-1 hash:/ {print $3}' \
  | while read -r h; do
      security delete-certificate -Z "$h" ~/Library/Keychains/login.keychain-db >/dev/null 2>&1
    done

# verify back to 0
security find-certificate -a ~/Library/Keychains/login.keychain-db \
  | grep -c '"labl"<blob>="Repro Test Certificate"'

Expected: startup time should be independent of keychain size — cert-anchor collection should not block the UI thread or run per-cert revocation-enabled trust evaluation.
Actual: startup freezes for a duration proportional to the number of certificates in the login keychain.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

1.19367.0

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

<img width="512" height="512" alt="Image" src="https://github.com/user-attachments/assets/503ce5f9-acd1-4356-8da0-40e4419ec212" />

View original on GitHub ↗