# Agent authority after Mac sleep and wake

An approved agent process should not inherit authority merely because a Mac woke up with the same processes still in memory. Sleep, display blanking, screen lock, lid close, and wake are different operating-system events, but they all create the same security question: did the person who approved this run remain present and able to intervene?

Treat that question as an authorization decision, not as a power-management detail. If you let a coding agent hold API or SSH authority, a stale approval can turn an ordinary coffee break, commute, or overnight pause into unattended credential use. The fix is not a maze of rules. It is a small state model, a clear expiration policy, and tests that force the awkward transitions people usually skip.

## Sleep is not one event

macOS distinguishes system sleep from screen sleep, and that distinction matters because a dark display does not prove that your agent stopped. Apple exposes separate NSWorkspace notifications for `willSleep`, `didWake`, `screensDidSleep`, and `screensDidWake`; the sleep and wake notifications carry no user data that tells your app why the transition occurred.

A laptop may dim or turn off its display while a build, network transfer, or local process continues. A desktop Mac can run with no active display. A notebook connected to power and peripherals can behave differently from the same notebook on battery. You cannot infer an authority decision from a pixel turning black.

Use four separate facts in your design:

- **Display state** tells you whether the screen slept or woke.
- **Power state** tells you whether the machine prepared for sleep or woke from it.
- **User-presence state** tells you whether the session is active, locked, logged out, or switched away.
- **Vault state** tells you whether secrets may be used at all.

Teams often collapse the first three into a Boolean named `isAwake`. That shortcut leaks authority. An agent may be running while the screen is asleep. A machine may wake to a lock screen. A user can lock the screen without sleeping the system. Each case needs its own expected result.

The right default is severe but easy to explain: a protected action needs an unlocked vault and a current approval that belongs to the current authority epoch. A wake, lock, user-session change, manual revoke, or vault lock can advance that epoch. Once it advances, calls from the old epoch fail.

## Screen lock should end interactive authority

A screen lock is the clearest signal that interactive approval should stop. The Mac may continue running, and a long-lived terminal process may still have its sockets and memory, but the approving user has left the interactive session. Continuing to send API requests or SSH commands under a previous click is difficult to defend.

This does not mean every agent must die on lock. Killing local computation is a separate decision. A model can keep reading a repository, compiling code, or preparing a patch if those actions do not require a credentialed gateway. The boundary is the external action. Keep the work, remove the authority.

That distinction is useful because it avoids an all-or-nothing choice. You do not have to choose between a frozen agent and a wide-open agent. Let the agent continue with tasks that are safe in its local workspace, then make the next protected call return a clear denial:

```text
authorization_denied
reason: authority_epoch_changed
required: unlock_vault_and_approve_session
```

A useful denial tells the agent what happened without leaking a secret or pretending the request failed due to networking. The agent can pause, record the blocker, and wait for the user instead of trying the same destructive command repeatedly.

Do not make an exception because the lock came from an idle timer. That is often the case where the user forgot the agent was still running. An explicit lock and an automatic lock have different human intent, but neither proves that the person remains available to approve a production change.

There is one narrow case for retaining authority across a lock: an intentionally unattended job with a separately granted, tightly limited capability. That should be a distinct run type, not a hidden exception to interactive approval. If your overnight automation behaves exactly like a daytime chat-driven agent, you have not separated the risks.

## A wake event begins a new authority epoch

Wake should invalidate interactive approval even when the approved process survives. A process can pause before sleep and resume afterward with the same PID, the same environment variables, and the same open file descriptors. None of that proves the old human decision still applies.

An authority epoch is a monotonic value that marks a continuous period in which a grant may be valid. When the system crosses a boundary you care about, increment the epoch before serving another action. The process does not negotiate this. It discovers the change at its next request.

A minimal authorization record might look like this:

```json
{
  "session_nonce": "6a018d62-2e94-4d4a-9e79-1f4e4b5ca501",
  "process_id": 84172,
  "process_start_marker": "2026-07-22T14:03:18Z",
  "signing_authority": "approved-agent-binary",
  "authority_epoch": 27,
  "approved_at": "2026-07-22T14:04:01Z",
  "per_call_approval": false
}
```

The process ID is only one field. macOS can reuse a PID after a process exits, so a bare PID becomes dangerous if the gateway forgets an old record and later sees the number again. Bind the approval to a fresh nonce and an observed process instance. Use the code-signing authority as an identity signal for the executable, then still require a new per-session approval for each new process run.

When your event handler sees `willSleep`, record the intent to revoke and immediately stop admitting new protected calls. Apple says an observer can delay sleep handling for up to 30 seconds, but do not use that allowance to complete a queue of pending actions. Deny them or mark them interrupted. The user did not authorize a last burst of work while the lid was closing.

When the machine reports wake, advance the epoch again if necessary and keep the vault gate closed until the user satisfies its unlock requirement. This handles imperfect event ordering. Power events are messy at the edges, and you should prefer a harmless extra approval over a secret-bearing call that slips through during a transition.

## Lid close deserves its own test case

Closing a notebook lid feels like a sleep command to a person, but software should not assume the physical gesture maps cleanly to one operating-system event. Power source, external displays, docking gear, and settings can change the machine's behavior. The only honest answer is to test the hardware and configuration that your team actually uses.

The security policy can still be simple: lid close ends interactive authority as soon as you observe a reliable related boundary. In a normal portable setup, `willSleep` gives you the early chance to block new actions. If a particular configuration keeps the system awake after the lid closes, use a user-session or display boundary as a conservative fallback. Do not wait for a perfect semantic label named `lidClosed`; the thing you need to prevent is unattended credential use.

Run this trial with an agent holding a benign credentialed capability, such as writing a marker to a test API or running a harmless command against a disposable SSH host:

1. Start a fresh agent process and approve its session.
2. Confirm one protected call succeeds, then leave the agent ready to make another.
3. Close the lid for long enough to trigger the expected power behavior, then reopen it.
4. Without unlocking or approving again, allow the agent to retry the protected call.
5. Confirm that the gateway denies the retry and records the authority transition before the denial.

Repeat the same trial while docked, on battery, and with an external display if people use those modes. Keep the test endpoint harmless. The purpose is to observe authority behavior, not to discover whether a production deployment can be interrupted halfway through.

A failure here often looks deceptively clean: the action log shows no call during sleep, then the first call after wake succeeds. That is still a failure if the user faced a lock screen and never approved the resumed run. The gap in time is the whole point of the test.

## Display sleep is a poor revocation trigger by itself

Revoking on display sleep alone is safe, but it may be too disruptive for a desktop Mac where the display sleeps during ordinary work. Keeping authority through display sleep alone is convenient, but it is also easy to get wrong when a display timeout doubles as an unattended-user signal.

Pick one rule and make the tradeoff explicit. For high-impact credentials, revoke on screen lock and system sleep, not merely on display sleep. For a machine where display sleep reliably occurs just before lock, you may choose to revoke at display sleep as an extra guard, accepting more approval prompts. The secret is not to call either choice "obvious." It depends on the deployment and what the credential can do.

Apple's separate screen and system notifications are a useful warning against assuming they mean the same thing. Monitor both, record both, and test the policy you attach to each.

A practical matrix keeps this from becoming folklore:

| Transition | Local agent work | Existing session approval | Vault-backed action |
| --- | --- | --- | --- |
| Display sleeps | May continue | Your stated policy decides | Usually hold or allow only low-risk use |
| Screen locks | May continue | End | Deny until fresh approval |
| System begins sleep | Pause naturally | End immediately | Deny new calls |
| System wakes to lock screen | May resume locally | Remains ended | Deny until unlock and approval |
| User unlocks | May continue | Still ended | Require a new session approval |

The user unlocks the Mac to regain their desktop. That action should not quietly restore an agent's previous authority. Unlocking a computer and approving an external action are related, but they answer different questions.

## Overnight runs need a separate contract

An overnight agent run should not inherit the permissions of an afternoon interactive session. People approve interactive work while looking at the diff, the terminal, or a request card. Overnight work is an explicit decision to let something proceed without that immediate oversight.

Make the job's contract narrow enough that you can explain it in one sentence. "Run tests and prepare a pull request" is understandable. "Do whatever is needed to finish the task" is not a contract; it is a blank check.

For unattended work, separate local actions from external actions. Allow the former when they are contained: repository analysis, edits on a branch, test execution, and artifact generation. Require fresh human approval for sensitive external effects, including a production API mutation, a deployment, a package publish, a write to a shared database, or SSH access to a machine that matters.

There are cases where an overnight run must call an external service. Handle those with a dedicated credential or a test environment whose blast radius matches the job. Do not reuse an administrator credential because it already exists in the vault. The popular argument is that the person approved the agent earlier that evening. That approval covered a visible, interactive run. It does not cover whatever remains after they fall asleep.

Use a bounded action budget if the job has a legitimate recurring external task. Bound it by destination, method, and effect rather than by vague confidence scores. For example, an unattended test job might be allowed to send a fixed request to one staging endpoint, but it cannot switch hosts, change HTTP methods, or use SSH. If the job needs more, it waits.

Sallyport's per-session authorization gives each new agent process its own approval boundary. Keep overnight work in a newly started process and let its protected actions face the same scrutiny as any other unattended run.

## Logs must prove denial, not just activity

A record that says "the Mac woke" is not proof that authority ended. You need evidence from both sides of the boundary: the operating system event that triggered a policy transition and the next protected action that the gateway refused.

macOS gives you a useful starting point for the power side of the investigation:

```sh
pmset -g log | grep -E 'Sleep|Wake|DarkWake|Display'
```

The exact lines vary by hardware and macOS release, but the output should show timestamped records with event names such as `Sleep`, `Wake`, `DarkWake`, or display transitions. Save the matching window from each trial. Do not use it as the source of truth for authorization, because it does not know your vault or your agent identity.

Your own journal should answer a different set of questions:

```text
14:20:16.402 session_approved process=84172 epoch=27 authority=approved-agent-binary
14:22:04.118 power_will_sleep epoch=27
14:22:04.119 authority_revoked old_epoch=27 new_epoch=28 reason=system_sleep
14:25:38.771 power_did_wake epoch=28
14:25:44.025 action_denied process=84172 request=ssh.exec reason=authority_epoch_changed
```

The order matters. If the action appears before the revocation entry, you found a race. If no denied action appears because your test agent quietly exited, you proved nothing about a persistent process. Arrange the trial so the same long-lived process attempts a protected call after every transition.

A tamper-evident audit trail adds a second benefit: it lets you verify after the fact that the event and action sequence was not edited into a prettier story. Sallyport projects its Sessions and Activity journals from a write-blind encrypted, hash-chained audit log, and `sp audit verify` checks that chain offline over ciphertext. That is useful for this test because a successful verification says the recorded sequence has not been silently rewritten; it does not excuse a weak revocation policy.

## Race conditions happen at the boundary

The dangerous bug is usually a request already in flight when the Mac begins to sleep or the user locks the screen. A gateway that only checks approval when it accepts a connection can let queued work execute after the authority transition. A gateway that checks only after it has injected a credential can leak the credential into a helper before it notices the revoke.

Check authority immediately before the privileged operation. That means before credential injection, before opening an SSH helper with a usable key, and before sending the HTTP request. If an operation has several privileged phases, check again at each phase where the system could otherwise cross an authority boundary.

A simple shape looks like this:

```text
receive request
identify process and session nonce
read current authority epoch
compare request grant epoch to current epoch
check vault gate
check per-call approval when required
inject credential and execute action
append result to audit log
```

Keep the epoch read and the commitment to privileged execution as close together as your implementation allows. You cannot make a distributed HTTP action perfectly reversible once bytes leave the Mac. You can stop a stale grant from starting it.

Do not try to solve every edge case by delaying sleep. Apple's `willSleep` notification permits a short delay for handling, but blocking a laptop from sleeping to finish agent actions reverses the priority. The machine is leaving an interactive state. Revoke access, record the interruption, and let the user decide what resumes.

Per-call approval is the clean answer for keys that can cause expensive or irreversible effects. It makes sleep and wake less interesting because every use already asks the user at the moment of use. That is not a reason to skip session revocation. It is a second barrier for a smaller set of credentials.

## Test the transitions people actually perform

A good test plan does not begin with unit tests of a notification handler. Those tests help, but the failures live in real power transitions, real lock screens, and agent processes that outlast a terminal window.

Build one test agent that can wait, receive a signal, and then request a harmless protected action. Give every trial a fresh run identifier. Keep a written expectation before you run it, because otherwise a surprising outcome becomes "probably fine" after the fact.

Test at least these cases on each supported Mac configuration:

- Lock the screen while the agent is idle, then retry an approved action before and after unlock.
- Put the system to sleep from the Apple menu, wake it to a lock screen, and retry with the original process.
- Let the display sleep while the system remains awake, then see whether the behavior matches your chosen display policy.
- Close and reopen a notebook lid, both on battery and in the desk setup people use.
- Leave an intentionally long agent run overnight, then inspect the power log, the action journal, and the first protected call after return.

Use a small result sheet for each run: expected epoch, observed power events, whether the original process survived, whether the vault was locked, and whether the first protected call was denied. That sheet catches a frequent mistake: teams test whether the app saw the event, but never test whether the action layer rejected a call afterward.

Also test the ugly timing. Start a protected action, lock the screen while it waits on a network response, then inspect whether a retry or follow-up action runs under the old grant. Start an action a fraction before sleep. Disconnect and reconnect a dock. Restart the agent process after wake and make sure it cannot borrow the old process's approval record.

You do not need a giant policy engine to make these tests pass. You need a strict vault gate, a per-process approval record, a revocable epoch, and an action path that checks them immediately before it uses a credential.

## Write the policy in outcomes, then enforce it

A good authority policy fits on a page because it describes observable outcomes, not a collection of guessed operating-system labels. State what happens to protected actions after lock, sleep, wake, lid close, logout, and manual revoke. State whether local work may continue. State what the user must do to resume.

For most interactive AI coding agents, the policy should read like this:

> A protected action requires an unlocked vault and an approval for the current process in the current authority epoch. Lock, sleep, user-session loss, vault lock, and manual revoke end that approval. Wake and unlock do not restore it. The agent must request a new approval before another protected action.

That rule has one cost: people approve again after returning to a Mac. Accept the cost. The alternative asks a user to remember every agent process that was alive before they closed a lid, then trust it to behave sensibly when the machine wakes hours later.

Run the tests before you call an agent setup safe for unattended work. If a request succeeds after a lock or wake without a new decision from the user, you have found authority that outlived the moment it was granted.
