Skip to content

macOS: explicit tls.getCACertificates("system") call adds 5+ seconds to every CLI invocation #3330

@Joi

Description

@Joi

Describe the bug

GitHub Copilot CLI's CA loader calls tls.getCACertificates("system") on every invocation in addition to "bundled" and "extra". On macOS, that call iterates every keychain cert and invokes SecTrustEvaluateWithError synchronously via XPC to trustd — which (because of a Node bug, see References) uses a revocation-enabled trust policy and triggers OCSP/CRL fetches per cert. On machines with any network-flow filter (corporate NetworkExtension, ZeroTier, etc.), each fetch pays a per-flow cost and totals 5-10 seconds of latency on every CLI invocation — even sub-commands like --version that don't make HTTPS requests inherit the cost because the call happens during SEA startup.

The "system" arg is also functionally redundant with "bundled" for *.github.com / *.githubcopilot.com endpoints. See related #869 (which reports a separate failure caused by the duplicate-certs concatenation from this same code).

Affected version

GitHub Copilot CLI 1.0.48 (also reproduced on 1.0.39). macOS 26.4.1, Apple Silicon. Node 24.15.0 inside the SEA.

Steps to reproduce the behavior

$ time copilot --allow-all-tools -p "reply with exactly: ack"
ack

real    0m12.43s
user    0m1.98s
sys     0m0.17s

Wall-clock vs user CPU mismatch confirms it's I/O wait, not CPU work. sample(1) on the grandchild SEA process during the wait:

node::crypto::GetSystemCACertificates
  node::crypto::ReadMacOSKeychainCertificates
    node::crypto::IsCertificateTrustedForPolicy
      node::crypto::IsCertificateTrustValid
        SecTrustEvaluateWithError
          SecTrustEvaluateIfNecessary
            securityd_send_sync_and_do          ← synchronous XPC to trustd
              xpc_connection_send_message_with_reply_sync
                mach_msg2_trap

Isolated reproduction with standalone Node 24 on the same machine:

$ node -e 'const t0=process.hrtime.bigint();
           require("tls").getCACertificates("system");
           console.log(`${(Number(process.hrtime.bigint()-t0)/1e6).toFixed(0)}ms`)'
5287ms
$ node -e 'const t0=process.hrtime.bigint();
           require("tls").getCACertificates("bundled");
           console.log(`${(Number(process.hrtime.bigint()-t0)/1e6).toFixed(0)}ms`)'
0.1ms

HTTPS request to api.github.com:

  • default (bundled): 53ms
  • node --use-bundled-ca …: 103ms
  • node --use-system-ca …: 5093ms — 96× slower

Expected behavior

CLI startup should not pay multi-second latency for a CA loader that produces certs equivalent to the bundled Mozilla store.

Root cause in copilot-cli source

Decoded from app.js (npm-loader fallback; the SEA's bundled JS contains the same logic):

function buildCAList() {
  const envCerts = ["NODE_EXTRA_CA_CERTS","SSL_CERT_FILE","CURL_CA_BUNDLE"].flatMap(readEnvCAFile);
  return typeof tls.getCACertificates === "function"
    ? [...envCerts,
       ...tls.getCACertificates(),           // default (bundled in Node 24+)
       ...tls.getCACertificates("system"),   // ← THIS LINE
       ...tls.getCACertificates("bundled"),  // bundled (already implied above)
       ...tls.getCACertificates("extra")]
    : [...envCerts, ...tls.rootCertificates];
}

Proposed fix

Remove the tls.getCACertificates("system") line. One-line patch. If retaining the option for users with private CAs is desired, gate it behind a COPILOT_USE_SYSTEM_CA=1 env var defaulting OFF. The current always-on cost is borne by every macOS user even when they don't need it. This also resolves the duplicate-certs issue tracked at #869.

Why this user sees the upper end

My login keychain had 1589 certs (mostly auto-imported S/MIME contact certs from Mail.app over 20 years). After pruning 1478 long-expired certs, the call dropped from ~10s to ~5s — the floor on this machine, set by ~270 still-valid certs each costing ~20 ms in SecTrustEvaluateWithError. ZeroTier's feth1960 virtual interface and zerotier-one daemon further amplify per-flow cost for the trustd OCSP traffic.

Users without a flow filter and a smaller keychain see a smaller version of this same tax (1-2s baseline, per anthropics/claude-code#53660's measurements).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions