gh-api.mjs: a GitHub client for shells where gh cannot do TLS

Measured 2026-08-20/21 across a full parent/subagent × sandboxed/flag-off matrix. All four cells fail identically for gh; node succeeds in all of them.

The mechanism, corrected

Claude Code's srt egress proxy is a plain CONNECT tunnel. There is no interception of the TLS stream, the upstream certificates are genuine, and there is nothing to install.

gh fails because OSStatus -26276 is a Security-framework internal error: the certificate verifier cannot consult the trust store at all, because the sandbox denies Mach IPC to securityd, and Go on darwin has no other certificate-verification path.

node works because it ships its own CA bundle and never needs to talk to securityd.

gh (Go) Security.framework Mach IPC to securityd: DENIED OSStatus -26276
node fetch bundled CA store CONNECT via srt tunnel TLS to api.github.com: 200

The launch-time proxy rule, proven empirically

Node only honors HTTPS_PROXY when NODE_USE_ENV_PROXY=1 is present at launch: undici wires its global dispatcher during bootstrap.

VariantCommand shapeResult
In-script assignmentprocess.env.NODE_USE_ENV_PROXY='1' then fetch()getaddrinfo ENOTFOUND (went direct, bypassed the tunnel)
No variable at allnode probe.mjsgetaddrinfo ENOTFOUND
Launch-time variableNODE_USE_ENV_PROXY=1 node probe.mjsHTTP 200

The helper therefore re-execs itself once with NODE_USE_ENV_PROXY=1 in the child environment when the variable is absent. That is the proven launch-time variant, packaged so callers never have to remember it.

Usage

$ node scripts/gh-api.mjs GET /rate_limit
{"resources":{"core":{"limit":5000,"used":91,"remaining":4909,...

$ node scripts/gh-api.mjs GET /repos/yonatangross/orchestkit
{"id":...,"name":"orchestkit","full_name":"yonatangross/orchestkit",...
$ node scripts/gh-api.mjs POST /repos/OWNER/REPO/pulls body.json
# body.json is sent verbatim as the JSON request body
$ echo 'query { viewer { login } }' | node scripts/gh-api.mjs graphql
{"data":{"viewer":{"login":"yonatangross"}}}
$ node scripts/gh-api.mjs GET /repos/yonatangross/no-such-repo-xyz
GhApiError: HTTP 404 for GET https://api.github.com/repos/yonatangross/no-such-repo-xyz: {"message":"Not Found",...}
$ echo $?
1

Named error, status code, first 200 characters of the body. No stack trace.

What the sandbox allowlist entry does, and does not do

Does: adding api.github.com to sandbox.network.allowedDomains in the project's .claude/settings.json lets sandboxed curl and node reach the GitHub API through the tunnel without a per-command override.

Does not: it does not fix gh itself. The securityd Mach IPC denial is upstream of any hostname allowlist; gh fails the TLS handshake before the destination matters.

OrchestKit playground · branch feat/gh-api-helper · 2026-08-21. Token handling: the helper reads gh auth token via execFileSync and never prints it.