unable to get local issuer certificate — the error that fails on the first try and never retries
- Instant, every-time failure means the fault is local. Almost everything else is retried with backoff; certificate validation is one of the few that gets none.
- Export your network's CA certificate, point
NODE_EXTRA_CA_CERTSat it, then restart the client. - Do not set
NODE_TLS_REJECT_UNAUTHORIZED=0— it disables TLS verification process-wide, trading a config problem for a security one.
Almost everything that can go wrong between your client and an endpoint is retried for you — server errors, overload, request timeouts, dropped connections and temporary throttles all get up to 10 attempts with exponential backoff before anything reaches your screen. TLS certificate validation failure is one of the few that gets none. It is reported on the first attempt. That inverts how you should read it: an error that appears instantly and every time is not evidence the service is unstable — it is evidence the failure is local, deterministic, and yours to fix.
What you're seeing
Error: unable to get local issuer certificate
Error: self-signed certificate in certificate chain
{ code: 'SELF_SIGNED_CERT_IN_CHAIN' }
{ code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' }
SSL certificate verification failed
SSL certificate error (...)
These are the same event described at four different heights. Your runtime took the certificate the server presented, tried to build a chain up to a root it already trusts, and couldn't finish. The message names where the walk stopped.
| Code | Message you'll see | Where the chain broke |
|---|---|---|
UNABLE_TO_GET_ISSUER_CERT_LOCALLY | unable to get local issuer certificate | The issuer of the presented certificate is not in your trust store |
SELF_SIGNED_CERT_IN_CHAIN | self-signed certificate in certificate chain | Something in the chain signed itself — typically a proxy's own root |
UNABLE_TO_VERIFY_LEAF_SIGNATURE | unable to verify the first certificate | The server sent the leaf but not the intermediates |
CERT_HAS_EXPIRED | certificate has expired | Dates, not trust |
self signed certificate in certificate chain — no hyphen. Same failure. Search for both spellings.
None of these means the server is down. A server that is down does not get far enough to present a certificate.
The asymmetry that makes this error look worse than it is
| Failure | Retry budget | What you actually experience |
|---|---|---|
| Server errors, overload, request timeout before any response has streamed | up to 10 attempts, exponential backoff | a long pause, then usually success — you often never learn it happened |
| Dropped connection, or a connection your machine broke by going to sleep | same budget | same |
Temporary 429 throttle | same budget | same |
| Stalled response stream, nothing arrived yet | one extra re-issue, outside the budget | same |
| TLS certificate validation failure | none — reported on the first attempt | instant, every time, no pause |
| TLS handshake timeout | still retried — it's transient, not a validation failure | a pause, then usually success |
Read the table as a filter on what reaches your eyes. Anything with a retry budget has to fail ten times in a row, with growing gaps between attempts, before it surfaces. Anything with no budget surfaces on attempt one. So the errors that arrive fastest are systematically the ones nobody retries — which means a large share of failures people describe as "this service is flaky" are the opposite: a stable, local, reproducible misconfiguration that simply had nothing standing between it and the terminal.
The practical version: an error that takes 30 seconds to appear and an error that appears instantly are not the same class of problem, even when the text looks similar. Time-to-error is diagnostic information. Use it before you change anything.
Two honest caveats. Certificate failures were not always exempt from retries — older client versions ran them through the full budget first, so on an older build a certificate problem does look slow and flaky. And "TLS error" is not the same as "certificate error": a handshake that times out is transient and still retried. Confirm the behaviour on the client version you're actually running before relying on timing as a signal.
Which layer is actually broken
| Layer | What's happening | How to recognise it | Who fixes it |
|---|---|---|---|
| Your runtime's trust store | The CA is installed in the OS, but the runtime can't read the OS store | An npm install on an older Node; a native install of the same client works | You |
| A TLS-inspecting proxy | Something terminates TLS and re-signs traffic with a private root | The presented issuer is not a public CA | You (trust the CA) or your IT team |
| The CA bundle never reached the process | NODE_EXTRA_CA_CERTS is set but the file wasn't loaded | Config screens show the path; the debug log has no "appended" line | You |
| Missing intermediates | The server sends the leaf only | UNABLE_TO_VERIFY_LEAF_SIGNATURE, and other clients on other machines fail identically | The endpoint operator |
| The endpoint's certificate expired | Dates, not trust | Issuer is a public CA, and notAfter is in the past | The endpoint operator |
Most write-ups on this error jump straight to "add the certificate." That works for rows two and three and does nothing at all for rows one, four, and five. The next section is ordered so you find out which row you're in before you edit anything.
Check in this order
1 · Look at the certificate that is actually being presented#
This is the single most informative command on the page. Run it before forming a theory.
HOST=api.9coding.com
openssl s_client -connect "$HOST:443" -showcerts </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
Read the issuer line:
- A public CA you recognise → nothing is intercepting you. Check the dates; if
notAfteris in the past, this is the operator's problem, not yours. - Your company's name, your security vendor's name, or a hostname on your own network → your traffic is being terminated and re-signed in transit. That is the whole cause. Go to step 3.
- The command hangs or returns nothing → you have a connectivity problem, not a certificate problem. See connection errors.
2 · Confirm your runtime can read the OS trust store#
Clients built on Node trust their own bundled CA set and the operating system's store — but reading the OS store requires a runtime new enough to support it. When it isn't, a certificate your IT team correctly installed system-wide is invisible to the client, and everything else on the machine keeps working. Browsers work. curl works. Only the client fails.
node -p "process.version"
node -p "typeof require('tls').getCACertificates" # "function" = can read the OS store
If that prints undefined, the OS store is not in play and only the bundled set plus NODE_EXTRA_CA_CERTS apply. Either upgrade the runtime or continue to step 3 — step 3 works in both cases.
3 · Export your organisation's CA and point the client at it#
security find-certificate -a -p /Library/Keychains/System.keychain > ~/corp-ca.pem
security find-certificate -a -p /System/Library/Keychains/SystemRootCertificates.keychain >> ~/corp-ca.pem
Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\CA |
ForEach-Object {
"-----BEGIN CERTIFICATE-----"
[Convert]::ToBase64String($_.RawData, 'InsertLineBreaks')
"-----END CERTIFICATE-----"
} | Set-Content -Encoding ascii $HOME\corp-ca.pem
Linux — install the root properly, then use the system bundle:
sudo cp corp-root.crt /usr/local/share/ca-certificates/corp-root.crt # Debian/Ubuntu
sudo update-ca-certificates
# RHEL/Fedora: /etc/pki/ca-trust/source/anchors/ then `sudo update-ca-trust`
Then point the variable at the bundle and restart the client — these variables are read once at startup, and a running process keeps the environment it launched with:
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu
# macOS/Windows: the file you just wrote
Verify the bundle actually satisfies the chain before you blame anything else:
openssl s_client -connect "$HOST:443" -CAfile "$NODE_EXTRA_CA_CERTS" </dev/null 2>&1 \
| grep -i 'verify return code'
# want: Verify return code: 0 (ok)
4 · Confirm the bundle was loaded, not just configured#
These are different facts, and this is where most "I already set that" conversations end. A status screen that shows the path is telling you the variable is set. It is not telling you the file was read — an unreadable path, a wrong permission, or a PEM with the wrong line endings all produce a configured-but-not-loaded state that looks identical from the outside.
Start the client with debug logging and look for the line that names the file:
claude --debug
# then read the newest file under ~/.claude/debug/
A line confirming extra certificates were appended from NODE_EXTRA_CA_CERTS, naming your path, means it loaded. A "failed to read" or "failed to load" line gives you the reason. No line at all is a failure too — it means the variable never reached the process, which happens routinely when it was exported in one shell and the client was started from another, or when the client runs under a supervisor or background agent that never saw your shell. Put it in the client's own settings file rather than a shell profile if that's your situation.
5 · Take the client out of the picture — carefully#
# <model-id>: copy an id from https://api.9coding.com/v1/models
curl -sS -w '\n%{http_code}\n' https://api.9coding.com/v1/messages \
-H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"<model-id>","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
node -e 'require("https").get("https://api.9coding.com/", r => console.log(r.statusCode))
.on("error", e => console.error(e.code, "-", e.message))'
Run both, because they do not read the same trust store, and the difference between them is the answer:
curlsucceeds, the Node one-liner fails → the certificate is fine system-wide and the problem is the runtime's trust store. Back to steps 2 and 3. This is the most common outcome, and it is exactly why "but my browser works" is not evidence of anything.- Both fail with a certificate error → nothing on this machine trusts that chain. Step 1 told you why.
- Both succeed, the client still fails → the client isn't reading the environment you edited. Step 4, last paragraph.
The variable you'll be tempted to set, and shouldn't
Every thread about this error eventually surfaces NODE_TLS_REJECT_UNAUTHORIZED=0. It will make the error go away. Here is what it actually does.
It disables TLS certificate verification for the entire process — not for one host, not for one request. Every outbound HTTPS connection that process makes, including ones you didn't think about, will now accept any certificate from anyone able to sit between you and the destination. On a machine where traffic is already being intercepted, that is precisely the property you were relying on to notice.
You had a configuration problem, solvable in step 3 in about two minutes. This trades it for a security problem that is silent, permanent until someone notices, and travels with any script or image you copy the setting into. If you set it once during a five-minute experiment, unset it in the same session.
There is no version of this that is fine "just for testing on an internal endpoint." Trusting a specific CA is the same amount of work and leaves verification switched on for everything else.
When it isn't your problem
You are looking at the operator's certificate, not your setup, when:
- step 1 shows an issuer that is a public CA, and
notAfterhas passed, - or step 1 shows a leaf with no intermediates and
curlfails the same way, - and the same failure reproduces from a different machine on a different network — a phone hotspot is enough,
- and it started without any change on your side.
Certificate expiry is a scheduled event that someone missed, so it usually resolves in hours rather than minutes. Check the operator's status page before you spend the afternoon regenerating trust stores — ours is at status.
When reporting it, include the request id — 9Coding error responses carry one in the form (request id: 2026...). That id plus the timestamp, the model name and the full error text lets the exact call be traced. A report that only says it doesn't work cannot be investigated.
Certificate errors and connection errors are not the same problem
A certificate error means the connection succeeded and then verification failed — you got far enough to be handed a certificate. ECONNREFUSED, ETIMEDOUT, and fetch failed mean you never got there, and no amount of CA configuration will change them. They also retry, which is why they feel slow and this one feels instant. See connection errors.
Related
- Connection errors — separating network, proxy, and endpoint
- 401 Unauthorized — two layers of authentication, one error message
- 429 Too Many Requests — why the retries are already gone by the time you see it
- All Claude Code errors — the quick reference table