[BUG] macOS: `tls.getCACertificates("system")` blocks startup ~9-10s on corporate-managed Macs (regression in 2.1.119)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
Every invocation of claude on my corporate-managed macOS machine blocks for ~9-10 seconds during startup before the TUI becomes responsive (for interactive use) or the first API request fires (for claude -p). Of that, ~9.7s is spent inside a single call to tls.getCACertificates("system") during CA store initialization. The API returns 18 certs, and each XPC round-trip to securityd takes ~530 ms.
I noticed it first in interactive mode (waiting 10 s for the prompt to appear every time I opened Claude); claude -p hi is just an easy way to time the cost deterministically.
The same underlying SecTrustEvaluateWithError API, called directly via ctypes on the same machine, evaluates 159 system domain certificates in 51 ms total (~0.3 ms/cert). Node.js 25's tls.getCACertificates("system") wrapper on the same machine completes in 1.49 s, 77 certs (~20 ms/cert). Claude's wrapper is ~25× slower than Node's and ~186× slower in aggregate than direct ctypes.
Root cause (diagnosed via log stream on trustd + securityd during the hang — see "Root cause diagnosis" below): Claude's wrapper calls SecTrustEvaluateWithError with a revocation-enabled policy, while my ctypes comparison used SecPolicyCreateBasicX509 (revocation off). The revocation policy makes trustd attempt AIA/OCSP/CRL fetches for every cert, which fire ~650 outbound network flows. On a machine with a corporate NetworkExtension content filter, each denied flow still pays the filter's per-flow cryptographic signing cost — that's where the 10 seconds go. On a personal Mac without a corporate NE filter, the same code path is much cheaper because flow denial is ~free.
The slowdown is triggered by the default CLAUDE_CODE_CERT_STORE=bundled,system. Forcing CLAUDE_CODE_CERT_STORE=bundled drops startup from ~15 s to ~5 s. This is a workaround, not a fix — it trades startup latency for loss of system-keychain trust. Any TLS endpoint whose chain depends on a CA that's only in the macOS keychain (not in NODE_EXTRA_CA_CERTS and not a Mozilla root) will stop working until its CA is added to the extra-CA file. In my environment NODE_EXTRA_CA_CERTS only contains the break-and-inspect proxy CA — a subset of what the keychain provides.
What Should Happen?
Startup should not block ~9-10 s on CA store initialization. The underlying Security framework APIs are not the bottleneck on this machine:
- Direct
SecTrustEvaluateWithErrorvia ctypes: 51 ms total for 159 system certs (~0.3 ms/cert) - Node.js 25's
tls.getCACertificates("system"): 1.49 s, 77 certs (~20 ms/cert) — slower than direct C but usable - Bun CLI (standalone)
tls.getCACertificates("system"): 2 ms, returns 0 certs (doesn't really implement system store) - Claude Code 2.1.119's wrapper: 9.7 s, 18 certs (~530 ms/cert) — orders of magnitude slower than any of the above
The actionable fix: when enumerating the system CA store, pass a policy that does not trigger revocation (e.g. SecPolicyCreateBasicX509), or — since getCACertificates("system") is collecting trust anchors, not validating a server cert chain — skip trust evaluation entirely and just return the certs that SecTrustSettingsCopyCertificates(kSecTrustSettingsDomainAdmin/System) yields. Either change keeps the filter-aware trust behavior out of the hot startup path. See root-cause diagnosis below for evidence.
Error Messages/Logs
No errors — the call succeeds, it's just slow. Debug log (--debug-file) from a fresh env -i invocation:
2026-04-26T20:43:22.806Z [DEBUG] CA certs: Config fallback - globalEnv keys: , settingsEnv keys: none
2026-04-26T20:43:22.807Z [DEBUG] CA certs: stores=bundled,system, extraCertsPath=undefined
2026-04-26T20:43:22.808Z [DEBUG] CA certs: Loaded 145 bundled root certificates
2026-04-26T20:43:31.570Z [DEBUG] CA certs: Loaded 18 system CA certificates ← 8.76s gap
2026-04-26T20:43:31.587Z [DEBUG] CA certs: stores=bundled,system, extraCertsPath=undefined
2026-04-26T20:43:31.588Z [DEBUG] CA certs: Loaded 145 bundled root certificates
2026-04-26T20:43:31.588Z [DEBUG] CA certs: Loaded 18 system CA certificates ← cached hit
Process sample (sample <pid> 3) during the stall — 100% of thread time is in this stack:
SecTrustEvaluateWithError (in Security)
SecTrustEvaluateInternal
SecTrustEvaluateIfNecessary
__SecTrustEvaluateIfNecessary_block_invoke
__SecTrustEvaluateIfNecessary_block_invoke_2
SecOSStatusWith
__SecTrustEvaluateIfNecessary_block_invoke_3
securityd_send_sync_and_do ← 2456 of 2458 samples
security_fw_send_message_with_reply_sync_inner
Claude is not CPU-bound. It's blocked on the synchronous XPC message to securityd.
Steps to Reproduce
On a corporate-managed macOS machine (MDM-installed profiles, corporate CA in keychain). Not all managed Macs reproduce identically — some will return 0 system certs and complete instantly, some will populate and be slow.
1. Baseline (slow) — reproduces in both interactive and print modes
Interactive:
time claude # press Ctrl-C as soon as the prompt appears
The TUI takes ~9-10 s before the first prompt renders. This is what made the bug visible in normal use.
Non-interactive (easier to measure):
echo '{"mcpServers":{}}' > /tmp/empty-mcp.json
mkdir -p /tmp/claude-clean-home
# Use the absolute path to the claude binary since env -i wipes PATH.
# Find yours with `which claude` then resolve the symlink (readlink -f).
CLAUDE_BIN="$(readlink -f "$(which claude)")"
env -i PATH=/usr/bin:/bin HOME=/tmp/claude-clean-home \
"$CLAUDE_BIN" -p hi --debug-file /tmp/cc.log \
--strict-mcp-config --mcp-config /tmp/empty-mcp.json
grep "CA certs" /tmp/cc.log
Observe the gap between the "Loaded 145 bundled root certificates" and "Loaded 18 system CA certificates" log lines. On a clean/personal Mac it's <100 ms. On my corporate-managed Mac it is 8.5-9.8 s. Same delay shows up in interactive mode.
2. Workaround: disable system store
CLAUDE_CODE_CERT_STORE=bundled time claude -p hi
Startup drops by ~9 seconds. Keychain-only trust is no longer honored after this change; in my case that's acceptable because NODE_EXTRA_CA_CERTS contains the break-and-inspect proxy CA I actually need, but it's a workaround, not a fix.
3. Confirm Security framework itself is fast (via Python ctypes)
import ctypes, time
Sec = ctypes.CDLL('/System/Library/Frameworks/Security.framework/Security')
CF = ctypes.CDLL('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
# (full script in "Additional Information" below)
# Result on the same machine that takes 9.7s in Claude:
# admin domain (14 certs): 4.4 ms total
# system domain (159 certs): 46.7 ms total
The same SecTrustEvaluateWithError calls that Claude spends 9.7 s on complete in 51 ms when called directly from C/Python. This rules out the Security framework, trustd, securityd, OCSP, CRL, and MDM profile processing as the cause — those are all fast.
4. Cross-version test (establishes regression in 2.1.119)
Running the same clean-env invocation on three installed versions:
| Version | Default stores | System cert result | Wall time |
|---|---|---|---|
| 2.1.114 | bundled,system | "system store returned empty" | ~0.4 s |
| 2.1.117 | bundled,system | "system store returned empty" | ~0.4 s |
| 2.1.119 | bundled,system | "Loaded 18 system CA certificates" | ~9.5 s |
The binary strings for SecTrustEvaluateWithError, SecTrustSettingsCopyTrustSettings, etc. are identical in all three versions. Something about how the system store is populated changed in 2.1.117 → 2.1.119 (possibly a Bun runtime bump — bun/1.3.13 npm/? node/v24.3.0 darwin arm64 appears in the 2.1.119 binary).
5. Public OCSP responders are reachable from user context
http://ocsp.digicert.com http=200 total=0.848s
http://ocsp.pki.goog http=404 total=0.269s
http://ocsp.sectigo.com http=200 total=0.251s
http://ocsp.rootca1.amazontrust.com http=400 total=0.272s
http://ocsp.apple.com http=403 total=0.487s
Note: the slowness is not from hung OCSP/CRL connections. trustd's fetch attempts get denied instantly by the corporate NetworkExtension filter. The cost comes from the filter's per-flow cryptographic signing, paid ~650 times. See "Root cause diagnosis" below.
Claude Model
Not sure / Multiple models (not relevant — bug fires before any API request)
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.117
(2.1.114 and 2.1.117 both default to bundled,system but the system store fetch was a no-op that returned empty. In 2.1.119 it began populating results and became slow.)
Claude Code Version
2.1.119 (Claude Code)
Also tested 2.1.114 and 2.1.117 on the same machine; both are fast (system-store fetch returns empty). Slowness is unique to 2.1.119.
Platform
Other (AWS Bedrock via LiteLLM-compatible corporate proxy; platform is not relevant — the slowdown is in CA store init, before any API call fires)
Operating System
macOS
Darwin 25.4.0, Apple Silicon (arm64). Corporate-managed (MDM).
Terminal/Shell
Other — WezTerm (bug reproduces in both interactive TUI startup and claude -p; TUI startup delays the first-prompt render by the same ~9 s. Terminal is not implicated — the delay is inside tls.getCACertificates("system") regardless of how Claude is launched.)
Additional Information
Full ctypes reproduction showing Security framework is fast
import ctypes, time
Security = ctypes.CDLL('/System/Library/Frameworks/Security.framework/Security')
CF = ctypes.CDLL('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
CF.CFRelease.argtypes = [ctypes.c_void_p]
CF.CFArrayGetCount.argtypes = [ctypes.c_void_p]
CF.CFArrayGetCount.restype = ctypes.c_long
CF.CFArrayGetValueAtIndex.argtypes = [ctypes.c_void_p, ctypes.c_long]
CF.CFArrayGetValueAtIndex.restype = ctypes.c_void_p
Security.SecTrustSettingsCopyCertificates.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_void_p)]
Security.SecTrustSettingsCopyCertificates.restype = ctypes.c_int32
Security.SecPolicyCreateBasicX509.restype = ctypes.c_void_p
Security.SecTrustCreateWithCertificates.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
Security.SecTrustCreateWithCertificates.restype = ctypes.c_int32
Security.SecTrustEvaluateWithError.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)]
Security.SecTrustEvaluateWithError.restype = ctypes.c_bool
for name, domain_id in [("admin", 1), ("system", 2)]:
out = ctypes.c_void_p()
Security.SecTrustSettingsCopyCertificates(domain_id, ctypes.byref(out))
n = CF.CFArrayGetCount(out.value)
t0 = time.perf_counter()
for i in range(n):
cert = CF.CFArrayGetValueAtIndex(out.value, i)
policy = Security.SecPolicyCreateBasicX509()
trust = ctypes.c_void_p()
Security.SecTrustCreateWithCertificates(cert, policy, ctypes.byref(trust))
err = ctypes.c_void_p()
Security.SecTrustEvaluateWithError(trust.value, ctypes.byref(err))
CF.CFRelease(trust.value); CF.CFRelease(policy)
print(f"{name}: {n} certs, {(time.perf_counter()-t0)*1000:.1f}ms")
CF.CFRelease(out.value)
# Output on the same machine where Claude takes 9.7s:
# admin: 14 certs, 4.4ms
# system: 159 certs, 46.7ms
Root cause diagnosis (log stream on trustd + securityd)
With sudo log stream --predicate 'process == "trustd" OR process == "securityd" OR subsystem == "com.apple.securityd"' --info --debug captured during a slow Claude run and the equivalent ctypes run, sliced to each run's window:
| Subsystem/category | Claude 10s hang | ctypes 2s window |
|---|---|---|
| com.apple.networkextension | 4056 | 0 |
| com.apple.network connection | 1909 | 0 |
| com.apple.network activity | 1238 | 0 |
| com.apple.securityd http | 175 (of 648 total) | 106 |
| com.apple.securityd rvc (revocation) | 809 | 752 |
| com.apple.securityd trust | 728 | 260 |
| com.apple.securityd reject | 1093 | 204 |
| com.apple.securityd dbconn (keychain reader) | 11910 | 4680 |
Revocation-check counts are similar between the two. What differs drastically is network work — Claude's path triggers 4000+ NetworkExtension filter invocations, ctypes' path triggers zero. The NE filter messages show the filter cryptographically signing flow metadata per attempt:
trustd com.apple.networkextension FILTER PROTOCOL: New flow info:
pid: 1008 uuid: FACF7619-... direction: OUT
trustd com.apple.networkextension SIGN NE Filter crypto data:
trustd com.apple.networkextension SIGN flow_id: 'A5 13 55 21 B4 A4 43 D6 …'
trustd com.apple.networkextension SIGN sock_id: '00 00 00 00 …'
trustd com.apple.networkextension SIGN direction: '01 00 00 00'
trustd com.apple.networkextension SIGN remote: '10 02 00 50 D8 3B 38 EE …'
…
Each flow then hits policy and gets denied:
trustd com.apple.securityd http network access disabled by policy (× 648)
The URLs trustd is attempting to fetch are federal PKI cross-cert bundles (keychain has cross-signed federal roots):
http://crl.disa.mil/issuedto/DODROOTCA5_IT.p7c
http://cdn.carillonfedserv.com/CFSRCA1.p7c
http://pki.rtx.com/G3/aia/Root-G3_01.p7c
http://validation.identrust.com/roots/commercialrootca1.p7c
http://validation.identrust.com/roots/igcrootca1.p7c
End-to-end chain:
1. Claude calls tls.getCACertificates("system")
2. Wrapper runs SecTrustEvaluateWithError per cert with a revocation-enabled policy
3. trustd attempts AIA/OCSP/CRL fetches to complete chain validation
4. Each fetch creates an outbound network flow
5. Corporate NetworkExtension filter intercepts
6. NE filter cryptographically signs flow metadata (several ms each)
7. NE filter denies per corporate policy
8. trustd logs "network access disabled by policy", moves on
9. Repeat ~650× per invocation
On a machine without a corporate NE content filter, step 6 is effectively free and the same code path completes quickly. On an NE-filtered machine, step 6 dominates wall time.
The ctypes baseline isolates the policy as the difference: same machine, same trustd, same NE filter, same keychain — the only change is SecPolicyCreateBasicX509 (no revocation) versus Claude's default-policy equivalent. 9.5 s → 51 ms when revocation is skipped.
What I ruled out
trustd/securityddaemon load — same daemons service the fast ctypes calls.- MDM profile processing — would be same cost for both callers.
- Number of certs — Claude evaluates 18, ctypes evaluates 159+; the fast path handles more work.
- Sandboxing — I ran outside any
sandbox-execwrapper (bare CLI invocation). - Bun CLI per se — calling
tls.getCACertificates("system")from the standalonebunCLI returns 0 certs in 2 ms. Only the compiled Claude Code binary exhibits the 9 s hang. - Hung OCSP/CRL network timeouts — trustd's network attempts are denied fast by the NE filter (policy-deny, not timeout). The cost is the filter's per-flow crypto signing, not waiting on the network.
Related context (not duplicates)
- #50115 — confirms Claude Code migrated Node → Bun runtime in v2.1.113; docs not yet updated.
- #46385 — added docs for
CLAUDE_CODE_CERT_STORE. - #51889 — separate
claude installcert-store bug with workaroundNODE_USE_SYSTEM_CA=1. - #50252 — v2.1.113+ Bun runtime regressions behind HTTP CONNECT proxy. Pattern of Bun-migration regressions affecting corporate environments.
Environment summary
- Claude Code 2.1.119 (
/Users/$USER/.local/share/claude/versions/2.1.119, Mach-O arm64) - macOS Darwin 25.4.0, Apple Silicon
- Corporate-managed (MDM), corporate break-and-inspect proxy;
NODE_EXTRA_CA_CERTScontains only the proxy's CA (subset of what's in the keychain) - Keychain contents: 14 admin-domain certs, 159 system-domain certs;
security find-certificate -aruns in 22 ms
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗