Let the connector run on macOS
basecamp connect refused to run on a Mac: "The connector runs on Linux only, not darwin: … Linux is the only platform that seals the descriptors a process inherits". Most of us are on Macs, so most people couldn't connect an agent at all.
That reason turned out not to be true. When the connector hands a worker its one-time token, it passes it on an inherited file descriptor. The CLI's first job at startup is to mark every inherited descriptor close-on-exec, so nothing it starts can pick the token up. It only did that on Linux. macOS can do the same: list /dev/fd and mark each one, exactly as the Linux fallback for older kernels already does with /proc/self/fd. I checked this on a Mac first. Without the marking, a child process could see the descriptor; with it, it couldn't.
What changes
- At startup on macOS, the CLI marks inherited descriptors close-on-exec by listing
/dev/fd. If that can't be read, it refuses to start rather than guess which descriptors are open. - The token reader is built for macOS as well as Linux. The existing test that keeps sealing and token-reading paired on every platform we release for still holds them together.
basecamp connectruns on macOS. On other platforms it now says "Linux and macOS only".- The background service (hidden for now) stays Linux-only, since it writes a systemd unit. On a Mac it now says so in plain words.
Found by running the connector's tests on a real Mac
CI only runs the Go tests on Linux, so this was the first time they'd run on macOS. Three things came up, and the first two are real behaviour differences:
- macOS won't signal a process group whose only members are zombies. That's a group of processes that have exited but haven't been cleaned up yet. Linux delivers the signal; macOS refuses with "operation not permitted". The connector took that refusal to mean the group might still be running, so it couldn't confirm a finished worker had gone. Now, when the system's own process list shows nothing in the group is running, it's treated as gone, the same as on Linux. A group that's still running but can't be signalled (another user's) still lists its members, and stays refused.
- macOS only names a socket's peer process while it's connected. If a worker's MCP server closed before it read its token, the connector recorded an untrusted connection and gave up on that socket. Now it's "undelivered", as on Linux, and the server's next start gets the token. A different user is still refused.
- A test's fake worker crashed on macOS before the test could run, because it blocked with an empty
select. It sleeps now.
Tried on a Mac
- The connector's test suites, built for macOS and run on an Apple Silicon Mac: every connector package passes. One timing-sensitive token test failed once in its first ten runs, then passed in seventy runs after; I didn't catch why that first one failed.
- Five general CLI tests (terminal detection, delete confirmations, bare
basecamp, the setup wizard) fail on macOS onmaintoo. They're unrelated to the connector and not touched here. - End to end, against a local Basecamp through an SSH tunnel:
auth agent connect,connect setupandconnect doctorall passed. The connector picked up a mention, boosted 👀, replied and completed the task. The worker was the e2e stand-in, because over SSH macOS keeps the Keychain locked and Claude Code reports itself logged out.
Summary by cubic
Lets basecamp connect run on macOS. Previously it refused to start on a Mac because the CLI only sealed inherited descriptors on Linux; macOS can seal its own by listing /dev/fd, so the token handover now works there. macOS always mounts /dev/fd, so if it can't be opened the CLI fails closed rather than guessing. The background service stays Linux-only since it writes a systemd unit, and now says so on a Mac.
Fixes found by running the connector's tests on a real Mac:
- macOS refuses to signal a process group whose only members are zombies; the connector now treats such a group as gone when the kernel's own process list shows nothing running. Linux behavior is unchanged.
- A worker's MCP server that closed before its token was read is now recorded as "undelivered" instead of "refused", so its next start gets the token.
- The token's recipient is now named from the trust check before the token is written, so a server that reads its token and closes at once still resolves to the right process instead of a reused pid.
- A test's fake worker that blocked on an empty
select(a runtime deadlock on macOS) now sleeps instead.