# Single-process vs daemon-based agent security tools

A process diagram can make a security product look either reassuringly simple or impressively serious. Neither reaction tells you whether an autonomous coding agent can misuse a credential, talk to an unintended local service, or leave an action unrecorded after a crash.

The useful comparison is concrete: count the trust boundaries, inspect the interfaces crossing them, and follow a secret from storage to the outbound request. A single application process can reduce local moving parts. A daemon can isolate privilege or keep a service alive across clients. Both designs fail when their authors confuse process separation with authorization.

## Process count does not define the security boundary

A single-process design puts the user interface, credential vault, approval state, request validation, network action, and logging in one address space. It has no local client socket between its own pieces, no independent service registration to maintain, and no protocol that one local process uses to command another. That can be a meaningful reduction in attack surface.

It also creates one large trust domain. If an attacker gains code execution inside that process, they may reach every component that process can reach. Memory corruption, malicious plugin loading, unsafe update behavior, or an overly broad scripting interface become especially consequential because the vault and executor sit beside ordinary application code.

A daemon-based design divides those jobs. A desktop application or command line client sends a request to a background service. The service might hold secrets and make the network call, while the client displays approvals and receives results. This can limit what a compromised client may do, but only if the daemon rejects requests that the client has no authority to make.

That condition gets skipped in architecture discussions. A Unix domain socket restricted to one user account is transport access, not proof that the caller is the intended agent or approved application. Every process run by that user may be able to open it. If the daemon accepts a request such as "use credential X against URL Y" from any local peer, then a malicious editor extension, shell script, or compromised agent can ask for the same action.

Separate these three ideas:

- A process boundary separates memory.
- A privilege boundary limits what code can do after compromise.
- An authorization boundary decides which caller may request which action.

Teams routinely treat the first as proof of the other two. It is not. Two processes running as the same user with an unauthenticated IPC channel have separate heaps, but they may still have one authorization domain.

A single process avoids an IPC authorization problem because its internal calls do not cross a local service boundary. That does not remove the need to identify an external agent session before approving an action. A daemon adds that identification problem twice: once at its client endpoint and often again at any administrative endpoint it exposes.

## A local socket is an API that hostile code can call

Daemon proponents often say, correctly, that the service listens only on localhost or a Unix socket. The omitted part is that local code is where most agent integrations run. The coding agent, terminal, editor, build scripts, package hooks, and browser helpers all share the machine.

Treat a local IPC endpoint as you would a small network API. Define message formats tightly. Reject unknown fields where your protocol permits it. Bind every request to a caller identity and a short-lived session. Put limits on request size, concurrency, and destination choices. Log rejected requests as well as successful ones, because repeated denials can reveal an integration trying to bypass the intended path.

On macOS, inspect a tool before you trust its claim that it is local only:

```sh
ps -axo pid,ppid,user,command | grep -i '[a]gent\|[d]aemon'
lsof -nP -iTCP -sTCP:LISTEN
launchctl print gui/$(id -u) 2>/dev/null | grep -i -C 2 'agent\|vault\|security'
```

The second command normally prints columns shaped like this when a process listens on TCP:

```text
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
service  4128 sam     9u  IPv4 0x...            0t0  TCP 127.0.0.1:48120 (LISTEN)
```

No output from `lsof` does not prove the tool has no IPC. Unix sockets do not appear in that TCP query. Check the application support directories, temporary directories, and the service documentation for socket paths. Then check permissions with `ls -l` and ask whether another process owned by your account can connect.

Apple's `launchd.plist` manual describes `KeepAlive` as a set of conditions under which launchd restarts a job. That is operationally useful, but it creates a security obligation. A restarting service must recover its locked state, its session state, and its audit state correctly. "It comes back automatically" is not an answer to what it accepts in the first second after restart.

A good daemon makes caller identity part of the protocol, rather than an assumption supplied by the client. The exact mechanism depends on the operating system. On platforms that can expose peer credentials for a local socket, use them. Where code-signing information is available, verify it before granting a session. Do not accept a process name, a PID supplied in JSON, or a path typed by the caller as identity. All three are easy to spoof or stale by the time you inspect them.

## Startup behavior decides whether protection exists when it is needed

A single application process usually starts when the user opens the app. Its sensitive state has a straightforward lifecycle: the app starts, the user unlocks it, approved sessions run, and closing or crashing the app ends access. That is easy to explain and easy to test.

The cost is availability. A command line agent cannot use the gateway while the app is closed, still launching, locked, or blocked on a system permission prompt. That may be exactly the right behavior for a human-controlled credential gateway. It is a poor fit if the intended job must run unattended after a reboot and the product has no secure way to recover its authority.

A daemon usually starts at login, on demand, or at boot. Each choice changes the threat model. A service at login may wait for the user's key store and desktop session. A boot service may run before a user can approve anything. An on-demand service can shrink idle exposure, but its first request must not race initialization.

I have seen the ugly version of this failure: the UI launches slowly, the background service is already accepting requests, and the service treats "no approval state loaded" as equivalent to "no approval required." Developers write that branch to avoid a startup error. Attackers and flaky automation do not care why it exists.

Write the state machine down. It should answer these questions without hand waving:

1. Can the executor accept a request before the vault reports locked or unlocked?
2. What happens to an approval tied to a client process when that process exits?
3. What does the service do after it restarts while a request is in flight?
4. Can a pending user prompt survive a restart and attach to a different caller?
5. Does logout revoke the service's usable credential state?

For high-risk actions, fail closed during every ambiguous transition. A request that arrives while the service initializes should receive an explicit denial or retryable unavailable response. It should not inherit an old approval, a cached decrypted credential, or a default allow decision.

Startup also affects user behavior. If an agent workflow breaks because a user must manually resurrect a hidden service, people will disable the gate or store a token in an environment variable. Security controls that cannot survive ordinary sleep, logout, and restart are training users to bypass them.

## Credentials must reach the executor without passing through the agent

The central credential question is not whether a tool encrypts a vault. It is whether the agent ever receives the secret in a form it can copy, print, place in a file, or transmit to another endpoint.

The safest arrangement keeps the credential in a vault controlled by the gateway. The agent submits an action request. The gateway checks its authorization state, injects the credential into the outgoing HTTP request or SSH operation, executes the action, and returns the result. The agent receives data produced by the remote system, not the bearer token or private key.

This distinction matters because agents work in text. If a token appears in tool output, a model can echo it into a shell command, source file, issue description, build log, or chat transcript. Redacting a display field afterward does not repair the exposure. The secret already crossed the boundary.

OWASP's Secrets Management Cheat Sheet advises teams to avoid hardcoding secrets and to rotate them when exposure is suspected. That advice is right but incomplete for agent workflows. A secret can avoid source control and still leak through a tool response. The gateway must prevent disclosure at the moment it executes the action.

A daemon can protect this boundary well when the daemon owns decryption and outbound networking. The client sends an operation description, never a request for raw secret material. The daemon should also avoid returning overly broad diagnostics. A failed authentication response may tell the caller that a request failed. It does not need to return the injected authorization header, a serialized SSH configuration, or a memory dump from the credential provider.

A single process can follow the same rule. Its internal executor reads the vault and makes the call, while the agent communicates through a constrained integration point. The benefit is that no second process needs access to decrypted material. The risk is that ordinary features inside the app might sit in the same memory domain as the secret.

Avoid these tempting arrangements:

- Putting API tokens in environment variables for the agent process.
- Writing an ephemeral credential file that a subprocess reads.
- Returning a placeholder that a client later exchanges for the real token.
- Letting a local daemon expose a "get secret" method for convenience.
- Passing SSH private key bytes through standard input.

The placeholder pattern deserves special suspicion. A placeholder is safe only if it never grants credential access outside the gateway and cannot be replayed by another process. In practice, teams often turn it into an undocumented bearer token. Then they have built a second credential with weaker lifecycle controls.

For SSH, require the same discipline. A helper may need to use a key to establish a connection, but the agent should ask it to perform a specific remote command or tightly bounded connection operation. Giving an agent a private key because the agent "only needs it briefly" is still giving it a private key.

## More processes create more upkeep, not more maturity

A daemon has operational work that a one-process desktop app avoids. Somebody must install it, start it in the correct user or system context, update it, verify its binary, handle crashes, remove stale registrations, and make sure its socket and log files have correct ownership after upgrades.

This is not an argument against daemons. It is an argument against pretending those jobs disappear into the operating system. `launchd` can restart a job. It cannot decide whether a new binary retains the expected caller checks, whether an old socket survived an upgrade, or whether a service now starts with a different entitlement set.

A daemon also complicates version matching. A command line shim may speak protocol version A while the installed service expects version B. If the authors handle that mismatch badly, the client falls back to an unauthenticated compatibility route or disables checks to preserve a smooth experience. Compatibility code has caused more trouble than many teams expect because it runs precisely when the system is least understood.

Use explicit rejection for incompatible versions. The error should tell the user to update one side, not silently negotiate a weaker mode. Keep the protocol small enough that you can test older clients, service restarts, malformed messages, and concurrent requests without building an entire test lab around it.

Single-process apps still need update discipline. A signed application can change its own implementation and its vault schema. The difference is narrower: there is one executable lifecycle to inspect, and the process that presents approval is the same process that performs the protected action. An update can still introduce a flaw, but it does not automatically introduce a local RPC protocol and service manager state.

The tradeoff becomes sharper for teams. A central daemon can offer a stable local endpoint to several tools, which may reduce duplicate integrations. It can also become a shared point of failure. One stuck service can block every developer workflow on the machine. One service with overly broad permission can give every local client access to every configured credential.

Do not treat a background menu bar icon as proof that a daemon is healthy. Test the harder cases: force quit the UI, kill the service, reboot during an active session, upgrade the client first, upgrade the service first, and remove a credential while a request waits for approval. If the answer to any case is "it probably recovers," you do not yet know the operational behavior.

## Approval belongs to an identified run, not a remembered machine

Approval systems fail when they authorize a vague concept such as "this computer" or "the current user." Autonomous agents often spawn child processes, restart after edits, and call tools through shells. A permission that follows all of that activity is too broad to mean much.

Bind approval to a specific process run or another identity that cannot be copied by unrelated local code. The approval screen should show enough provenance for a human to notice a surprise: the executable's signing authority, the command path, and the request class are better evidence than a friendly client name alone. A process name can be chosen by anyone.

Then make the duration explicit. One approval for a short agent session may be reasonable for low-risk repeated calls. Sensitive credentials may require consent each time. Revocation must stop the identified run immediately, rather than merely hiding it from a dashboard while its local connection stays open.

A daemon has an extra burden here. It must map an IPC connection to the identity a person approved, and it must discard that mapping when the client exits or reconnects. Never let a client resume an old session merely by presenting a cached identifier. If the identifier can travel through a file or command argument, it is a bearer credential with a fancy name.

A one-process design can keep approval state beside the active integration, which reduces the mapping work. It still needs to distinguish one agent launch from another. If the app cannot tell whether a request came from the approved run or from a later replacement process, it should prompt again.

The useful policy is usually simpler than teams expect: deny while the vault is locked, ask once for a newly identified run, and require an individual confirmation for credentials that deserve that friction. Elaborate local policy languages often create rules nobody can audit under pressure. A short decision path that users can predict is easier to test and harder to accidentally bypass.

## A restart failure exposes the weak seam

Consider a developer who has approved a coding agent to make ordinary HTTP calls for one active terminal run. The gateway stores the approval in memory. The agent submits a request, and the gateway begins preparing the outbound call. At that moment, the UI crashes or the background executor restarts after an update.

A careless daemon implementation can make this worse in several ways. The executor returns before it has loaded the vault lock state. Its client socket reappears at the same path. The agent reconnects. The daemon sees a cached session identifier on the request and assumes that the old approval remains valid. Meanwhile, its audit writer has not reopened the log, so the successful retry lacks a durable record.

Every individual choice sounds convenient: preserve a session, reduce prompts, restart quickly, buffer logs. Together they create an unapproved action during the one period when the system has the least reliable state.

The safe behavior is plainer. On restart, discard volatile approvals. Keep the vault unavailable until its protection state is known. Refuse new actions until the audit writer is ready to commit a record. Make the agent receive a denial or a temporary unavailable response, then require a fresh approval if the session no longer exists.

You can exercise this without special test hardware:

1. Open a new agent session and approve an allowed low-impact request.
2. Start another request that waits at an approval point or test endpoint.
3. Terminate the security process or service while the request waits.
4. Restart it and retry the request from the original agent process.
5. Verify that it needs fresh authorization and that the final outcome appears once in the audit record.

Run the same exercise with a client reconnect, a device lock, a removed credential, and an upgrade. You are looking for stale authority, duplicate execution, or missing records. A tool that cannot make these outcomes visible is hard to trust in an incident.

## Audit records need a writer that cannot be bypassed

Security logs often record what the user interface saw, not what the credential executor did. That gap becomes serious when a daemon accepts local requests directly. A compromised client may skip the UI, or the UI may crash after the executor acts.

Put the audit write on the action path. The component that sends the HTTP request or invokes the SSH helper should record the caller identity, credential reference, destination, request class, decision, and outcome. Do not log raw secrets or bodies by default. Logs that contain bearer tokens become another vault with worse access control.

A single process can make this ordering direct: validate, record the decision, execute, and record the outcome. A daemon must ensure that its own executor cannot perform a request over a side channel that avoids the logging component. Separate logging over IPC from execution only when you can tolerate the failure mode and prove the ordering.

Tamper evidence matters after compromise, not only during routine troubleshooting. If an attacker can edit a local SQLite file or delete selected lines from a text log, the record may help with debugging but it does not establish a trustworthy history. An append-oriented record with cryptographic chaining gives investigators a way to detect alteration, provided they retain the needed files and verify them.

Sallyport keeps session and call views projected from one encrypted, hash-chained audit log, and its `sp audit verify` command checks the chain offline without needing a vault key. That design addresses a problem that separate UI and daemon logs often create: two records that disagree about whether an action occurred.

Do not overstate what audit can do. A chain cannot restore an event that an attacker prevented the program from writing. It can detect changes within the record set it receives. That is still useful, especially when a crash, an upgrade, or an unexpected local client leaves you reconstructing a sequence after the fact.

## Choose the smallest design that meets the lifecycle you need

Choose a single process when the protected work is human-present, the application can be open while agents run, and reducing local endpoints is more useful than surviving independently of the UI. This layout fits a credential gateway whose main promise is that a person sees and controls each agent run.

Choose a daemon when the job genuinely needs service continuity, separation from a less trusted client, or a distinct operating system privilege. Demand an answer for every interface it adds: who connects, how the service identifies them, what request they may make, how state expires, and what happens when either side restarts.

A desktop tool such as Sallyport takes the single app process route while using a regular MCP shim for agent requests. Its vault performs the HTTP or SSH action itself, so the agent never receives the configured API or SSH credential.

Before adopting either model, perform one unsentimental check. Kill the component that holds authority while an agent works, restart it, and inspect whether the next request is denied, reapproved, and logged. That test reveals more about the design than a polished architecture diagram ever will.
