MCP stdio vs Unix sockets depends on the trust boundary
MCP stdio vs Unix sockets compared for desktop secret brokers, covering lifetime, permissions, caller identity, cleanup, and deployment tradeoffs.

A desktop secrets broker should use stdio when authority belongs to one agent process and a Unix domain socket when authority belongs to a shared local service. That sounds like a transport choice, but bytes are the easy part. The hard part is deciding who may cause an action, how long that permission lasts, and what evidence remains after either side crashes.
I have seen teams start with a socket because it looks like infrastructure, then spend weeks rebuilding caller attribution and lifecycle controls that the process tree had already supplied. I have also seen stdio stretched into a resident service by layers of launchers, multiplexers, and hidden state. Both transports can be safe. Either can quietly defeat the threat model when its lifetime and identity assumptions do not match the authority being granted.
This comparison assumes a macOS desktop broker that holds API or SSH credentials and performs actions for local AI agents. The agent must never receive the secret. The broker therefore protects something more consequential than a local cache: it decides which process may turn stored authority into an external side effect.
Choose the trust boundary before the transport
The right transport follows the unit of authorization. If a user approves one agent run, a child stdio server gives that approval a natural boundary: the pipe exists for that process relationship and closes when one side exits. If several tools should share one unlocked broker, a Unix socket gives them a stable rendezvous point, but the broker must create its own session boundary above the socket.
Write the threat model as actors and actions, not as a general desire for secure IPC. Name the local processes that may connect, the credentials the broker holds, the operations those credentials permit, and the events that must terminate access. Include malware running as the same login user. File permissions often stop other users, but they do little against another process under the same account. Include a copied agent binary, a modified plugin, a shell launched from the agent, and an old process that survives after the visible job ends.
Then decide what approval means. It might authorize a signed executable, one operating system process, one process tree, one terminal job, or every client owned by the logged in user. Those are different promises. A transport cannot choose among them for you. It can only make some promises easier to enforce.
A useful design test is revocation. Ask what the broker can revoke immediately without locking the whole vault. With stdio, closing a pipe or killing the child ends that channel, although descendants may still have inherited file descriptors if the launcher was careless. With a socket, closing one accepted connection ends that client, while the listening socket remains available. If the authorization record outlives the connection, neither close operation is enough.
Do not put network attackers at the center of this local decision unless the broker also exposes a network listener. The sharper risks are confused identity, ambient authority from the login session, descriptor inheritance, replacement of a filesystem socket, and authorization that lasts longer than the work the user approved.
Stdio binds the channel to a process lifetime
MCP over stdio is strongest when the broker shim should live and die with the agent process. The client launches a server command, writes JSON-RPC messages to its standard input, and reads responses from standard output. EOF is a lifecycle signal supplied by the operating system, not a convention buried in an application heartbeat.
The Model Context Protocol transport specification also makes a useful operational demand: a stdio server must not write nonprotocol data to stdout. Logs belong on stderr. That rule is easy to dismiss as framing hygiene, but it prevents a diagnostic line from becoming an invalid message in a security boundary. Treat stdout as protocol memory. Configure libraries and crash reporters before the first response so they cannot contaminate it.
Stdio does not require a filesystem name, socket directory, permission mode, discovery file, or resident listener. An agent configuration points at an executable and arguments. This is a genuine deployment advantage for a desktop product because there is no endpoint to find and no stale path to repair. Updates also have a clear activation point: the next launched shim uses the new executable. A running session may continue on the old version, so record the executable identity and version with the session rather than assuming every active client changed at install time.
The process relationship is useful evidence, but it is not complete identity. A broker can inspect the shim that connected to its private backend, while the shim can inspect its parent process. Parent process identifiers can be reused after exit, and a launcher may sit between the visible agent and the shim. Capture the audit token or code signing information while the process is alive. Do not save only a PID and resolve it later.
Pipes also have inheritance traps. If a launcher marks descriptors as inheritable, a grandchild can keep the write end open after the agent exits. The broker then waits for EOF that never arrives. Set close on exec wherever the platform does not do it automatically, close unused ends immediately after spawning, and make the session watch both the pipe and the expected client process. EOF should revoke access, but process exit should independently revoke it too.
Concurrency is intentionally narrow. One stdio server instance normally serves one client process. That gives clean isolation and costs another process per agent. If the actual vault lives inside a desktop app, the stdio executable is usually a small shim that forwards typed requests to that app. The internal hop still needs authentication and session binding. Stdio at the MCP edge does not make an unauthenticated shared backend safe.
A Unix socket creates a service lifetime
A Unix domain socket fits a broker that is already a resident service and expects independent clients to arrive over time. The listening endpoint survives client exits, so a menu bar app can accept calls without making every agent own the broker process. Multiple clients, backpressure, and centralized upgrades are straightforward. The cost is that the service must define every boundary that a one client process supplied implicitly.
The socket pathname is discovery, not authentication. A client that can find it still needs permission to connect, and a client that can connect still needs an authorization decision. Put the socket in a directory controlled by the user or the application, create the directory with mode 0700, and set the socket to 0600. Avoid a predictable path in a shared writable directory. The sticky bit on a temporary directory prevents some deletion attacks, but it does not turn that directory into a trusted namespace.
Permissions depend on more than the final mode. The process umask affects creation. Existing parent directories determine whether another account can traverse or replace names. A symlink or stale node may already occupy the chosen path. The service should open a trusted parent directory, inspect an existing entry without following links, and remove it only after proving that it is the socket from a dead instance or a path owned by the current installation. Blindly unlinking a known name creates a replacement race.
On macOS, an accepted local socket can yield peer credentials through system calls such as getpeereid, and lower level APIs can expose an audit token. User and group IDs answer which account owns the peer process. They do not tell you which application the user intended to authorize. For that, resolve the audit token to the live process and evaluate code signing identity, executable path, and launch context according to the product's stated policy.
A socket makes multiplexing easy, but multiplexing can blur sessions. Never treat one successful connection as approval for every request carrying an arbitrary session identifier. The server should assign the connection identity, bind authorization to it, and reject a client supplied attempt to switch identities. If a helper reconnects after the service restarts, require a new authorization decision unless the threat model explicitly permits approval to survive that event.
Performance rarely decides this choice. Both local pipes and Unix sockets can carry small JSON-RPC requests far faster than an HTTP API or SSH operation completes. Measure if the broker transfers large response bodies, but do not exchange a legible authority model for a speculative reduction in local IPC overhead.
Socket permissions cannot prove user intent
Mode 0600 means that processes running as other user IDs cannot open the socket through ordinary discretionary access checks. It does not mean that every process with the same user ID deserves stored credentials. Desktop malware, an untrusted package script, an editor extension, and the approved agent commonly share that user ID.
This distinction gets blurred because Unix permissions are concrete and easy to inspect. Teams see a private socket and call the endpoint authenticated. It is authenticated only to the account boundary. If the threat model stops at other login users, that may be sufficient. A secrets broker for autonomous tools usually claims control within one login session, so it needs a narrower identity.
Code signing information can narrow that identity on macOS. Validate the live peer, not a path string supplied in the request. A path can point at a replaced file, and a process can keep executing an older mapped image after an update. Record the signing authority and designated requirement that the operating system reports for the process. Decide how ad hoc signed development builds behave; silently treating them as the production application turns a developer convenience into a bypass.
Caller identity and caller authority also differ. Identity answers which process opened the connection. Authority answers whether this process may use a particular credential for a particular action at this moment. A valid signed agent may be running an unreviewed repository, or its prompt may have been influenced by hostile content. Granting every action because the executable is familiar confuses provenance with consent.
For stdio, the launcher can present the immediate parent's identity when it starts the shim, and the broker can bind that evidence to a fresh channel nonce. For a socket, the broker can derive peer identity at accept time and bind it to the accepted file descriptor. In both designs, make the broker the source of identity. A field such as client_name is display metadata, never evidence.
An honest approval card should show what the operating system can establish and what the user is approving. If a wrapper script prevents reliable attribution to the top level agent, say so or deny the call. Guessing up the process tree until a familiar name appears creates an attacker controlled search path.
Cleanup is part of the security model
Stdio cleanup centers on descriptors and child processes. The normal case is simple: the client closes stdin, the server sees EOF, finishes or cancels active work, and exits. The abnormal cases matter more. The client can crash while a descendant holds the pipe. The server can hang while the client assumes it died. A privileged action can finish after revocation unless cancellation reaches the worker that performs it.
Give every request a broker assigned operation ID and a state such as queued, executing, completed, denied, or indeterminate. When the channel closes, revoke future requests immediately. For an action already sent to a remote API, do not claim cancellation unless the remote operation supports it. Record an indeterminate result when the local process dies before it learns the outcome. That entry prevents a retry loop from quietly performing the action twice.
Socket cleanup has two separate objects: accepted connections and the listening pathname. Close each client connection on protocol failure, failed authorization, idle expiry, or service shutdown. Remove the pathname only if the service still owns the same filesystem object. On restart, an EADDRINUSE error is a reason to investigate, not permission to unlink whatever exists.
This shell check is useful during development because it shows the path type, ownership, mode, and listening process without changing anything:
sock="$TMPDIR/com.example.broker.sock"
stat -f 'type=%HT owner=%Su mode=%Sp inode=%i' "$sock"
lsof -n -U "$sock"
A normal macOS result has this shape:
type=Socket owner=alice mode=srw------- inode=123456
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Broker 48102 alice 9u unix 0x0123456789abcdef 0t0 /.../com.example.broker.sock
Do not parse the sample values. Assert that the type is a socket, the owner is the expected user, the mode denies group and other access, and the process is the installed broker instance. Run the same inspection after a forced crash, an application update, logout, and a second launch. A cleanup design that works only after a normal quit has not been tested.
Use launchd ownership consistently if launchd starts the service. Mixing application managed startup with a launch agent can produce two instances that race over the same path. One component must own creation, readiness, and removal, and clients need an explicit unavailable state instead of repeatedly spawning more brokers.
Deployment effort moves to different places
Stdio is cheaper at installation time and more expensive when many clients need the same resident state. Each MCP client needs a command entry. The shim must locate the signed application or its private endpoint, negotiate a compatible protocol version, and report a clear error when the desktop app is absent or locked. Packaging must preserve executable permissions and code signatures. Shell startup files should not be required; GUI applications often launch without the environment a terminal user expects.
A Unix socket needs service installation, startup ownership, endpoint discovery, directory permissions, crash recovery, and compatibility across independent client and server upgrades. In return, every client can use one stable endpoint, and the service can maintain vault state and audit ordering in one process. This is attractive when the desktop app already runs continuously.
Endpoint discovery deserves a contract. $TMPDIR is per user on macOS, but environment inheritance can differ between a terminal, an editor, and a GUI launcher. A fixed path inside a protected application support directory is easier to reason about, though sandboxing and installation layout may constrain it. If clients obtain the path from a bootstrap command, authenticate the bootstrap result rather than accepting an arbitrary path from project configuration.
Version skew happens in both models. With stdio, the client chooses which shim executable it launches. With a resident socket, an old client may reach a newly updated service or the reverse during staged updates. Put a small version negotiation in the first exchange. Reject unsupported combinations before asking for approval, because the user cannot meaningfully approve a request the broker may parse differently than the client intended.
Operations teams sometimes prefer sockets because familiar tools can list them. Developers sometimes prefer stdio because a command can be run in a terminal. Neither convenience should become a debug backdoor. A diagnostic client that can submit privileged requests must go through the same attribution and approval path as an agent. A debug flag that skips authorization will eventually escape a development machine.
The deployment comparison changes if the broker is not resident. Starting a full UI application on every stdio connection creates slow launches and awkward prompts. Keeping a socket service alive solely to avoid a small shim adds update and cleanup work. Decide the application lifetime first, then make the external transport fit it.
Match the design to explicit threats
A platform team should record which design property answers each threat. The table below is a decision record, not a scorecard. A transport wins a row only when the surrounding implementation supplies the stated control.
| Threat or requirement | Stdio design | Unix socket design |
|---|---|---|
| Another login user attempts access | Private process descriptors and correct inheritance | Protected parent directory plus mode 0600 |
| Another process under the same user connects | Harder if only the launcher owns the pipe, but inherited descriptors remain a risk | Expected by default, so peer credentials and application identity checks are required |
| Approval should end with one agent run | EOF and watched process exit provide natural revocation signals | Server must create a session and bind it to a connection and process lifetime |
| Several independent agents share one vault | Each shim needs a private authenticated hop to the vault process | Resident service accepts separate authenticated connections |
| Broker crashes and restarts | Client sees EOF and launches a fresh instance or reports failure | Service must handle a stale path and force clients to reconnect and reauthorize |
| Client executable changes during a run | Captured identity remains attached to that session | Captured peer identity remains attached to that connection; reconnect triggers fresh evaluation |
| Simple first install | Command plus signed executable, with no endpoint setup | Startup registration, protected path, discovery, and cleanup are required |
| Central audit ordering | Requires the shared vault core to order events across shims | Natural in one resident service, though durable logging still needs design |
Two conclusions usually survive this exercise. First, a same user attacker erases most of the comfort offered by filesystem mode bits. Second, approval duration matters as much as endpoint access. A perfectly private socket with an approval record that lasts all day can grant more authority than a carefully scoped stdio channel.
Do not assign numeric weights unless the team can defend them. A severe credential can make one missed identity check dominate every deployment advantage. Write an acceptance test for each row instead. For example, launch an unapproved process under the same user and prove the broker denies it; approve an agent, kill it, keep a descendant alive, and prove the old authorization cannot be reused.
The choice can also be layered. MCP can use stdio between the agent and a small shim, while the shim talks to a resident desktop broker over a private Unix socket or another operating system IPC mechanism. This is often the right desktop shape, but it creates two boundaries. Authenticate both. The shim must prove which agent run it represents, and the app must prove that the shim reached the intended broker rather than an endpoint substituted by project files.
Put identity inside a verified session envelope
A small protocol envelope makes the security decision reviewable. It does not replace operating system identity. It binds facts that the broker has already verified to the request the broker is about to execute.
The broker should create the session identifier and nonce after inspecting the live caller. It returns them over the authenticated channel. Each later request carries that session identifier, a monotonically increasing sequence number, the requested capability, and the action parameters. The broker rejects an unknown session, a repeated or skipped sequence when strict ordering applies, a capability outside the approval, or a request received on a different channel.
{"session_id":"s_7M4K","sequence":12,"capability":"http:billing.read","action":{"method":"GET","path":"/v1/invoices"}}
The response should repeat the operation identity and state, not secrets or injected authorization headers:
{"operation_id":"op_01J8","sequence":12,"state":"completed","result":{"status":200,"body_ref":"activity:8841"}}
Treat these as protocol shapes, not universal field names. The useful property is that the server assigns identity, enforces ordering, and returns a durable reference to the recorded action. Do not sign this envelope with a key given to the agent; that would turn the agent into a credential holder. Bind it to the authenticated local channel and keep secret material inside the broker.
For stdio, the channel binding can include a random value delivered only through the fresh pipe plus the captured launcher identity. For a Unix socket, it can include the accepted connection and peer audit token. If requests may move between connections, define a deliberate resumption protocol with short lifetime and one time tokens. Ambient session identifiers copied from a log must not resume authority.
Keep approval data separate from display data. Repository path, agent supplied label, tool name, and requested rationale can help a person decide, but an attacker can choose them. The broker's record should distinguish verified process facts, user assertions, approved capabilities, and request content. That distinction pays off during incident review, when a polished label otherwise looks like evidence.
A desktop broker can use both without confusing them
For a signed, always running macOS app, the cleanest design is often stdio at the MCP boundary and a separately authenticated local hop into the app. The agent gets MCP's expected launch model and process scoped lifetime. The app keeps one vault, one approval surface, and one ordered audit history. The design succeeds only if the internal hop preserves the outer caller identity instead of collapsing every shim into one trusted client.
Sallyport uses this shape: MCP capable agents launch the bundled sp mcp stdio shim, while the app executes HTTP and SSH actions so secrets do not reach the agent. Its session approval begins with the process code signing authority, which addresses the weak point that socket permissions alone cannot answer.
That choice does not make Unix sockets generally wrong, and it does not make stdio sufficient by itself. A platform team building a broker with several native clients may reasonably expose a protected socket as its primary API. The team then owns peer inspection, session construction, stale endpoint recovery, and upgrade compatibility. If it cannot state how each one works, the socket is not ready to carry credentials.
Choose the design whose failure state is easiest to deny. When attribution fails, deny the action. When the broker cannot prove that an endpoint belongs to its current instance, do not connect or unlink it. When a client reconnects after either process restarts, create a new session. Those rules cost a little convenience and remove the silent continuity that makes local brokers dangerous.
The final architecture review should fit on one page: authorization unit, verified caller evidence, session start, session end, endpoint ownership, crash behavior, update behavior, and audit record. If any answer relies on the phrase "same user," the review should say whether same user malware is outside the threat model. That one sentence will reveal whether stdio and Unix sockets are being compared as transports or as substitutes for a security decision.
FAQ
Is MCP stdio safer than a Unix domain socket?
Not by itself. Stdio naturally scopes a channel to a launched process, while a Unix socket naturally supports a resident service; safety depends on whether that lifetime matches the authorization unit.
Do mode 0600 socket permissions authenticate the calling app?
No. They usually restrict access to the owning user, but every process under that user may still attempt a connection. Inspect the live peer and make a separate authorization decision.
Can a desktop secrets broker support multiple agents over stdio?
Yes. Run one shim per agent and have each shim use an authenticated private hop to the shared vault core. Keep session identity separate so one shim cannot claim another agent's approval.
What should happen when an stdio client crashes?
The broker should revoke future calls when it sees EOF or process exit and record the outcome of work already running. If a remote action may have completed, mark it indeterminate instead of retrying blindly.
How should a broker remove a stale Unix socket?
Inspect the existing filesystem object without following links and verify its type, owner, and relationship to a dead broker instance. Never unlink a predictable path merely because bind returned EADDRINUSE.
Are Unix domain sockets fast enough for MCP?
Yes for ordinary MCP requests. Local transport time is usually minor beside an HTTP API or SSH operation, so identity and lifecycle should decide the architecture before throughput does.
Should authorization survive a broker restart?
Usually not. A restart breaks the verified channel and process evidence, so require reconnection and a fresh session unless the product explicitly promises durable authorization and protects it accordingly.
Can the client send its own process name for attribution?
It can send a label for display, but the broker must not trust it. Derive identity from the live process, audit token, code signature, or launch relationship supplied by the operating system.
Where should a macOS app place a Unix socket?
Use a parent directory controlled by the user or application and deny group and other access. Also define how terminal, editor, and GUI clients discover that path without trusting project supplied configuration.
When does a hybrid stdio and socket design make sense?
It fits a resident desktop vault that serves separately launched MCP agents. The stdio edge supplies process scoped sessions, while the internal socket needs its own authentication and must carry verified caller context.