Bastion 1.0.0
Version 1.0.0 ·
First release.
Added
One supervised server process per profile, instead of one per editor. Every MCP client that wants a server spawns its own copy, so three editors mean three processes holding three copies of the same credential. Bastion runs one, behind a gateway on
127.0.0.1, and clients reach it at/s/<profile>/<server>. A profile names a credential set —prod/shopify,rgis/keycloak— so the same server can serve two accounts without either knowing about the other.Children are started on demand, backed off when they crash, tripped out by a circuit breaker when they crash repeatedly, and stopped after half an hour idle.
Credentials live in the Keychain, and nothing reads them back. Bastion holds the secrets its children need and hands them over at spawn. Nothing in the app, and nothing any client can call, returns a credential once set. The built-in server can say which secrets a profile has; it cannot say what they are.
The gateway binds loopback and refuses anything that is not local. It validates
OriginandHoston every request and requires a per-client bearer token. Those are not preferences:make auditasserts all five rules against the built binary, because CVE-2025-49596 was exactly this shape — a localhost MCP listener with no CSRF protection that a visited web page could reach and execute code through.Both protocol eras, translated. Bastion serves modern clients that send per-request
_metawith no handshake, and legacy clients that expect aninitializehandshake, in front of children that are all legacy.make dialectasserts the translation, including the exact status codes a dual-era client branches on.Remote MCP servers over HTTP and SSE, with OAuth. A remote endpoint is fronted like any other server, so a client sees no difference between something running on your Mac and something running on Stripe's. The callback listener is audited on the same terms as the gateway.
An editable catalog, and on-demand installation. Nine servers ship seeded, and the list is yours to add to, remove from and switch off. Installing is a separate step from adding, so a failed download leaves something to retry rather than a half-added entry. A minimum package age can be set in Settings — npm can refuse versions published too recently, which is the window in which a compromised release tends to get caught.
Client wiring that leaves the rest of the file alone. Claude Code, Claude Desktop, Visual Studio Code and Codex are wired from the Clients pane, including the three surfaces Codex shares in one TOML config.
make wiring-checkasserts the property that matters: after Bastion writes one key, every other byte in somebody's config is identical. Hand-configured entries are shown rather than silently overwritten, and stdio-only hosts reach the gateway through the bundledbastion-bridge.Every call is recorded, and the record has limits. The Activity window shows which profile, which method, which tool and how long, with the arguments a tool was called with. A profile can record results too, or names only. Capture sits behind a secrets wall and a size cap: a tool whose argument is the credential —
set_credential— has its arguments withheld whatever the setting, because a naive capture would write a Keychain secret into a feed the built-in server hands back to a model. Nothing is written to disk, and none of it leaves your Mac.Bastion manages itself, through its own MCP server. An agent can install servers, set credentials, wire clients and probe a profile. It ships disabled and takes three deliberate acts to reach, it obeys the same per-profile write gate as everything else, and it cannot switch itself off or delete itself.
A Chat pane, for trying a server's tools by hand. Calling a tool yourself is how you find out whether a server works before wiring an assistant to it.
Licensing, verified offline. A key is an Ed25519 signature the app checks locally in microseconds; there is no activation server and no way to add one without breaking what
make auditasserts. A thirty-minute trial, started by hand and held in memory, runs every server at full function. The relay is what is licensed: Bastion's own server, and the write gates, are outside the gate on purpose. See docs/licensing.md.Updates, off until you say otherwise. Sparkle is pinned and checksum-verified, and the updater is not built at all until you opt in — so a Bastion nobody has said yes to has never resolved a name. It is the only outbound connection the app makes on its own account.
Notes
Requires macOS 26 or later.
The app ships with no entitlements file at all. Spawning children and binding loopback need none.