macOS logout security: end local agent authority
macOS logout security should revoke vault access, agent approvals, and queued actions before a departed developer can trigger another call.

A developer logout must end local agent authority, even when an agent process, a network request, or a menu-bar app has not noticed yet. Treating logout as a polite request to clean up creates a window where an unattended machine can still act with a human's credentials.
This matters most for coding agents because their useful work crosses a boundary. They call APIs, open SSH sessions, create tickets, publish packages, or change infrastructure. If their authority came from the person at the keyboard, it expires when that person leaves the macOS session. The system should preserve the evidence of what happened, but it must not preserve the ability to do more.
The mistake I see repeatedly is mixing three different things: a secret stored locally, an approval granted to a process, and an action already underway. They need different shutdown behavior. One blanket quit handler is too late and too vague.
Logout is an authority boundary, not an application event
macOS logout security means the absence of an interactive user session has to deny future credentialed actions, regardless of whether every application exits neatly. A desktop application may receive termination notifications during normal logout, but security cannot depend on those notifications arriving, completing, or running in a convenient order.
A user can close a laptop, switch users, force-quit an app, lose power, or trigger logout while an agent waits on a slow endpoint. The operating system can also tear down processes in an order your code did not expect. A design that says "we will revoke access in applicationWillTerminate" has already accepted too much uncertainty.
Apple's launchd documentation makes the scope distinction that matters here. It separates system, user, and graphical login domains. A job in a graphical user domain belongs to a particular logged-in session, while a system job has a different lifetime and authority model. Do not read "the process is still running" as proof that it still has permission to act for the departed user.
Build the authorization check around a current session fact that the action gateway can verify at call time. Every action must ask, in this order:
- Is the vault currently open for this logged-in user session?
- Does this request belong to a live, authorized agent process in that same session?
- Does this credential require an approval for this individual use?
- Has a logout or session-revocation event already advanced the session generation?
The fourth check prevents a subtle race. A request can pass the first three checks, sit in a queue, and reach the executor after logout starts. A generation number, checked again immediately before credential injection or SSH execution, makes that stale request fail.
Do not make logout behavior depend on an agent agreeing to stop. The agent is an untrusted caller at the boundary. It may be confused, busy, compromised, or simply gone.
A locked vault must deny before cleanup begins
The vault gate should close first, and it should close synchronously from the executor's point of view. Once the gate closes, no new HTTP credential injection and no new SSH authentication may begin. Cleanup can follow, but it cannot be the mechanism that makes the system safe.
This order sounds obvious until an app maintains a request queue. A typical failure looks like this: an agent submits five deploy calls, the UI begins logout cleanup, the app clears the visible session card, and a worker thread dequeues the fourth call with a credential reference it resolved earlier. The app appears logged out while the action still reaches the outside service.
Keep a single authority object in the process that owns secret use. It should contain an opaque session identifier, a generation counter, and an enabled state. Workers never receive secret bytes. They receive an action request and must obtain a fresh authorization lease immediately before the vault performs the action.
A small shape is enough:
AuthorityState {
sessionID: 6C17...
generation: 41
vaultOpen: true
logoutStarted: false
}
execute(request):
lease = authority.issueLease(request, generation: 41)
vault.perform(request, lease)
beginLogout():
authority.logoutStarted = true
authority.vaultOpen = false
authority.generation = 42
cancelPendingRequests()
vault.perform must reject the lease if its generation no longer matches. That second comparison belongs as close as possible to the point where the vault supplies an HTTP header, starts an SSH helper, or signs a request. Checking only when the request enters the queue leaves a race large enough to matter.
For a macOS app that protects its vault with Secure Enclave and Touch ID, hardware-gated access is useful because it gives the gate a clear local owner. It does not remove the need for session state. A biometric prompt that was accepted before logout cannot authorize a call after logout.
Sallyport's vault gate follows this rule: while it is locked, every action is denied. That fixed boundary is preferable to a shutdown exception list, because exception lists spread until nobody can explain which call still escapes them.
An active approval belongs to one process and one session
A user approval should bind to a specific agent process, its code-signing authority, and the current macOS login session. It should never mean "this account approved this tool sometime earlier."
Process identity is more than a process ID. Process IDs get reused. A caller that records only PID 4812 can accidentally bless an unrelated process after enough churn. Record the process start time and a code-signing identity along with the PID. If the process is a child of a terminal or editor integration, keep enough parent information to explain the route in the approval record, but do not make ancestry your only trust signal. Shell wrappers and process supervisors change it constantly.
The approval card should lead with signing authority, because that gives the developer a meaningful question: "Do I want this signed agent process to act in this session?" A package name or an arbitrary string supplied through MCP does not answer that question.
When logout begins, discard every active approval for that session. Do not suspend it. Do not serialize it for the next login. Do not recreate it because the same binary returns after a restart. The developer must approve the new run as a new run.
This also applies to approval dialogs already on screen. They should disappear or become inert when the session changes. A delayed click on an old card must not reactivate a now-dead authorization. Give each approval prompt an expiration tied to the same session generation that guards requests.
There is a useful distinction many implementations blur:
- A vault unlock permits the local gateway to consider actions.
- A session authorization permits one identified agent process to submit actions.
- A per-use approval permits one particular credential use.
A logout invalidates all three, but they do not carry the same evidence or timing. The vault closes immediately. Session approvals become invalid as a group. Per-use prompts fail individually because their session generation changed. Collapsing these into one Boolean makes it hard to audit why a call succeeded or failed.
Running tools need cancellation and honest uncertainty
Logout should stop work that has not crossed the external boundary and attempt to stop work that has. It cannot reverse an operation that a remote service already accepted.
Separate an action into states that have operational meaning:
queued -> authorized -> dispatched -> response received
\-> cancelled
A queued action has not left the Mac. Remove it from the queue and report cancelled_before_dispatch. An authorized action may hold only a short-lived internal lease. Invalidate the lease before dispatch and report revoked_before_dispatch if the worker reaches it too late.
A dispatched action is different. The remote system may have received it even if the local process never receives a response. Do not report it as cancelled merely because you closed a socket or killed a helper. Record logout_during_dispatch, capture the request identifier if the remote protocol provides one, and tell the user that the outcome is unknown until they inspect the remote system.
HTTP requests deserve special care. Closing a client connection can stop an upload before the server reads it, or it can happen after the server committed a change. Idempotency tokens reduce the damage when a user later retries, but they do not turn an uncertain request into a cancelled request. For operations that create an external resource, send an idempotency identifier that the remote API actually honors, then log that identifier without storing the credential.
SSH is even less tidy. Sending a signal to a local helper may kill the local process while the remote command keeps running in its own process group. When you control the remote environment, run long work under a remote job supervisor with an explicit job identifier and cancellation path. When you do not control it, say so in the activity record. Pretending you cancelled a remote migration because the local terminal closed is how people make a bad incident worse.
Do not let logout wait indefinitely for cleanup. Close authority first, ask workers to cancel, give the application a short bounded cleanup interval, and let the operating system finish logout. The security property is denial of future use. A graceful exit is a best-effort convenience.
Background persistence changes the threat model
A per-user app that keeps acting after the user logs out has changed from a desktop assistant into an unattended service. That may be appropriate for a deliberately designed service account. It is not appropriate as an accidental side effect of a menu-bar application.
Avoid installing a privileged helper or system-domain launch job merely to keep an agent alive through logout. That move is popular because it makes long jobs appear reliable. It also detaches the action path from the person who approved it and often widens access beyond the original user session.
If a team truly needs work to continue after a developer leaves, give that work a separate home. Use an explicit service identity, credentials with narrow remote scope and expiry, defined ownership, an audit trail, and a cancellation procedure that another operator can use. Make the handoff visible. A local agent should not silently inherit that role.
Fast User Switching exposes the same issue. User A may leave a graphical session open while User B signs in. User B must not be able to approve or observe User A's agent authority. Bind each gateway instance, approval record, and vault access check to the correct user and graphical session. A machine-wide daemon that casually multiplexes both users needs very careful isolation; most desktop tools should avoid that architecture.
Sleep is not logout. A sleeping Mac may resume the same user session, so teams need a separate decision for sleep and screen lock. For sensitive credentials, closing the vault on screen lock is often sensible. For less sensitive local work, the gateway may retain the vault state but require a fresh approval after wake. Whatever policy you choose, do not describe it as logout behavior. Users and incident reviewers need precise words.
Logs should outlive authority without becoming a second secret store
You need a durable record of the authority that ended, especially when a network action overlapped with logout. You do not need a second database full of tokens, request bodies, or SSH private material.
Write lifecycle events as facts: session opened, agent process approved, request submitted, credential use authorized, dispatch began, logout observed, lease revoked, helper termination requested, and final outcome. Include stable identifiers that let an operator connect the events, but minimize user content. An activity record can say that an HTTP call to a configured endpoint succeeded without retaining an authorization header or a sensitive response body.
A hash-chained audit log adds a property ordinary application logging lacks: an offline verifier can detect removed or altered records. That is useful after a disputed deployment or a suspected local compromise, but it does not make a log truthful by magic. The log proves continuity among the records it contains. It cannot prove that a malicious process never stopped logging before it acted.
For that reason, write the revocation event before any best-effort process cleanup. If the app crashes while killing a helper, the record should still show that local authority ended and that the action outcome may be uncertain. An audit trail that records only clean completions teaches operators the wrong lesson.
Sallyport projects both its session and activity journals from one write-blind encrypted, hash-chained audit log, and sp audit verify can check that chain offline over ciphertext. That lets a team check continuity without opening the vault merely to inspect whether logout revoked a run.
The useful audit question is specific: "Which process had authority, which credential path did it request, and what did the gateway know when the user left?" A giant debug log rarely answers it.
Test races instead of testing a clean quit
A logout test must make an action overlap the boundary. Tests that call an application shutdown method after all workers finish only verify that normal cleanup works.
Start with an endpoint under your control that accepts a request, records its receipt, then delays its response. Submit an action through the agent gateway and trigger logout after the gateway has marked the action dispatched but before the response returns. On the next login, compare the local audit state with the endpoint's record. The expected result is not always "cancelled." It is an accurate state such as logout_during_dispatch plus a request identifier you can investigate.
Use a separate test for queued work. Pause a worker after it takes an item from the queue but before it asks the vault for an action lease. Initiate logout, release the worker, and assert that it gets a revocation result rather than sending anything. This test catches the common mistake of checking authorization only when requests enter the queue.
Then exercise the uncomfortable cases:
- Switch to another user while the original session remains open.
- Lock the screen, wake the machine, and test the policy you chose for that transition.
- Force-quit the agent while a request awaits approval, then start a new process with a reused PID if your harness can arrange it.
- Interrupt the gateway during shutdown and inspect whether the audit record still identifies unresolved work.
- Send an SSH command that starts remote work, then confirm remote-side behavior rather than trusting local helper exit status.
On macOS, inspect the launch domains during test setup so you know what you actually started:
uid="$(id -u)"
launchctl print "gui/$uid" | grep -E "(agent-gateway|your-test-label)"
The output varies by installed job and macOS version, but it should show the matching job inside the current gui/<uid> domain. If your test job appears under a system domain instead, your logout result says little about a normal desktop application's session behavior.
Do not automate real logout against a developer's primary account first. Use a disposable local account, a disposable API credential, and an endpoint where you can inspect every request. Logout tests can destroy unsaved work and leave remote state half-changed. That is not a reason to skip them. It is a reason to stop treating them as a unit test.
Remote credentials should limit the blast radius of a late call
Local revocation cannot reach back through time and revoke a bearer token a remote service already accepted. Remote credential design limits the harm if the gateway discovers a request too late, an app crashes, or a machine is compromised before logout.
Prefer remote credentials that have narrow permissions and short lifetimes where the target supports them. Use separate credentials for different environments. An agent that can update a staging deployment should not receive a credential that can delete production data simply because both endpoints happen to use the same API vendor.
For SSH, use a dedicated remote account and restrict what that account can do. If a command must launch a long-running task, make the task's ownership and cancellation visible on the remote side. A personal SSH identity with broad shell access is convenient until you need to explain a job that continued after its local parent disappeared.
Do not pass credentials to the agent as environment variables, configuration text, placeholder substitutions, or shell arguments. Once a secret enters the caller, logout can stop future local actions but cannot make that copy disappear from process memory, shell history, crash reports, or a transcript. A gateway should inject the credential only into the action it executes and return the result to the agent.
That design gives logout a clean job: close the vault, invalidate active grants, reject stale leases, stop pending work, and record uncertainty for work already sent. It cannot promise to undo the internet. It can prevent the next call from borrowing authority from a person who is no longer there.
The exit condition should be easy to explain
Write the rule in a sentence a tired developer can use during an incident: when the macOS user session ends, no local agent process may initiate another credentialed action under that user's authority.
Everything else follows from that rule. The vault gate closes before cleanup. Approvals die with the process and session that earned them. A queued request loses its lease. A dispatched request gets an honest unknown state until a remote system confirms the result. Logs remain available for review, while secrets and executable authority do not.
If a product needs authority after logout, build an explicit unattended service with its own identity. Do not smuggle that decision into a desktop application's shutdown path.
FAQ
Is locking a Mac screen the same as logging out for agent security?
No. A screen lock protects the console from casual use, but the user session and its processes may remain alive. Treat screen lock as a reason to restrict or require fresh approval, while logout should terminate local authority and invalidate the session completely.
What should happen if an agent makes a request during logout?
It should deny immediately. The vault gate must close before any delayed shutdown work, cleanup request, or user interface animation. If a tool already started an external operation, record its known state and prevent follow-up calls after logout.
Should an approved agent remain approved after the user logs back in?
No. An approval belongs to one agent process inside one logged-in user session, not to the account in general. A new login needs a new process identity check and a new authorization decision.
Should audit logs survive a macOS logout?
A safe system should preserve evidence, not authority. Keep an encrypted audit record of the session, approvals, denied calls, and any logout revocation, but do not restore executable credentials or approval state from that record.
Can logout safely stop an SSH command or HTTP request already in progress?
Attempt to stop it, but do not pretend that termination proves an external action stopped. Record the process identifier, its parent, its command, and the action state before revocation. For remote work, use narrowly scoped credentials and remote-side cancellation or expiration where the service supports them.
What if a per-user macOS process survives logout?
It should fail closed. The app must treat a missing interactive login session as an authority failure, even if a background process remains alive briefly. A later login may start a new service instance, but it must not inherit the old one’s grants.
Should autonomous agent work continue after the developer logs out?
Only if the user deliberately built a separate service authority with separate credentials, ownership, audit records, and shutdown rules. A desktop agent gateway that borrows a developer’s local authority should not quietly become a server just because a command takes a long time.
How can an agent use API or SSH credentials without keeping them after logout?
Yes, if the app runs on the same Mac and owns the credentials, it can make logout revoke authority without placing secrets in the agent process. Sallyport keeps secrets in its encrypted vault and performs the HTTP or SSH action itself, so the agent receives a result rather than reusable credential material.
What should an audit log record when local agent authority ends?
At minimum, record the time, user identity, session identifier, agent process identity, approved authority, action identifier, action channel, outcome, and the reason for revocation. Do not place raw request bodies, response bodies, tokens, or private SSH material in a broadly readable log.
Is macOS logout suitable for long-running production agent jobs?
Do not use a user session as a casual place to run unattended production automation. Put unattended work behind a service account, short-lived remote credentials, explicit job ownership, and a reviewable deployment path. A developer logging out should not leave a personal desktop holding production authority.