Every devkit HTTP call fails in an environment that terminates TLS at a proxy. Observed in a Claude cloud session on 2026-09-15:
$ issue setup 75 --dry-run --no-summary
Error: fetching the title for 75
Caused by:
0: https://api.github.com/graphql: Connection Failed: tls connection init failed: invalid peer certificate: UnknownIssuer
1: invalid peer certificate: UnknownIssuer
The handshake completes and the proxy's certificate arrives; devkit rejects it. ureq's tls feature pins webpki-roots, a copy of Mozilla's root list compiled into the binary, so devkit never consults the machine's own trust store. The environment had already installed its CA there and set SSL_CERT_FILE. Neither reaches devkit.
This is not cloud-specific. A corporate laptop running Zscaler, Netskope, or any MITM inspection proxy fails the same way, and a self-hosted GitHub Enterprise behind an internal CA fails the same way.
Blast radius
Every network path, because none of them share a client:
| Call site |
Client |
devkit-common/src/github.rs |
pooled ureq::Agent via AgentBuilder |
devkit-common/src/tracker/linear.rs |
bare ureq::post, fresh default agent per call |
devkit-common/src/slack.rs |
bare ureq::post, fresh default agent per call |
devkit-docs/src/lookup.rs |
bare ureq::get against crates.io, npm, PyPI |
Only github.rs builds an agent it could configure. The other three construct a default agent per request, so there is no single place to set trust roots today.
The gh fallback does not save it. fetch_graphql in devkit-issue/src/prs.rs falls back to gh api graphql when the HTTP path fails, but the tracker's title fetch has no such fallback, and the cloud image has no gh on PATH at all. devkit auth github reports token from GH_TOKEN but could not resolve the identity for the same reason, which reads like a bad token rather than a TLS failure.
Proposed fix
Enable ureq's native-certs feature and route every call site through one shared agent builder in devkit-common.
native-certs = [dep:rustls-native-certs] is available in the locked ureq 2.12.1. It reads the platform trust store, which is where a proxy CA, a corporate CA, and an internal CA all already live. Also honor SSL_CERT_FILE and SSL_CERT_DIR explicitly, since those are the conventional names a sandbox tells its tools to use and the cloud environment sets them.
Centralizing the agent is a prerequisite, not a separate cleanup: three of the four call sites cannot be configured without it. It also gets connection pooling on the Linear, Slack, and docs paths, which currently redial per request.
On gating this behind a cloud environment variable
I would not. Two reasons.
The condition that matters is "something is intercepting my TLS", not "I am in a cloud VM". A CLOUD/IS_CLOUD check fixes the hosted session and leaves every corporate laptop broken, which is the larger population.
And the behavior being gated is "also trust the certificates this machine trusts". That is the correct default everywhere, not a concession to make in one environment. Making it conditional adds a knob whose only other setting is wrong, and invites someone to reach for a verification bypass when the real answer is a trust store read.
If devkit does want a cloud signal for other behavior, that is worth its own discussion, but this bug should not be the thing that introduces it.
Acceptance criteria
Notes
Slug derivation is the visible symptom but the smallest one. With this broken, a cloud agent also loses issue state, PR triage, issue status, issue prs, and the docs lookups, and must pass --slug by hand to issue setup.
Secondary, not required here: ureq's proxy-from-env feature is off, so devkit ignores HTTPS_PROXY. It did not matter in the observed environment because interception was transparent, but an environment requiring an explicit CONNECT would fail differently.
References
Every devkit HTTP call fails in an environment that terminates TLS at a proxy. Observed in a Claude cloud session on 2026-09-15:
The handshake completes and the proxy's certificate arrives; devkit rejects it.
ureq'stlsfeature pinswebpki-roots, a copy of Mozilla's root list compiled into the binary, so devkit never consults the machine's own trust store. The environment had already installed its CA there and setSSL_CERT_FILE. Neither reaches devkit.This is not cloud-specific. A corporate laptop running Zscaler, Netskope, or any MITM inspection proxy fails the same way, and a self-hosted GitHub Enterprise behind an internal CA fails the same way.
Blast radius
Every network path, because none of them share a client:
devkit-common/src/github.rsureq::AgentviaAgentBuilderdevkit-common/src/tracker/linear.rsureq::post, fresh default agent per calldevkit-common/src/slack.rsureq::post, fresh default agent per calldevkit-docs/src/lookup.rsureq::getagainst crates.io, npm, PyPIOnly
github.rsbuilds an agent it could configure. The other three construct a default agent per request, so there is no single place to set trust roots today.The
ghfallback does not save it.fetch_graphqlindevkit-issue/src/prs.rsfalls back togh api graphqlwhen the HTTP path fails, but the tracker's title fetch has no such fallback, and the cloud image has noghonPATHat all.devkit auth githubreportstoken from GH_TOKEN but could not resolve the identityfor the same reason, which reads like a bad token rather than a TLS failure.Proposed fix
Enable ureq's
native-certsfeature and route every call site through one shared agent builder indevkit-common.native-certs = [dep:rustls-native-certs]is available in the locked ureq 2.12.1. It reads the platform trust store, which is where a proxy CA, a corporate CA, and an internal CA all already live. Also honorSSL_CERT_FILEandSSL_CERT_DIRexplicitly, since those are the conventional names a sandbox tells its tools to use and the cloud environment sets them.Centralizing the agent is a prerequisite, not a separate cleanup: three of the four call sites cannot be configured without it. It also gets connection pooling on the Linear, Slack, and docs paths, which currently redial per request.
On gating this behind a cloud environment variable
I would not. Two reasons.
The condition that matters is "something is intercepting my TLS", not "I am in a cloud VM". A
CLOUD/IS_CLOUDcheck fixes the hosted session and leaves every corporate laptop broken, which is the larger population.And the behavior being gated is "also trust the certificates this machine trusts". That is the correct default everywhere, not a concession to make in one environment. Making it conditional adds a knob whose only other setting is wrong, and invites someone to reach for a verification bypass when the real answer is a trust store read.
If devkit does want a cloud signal for other behavior, that is worth its own discussion, but this bug should not be the thing that introduces it.
Acceptance criteria
devkit-commonserves GitHub, Linear, Slack, and docs lookups.SSL_CERT_FILEandSSL_CERT_DIR, when set, are honored.could not resolve the identity.issue setup <n> --dry-runderives a slug from the issue title in a cloud session.Notes
Slug derivation is the visible symptom but the smallest one. With this broken, a cloud agent also loses issue state, PR triage,
issue status,issue prs, and the docs lookups, and must pass--slugby hand toissue setup.Secondary, not required here: ureq's
proxy-from-envfeature is off, so devkit ignoresHTTPS_PROXY. It did not matter in the observed environment because interception was transparent, but an environment requiring an explicitCONNECTwould fail differently.References