Sandboxed Go CLIs fail TLS verification: sandbox denies mach-lookup com.apple.trustd.agent

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 3 comments · opened Aug 11, 2026

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
  • gh 2.94.0 (Homebrew), a Go binary
  • Sandbox enabled via sandbox.enabled: true
  • api.github.com reachable (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.com were

already in allowedDomains, and curl to the same host succeeds.

  • A CA difference between tools. curl is 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.
  • trustd being unavailable. security verify-cert succeeds during the failures.
  • Go-side workarounds. SSL_CERT_FILE, GODEBUG=x509usefallbackroots=1 and

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

  1. Allow mach-lookup com.apple.trustd.agent in 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.

  1. If it must stay denied, surface the denial rather than letting it surface as an

opaque x509: OSStatus -26276.

  1. Consider whether excludedCommands should 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 curl with the token instead of gh.
  • For anything that shells out to gh from a script, run it in CI instead of locally.

View original on GitHub ↗

3 Comments

q1353 · 2 months ago

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:

Go-based CLIs fail TLS verification on macOS: tools such as gh, gcloud, and terraform may fail TLS verification under Seatbelt. List these tools in excludedCommands to run them outside the sandbox.

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. excludedCommands contained gh *, 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:

  1. An entry can look applied while silently not applying, and the failure surfaces as x509: OSStatus -26276 rather 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.
  2. A script that shells out to an excluded tool can never match. python3 audit.py does not match gh *, 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 editing excludedCommands.

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:

  • Document that excludedCommands matches top level segments only, and therefore does not cover commands nested in loops, command substitution, or scripts.
  • Consider whether the existing troubleshooting note should say that the excludedCommands workaround 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, and curl and security verify-cert are not denied it in the same sandbox at the same moment.

shleder · 1 month ago

Regarding the mach-lookup com.apple.trustd.agent denial 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/x509 platform verification, macOS routes verification IPC to trustd. If the SBPL profile omits (allow mach-lookup (global-name "com.apple.trustd.agent")), Go surfaces x509: OSStatus -26276 regardless of environment variables or proxy certificates.

Furthermore, string-based excludedCommands matching 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:

# Wrap Claude Code with full Go TLS compatibility + secret masking:
vetto --agent claude --profile standard -- claude

How Vetto handles this at the OS layer:

  • macOS Seatbelt: Allows requisite Mach lookups (com.apple.trustd.agent, com.apple.system.opendirectoryd.libinfo) for certificate verification while strictly enforcing VFS boundaries and /dev/null masking for ~/.ssh, ~/.aws, and .env.
  • Linux Landlock: Operates purely at the VFS syscall layer (ABI v1–v5), meaning Go read access to system trust stores (/etc/ssl/certs/ca-certificates.crt) remains transparent with zero MITM proxy certificate injection.
  • Subshell Invariance: Security boundaries apply to the process hierarchy, completely avoiding string-matching AST parser bypasses on $(...) or nested scripts.
s27183 · 14 days ago

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 narrow allowMachLookup: ["com.apple.trustd.agent"] that actually passes through (#82793) would solve it.