Sandboxed Go CLIs fail TLS verification: sandbox denies mach-lookup com.apple.trustd.agent
Sandboxed Go CLIs fail TLS verification: deny mach-lookup com.apple.trustd.agent
Summary
On macOS, any Go-based CLI running inside the Bash sandbox fails TLS certificate
verification, because the sandbox profile denies mach-lookup com.apple.trustd.agent.
Go's crypto/x509 delegates chain verification to macOS trust services, so the
connection itself succeeds but verification fails.
The symptom is a misleading error that looks like a certificate or network problem:
Get "https://api.github.com/rate_limit": tls: failed to verify certificate: x509: OSStatus -26276
curl and security verify-cert are unaffected in the same sandbox at the same moment.
Environment
- Claude Code 2.1.222 (desktop app)
- macOS 26.5.2 (25F84), Apple silicon
gh2.94.0 (Homebrew), a Go binary- Sandbox enabled via
sandbox.enabled: true api.github.comreachable (routed through the sandbox's local proxy)
Evidence
Kernel sandbox log during a failing run:
sandboxd [com.apple.sandbox.reporting:violation] Sandbox: gh(64174) deny(1) mach-lookup com.apple.trustd.agent
Path: /opt/homebrew/Cellar/gh/2.94.0/bin/gh
Responsible: .../claude.app/Contents/MacOS/claude
Denial counts over a 3 minute window, same host, same sandbox:
| process | mach-lookup com.apple.trustd.agent denials |
|---|---|
| gh (Go) | 47 |
| security verify-cert | 0 |
| curl | 0 |
curl returns HTTP 200 and security verify-cert reports No error at the same
moments gh fails, so the certificate chain and the trust daemon are both fine.
The chain served inside the sandbox is identical to the one served outside
(3 certificates, genuine Sectigo issuer) - the proxy is not substituting anything.
What we ruled out
Each of these was tested and refuted, in case they come up:
- Domain restrictions.
github.com,*.github.com,*.githubusercontent.comwere
already in allowedDomains, and curl to the same host succeeds.
- A CA difference between tools.
curlis built against SecureTransport, so it uses
the same macOS trust services.
- TLS interception by the sandbox proxy. The presented chain is the genuine one.
- A missing intermediate forcing an AIA fetch. The chain is complete inside the sandbox.
trustdbeing unavailable.security verify-certsucceeds during the failures.- Go-side workarounds.
SSL_CERT_FILE,GODEBUG=x509usefallbackroots=1and
GODEBUG=netdns=go all have no effect, because Go on macOS uses the platform
verifier regardless.
Why this was hard to diagnose
excludedCommands contained gh *, so most invocations ran outside the sandbox and
worked. Only invocations where gh was nested hit the sandbox:
| invocation | excluded? |
|---|---|
| gh api ... | yes |
| echo x; gh api ... | yes |
| VAR=1 gh api ... | yes |
| for i in 1 2; do gh api ...; done | no |
| x=$(gh api ...) | no |
Because the exclusion appears to be matched against top level command segments only,
the same command succeeded or failed depending on how it was written. That made the
failure look intermittent and time dependent, and cost several hours to localise.
This also means a script that shells out to gh (for example a Python audit script
invoked as python3 audit.py) can never match gh *, and always fails.
Suggested fixes
- Allow
mach-lookup com.apple.trustd.agentin the sandbox profile. It is required
for TLS verification by any Go binary, and denying it does not add isolation once
network egress is already gated by the proxy.
- If it must stay denied, surface the denial rather than letting it surface as an
opaque x509: OSStatus -26276.
- Consider whether
excludedCommandsshould match nested invocations, or document
that it does not. The current behaviour makes an entry look applied when it is not.
Workarounds in use
- Keep Go CLIs at the top level of the command, never inside loops or
$(...). - For repeated calls, use
curlwith the token instead ofgh. - For anything that shells out to
ghfrom a script, run it in CI instead of locally.
3 Comments
Correction: the TLS failure itself is already documented
I filed this without having found the existing documentation. The sandboxing page already covers it under Troubleshooting:
So the symptom and the workaround are both known, and my framing of it as a new finding was wrong. Sorry for the noise.
What I think is still worth reporting is narrower, so I'll restate it.
The part that is not covered by that note
The workaround was already applied.
excludedCommandscontainedgh *, and most invocations did run outside the sandbox. What is not obvious is that the exclusion is matched against top level command segments only, so the same command succeeds or fails depending on how it is written:| invocation | excluded? |
|---|---|
|
gh api ...| yes ||
echo x; gh api ...| yes ||
VAR=1 gh api ...| yes ||
for i in 1 2; do gh api ...; done| no ||
x=$(gh api ...)| no |Two consequences:
x509: OSStatus -26276rather than anything that points at the exclusion. Because my own commands alternated between these forms, the failure looked intermittent and time dependent, which is what sent me down the wrong path for several hours.python3 audit.pydoes not matchgh *, so any tool invoked from inside a script is always sandboxed, and for Go CLIs that means it always fails TLS verification. This is not something the user can fix by editingexcludedCommands.Case 2 is the one that seems worth a doc note or a behaviour change: the documented workaround does not work for the common case of a wrapper script.
Suggested change, revised
Rather than the three items in the original report, I would narrow it to:
excludedCommandsmatches top level segments only, and therefore does not cover commands nested in loops, command substitution, or scripts.excludedCommandsworkaround does not extend to tools invoked from scripts.The kernel side detail is still in the original report if it is useful: the specific denial is
deny(1) mach-lookup com.apple.trustd.agent, andcurlandsecurity verify-certare not denied it in the same sandbox at the same moment.Regarding the
mach-lookup com.apple.trustd.agentdenial on macOS and Go CLI TLS failures:The failure is caused by an incomplete Seatbelt SBPL rule generator in the in-tree sandbox harness. When Go binaries invoke
crypto/x509platform verification, macOS routes verification IPC totrustd. If the SBPL profile omits(allow mach-lookup (global-name "com.apple.trustd.agent")), Go surfacesx509: OSStatus -26276regardless of environment variables or proxy certificates.Furthermore, string-based
excludedCommandsmatching at the command line string level inherently collapses on subshells (x=$(gh api ...)), pipes, or script-invoked child tools because the AST parser only evaluates the outer binary name.If you need full Go/CLI toolchain compatibility on macOS and Linux without TLS certificate breakages, Vetto wraps agent processes with a complete OS perimeter:
How Vetto handles this at the OS layer:
com.apple.trustd.agent,com.apple.system.opendirectoryd.libinfo) for certificate verification while strictly enforcing VFS boundaries and/dev/nullmasking for~/.ssh,~/.aws, and.env./etc/ssl/certs/ca-certificates.crt) remains transparent with zero MITM proxy certificate injection.$(...)or nested scripts.Still reproduces on 2.1.286, macOS 27.0.1. In a sandboxed Bash:
$ DIGITALOCEAN_ACCESS_TOKEN=bogus doctl account get
Error: Get "https://api.digitalocean.com/v2/account": tls: failed to verify certificate: x509: OSStatus -26276
$ kubectl --server=https://api.digitalocean.com --token=bogus get ns
Unable to connect to the server: tls: failed to verify certificate: x509: OSStatus -26276
$ curl -s -o /dev/null -w '%{http_code}' https://api.digitalocean.com/v2/account
401
curl (which uses a different TLS stack) reaches the API through the same proxy, so the proxy's certificate is fine; the Go binaries fail because they can't reach trustd. The only setting that fixes it that we found is
enableWeakerNetworkIsolation, which is much broader than this needs. A narrowallowMachLookup: ["com.apple.trustd.agent"]that actually passes through (#82793) would solve it.