# How agent approval prompts prevent cross-run approval

An approval queue for autonomous agents is not a stack of permission buttons. It is a live attribution system. When two or more agent runs can request the same credential, call the same API, or open the same SSH connection, a reviewer must be able to answer one narrow question before approving: *which running process is asking me to authorize this exact action?*

Most approval interfaces fail because they make every card look like it came from one generic actor called “the agent.” That label is harmless during a demo with one terminal window. It becomes dangerous when a coding agent repairs a production incident while another agent prepares a release, both using the same client and both asking for access to the same service. The reviewer sees a familiar destination, recognizes the task in broad outline, and approves the right action for the wrong run.

## Approval is an attribution decision

A reviewer does not approve an HTTP request in isolation. They approve a request *because it belongs to a particular piece of work* that they expect to be happening. If the interface cannot preserve that relationship, the approval button becomes a guess dressed up as human control.

Treat every approval as a statement with five parts:

1. A particular executable or agent process initiated it.
2. A particular session of that process is still active.
3. That session claims a named task or work item.
4. It wants to perform one concrete external action.
5. The reviewer either authorizes that tuple or declines it.

The fifth item matters more than it sounds. “Approve access to the deployment API” is not a complete statement. A reviewer may be willing to let the release run read a build record but unwilling to let an unrelated debugging run change a release setting. The destination alone cannot carry the meaning.

This distinction gets blurred because the same agent client often creates both runs. The command line may be identical. The code-signing authority may be identical. The repository path may be identical. The agent may even generate nearly identical requests. None of that makes two simultaneous invocations the same security subject.

I have seen teams add more words to the action summary when the real defect was missing attribution. They change “POST /deployments” to “POST /deployments to trigger staging deployment for version 1.8.4.” That helps explain the operation, but it still leaves a reviewer with two cards that both claim to be staging deployments. A better sentence cannot repair a missing session boundary.

The approval interface must make the identity hierarchy visible. The action is the primary object. The initiating process and the active session explain why that action has appeared. The task label helps the reviewer recognize intent, but it does not replace either technical identifier.

## A signed process is not a running session

A code-signing identity can tell a system who signed code, but it cannot tell a reviewer which invocation of that code produced a pending request. Apple’s code-signing documentation draws the same boundary in different language: code requirements establish code identity, while a designated requirement describes what counts as the same code across versions. That is useful evidence about the caller, not a substitute for a run identifier.

This matters when an approval prompt leads with something like:

```text
Request from: Acme Agent CLI
Signed by: Example Engineering, Team ABCD1234
```

That is a good start. It tells the reviewer whether an expected client asked for access. It does not answer whether this is the release assistant started five minutes ago, the test assistant left open from this morning, or a copied shell command running in a second terminal.

Process identity and session identity answer different questions:

| Field | Question it answers | What it cannot answer |
|---|---|---|
| Code-signing authority | Who produced this executable? | Which invocation is this? |
| Executable path | Which installed client launched it? | Which task is it doing? |
| Process ID | Which local process currently owns the connection? | Can a human recognize it across restarts? |
| Session ID | Which bounded run made this request? | Is the requested action sensible? |
| Task label | What did the user ask this run to do? | Is the label truthful or sufficient evidence? |

Do not hide the first two rows just because you add the fourth. A reviewer needs the process authority to spot an unexpected client. They need the session ID to separate concurrent expected clients. They need the task label to connect a technical identifier to a mental model of the work.

The practical rule is simple: show process identity as provenance, and show session identity as the unit being authorized.

On macOS, a code-signing identifier alone is especially weak as an approval label. Apple explicitly notes that signing identifiers can be claimed by multiple signers and recommends pairing an identifier check with validation category and, for non-Apple code, a team identifier. A card that displays only a friendly bundle name or only a claimed identifier gives the reviewer less evidence than the platform can provide.

Do not expose raw requirement language as the main label. It is precise, but most reviewers cannot read it quickly enough to make a decision. Render the signing authority in plain language, keep the technical requirement available in details, and pair it with a recognizable executable name. Then place the session identifier where the eye can find it without expanding anything.

## Give every run a session identity that survives the queue

A session ID must begin before the first protected call, remain stable for the life of the run, and disappear from the authorization set when that run ends. Anything weaker creates ambiguity under load.

Generate a high-entropy opaque ID at connection or process registration time. Store the full value in the audit record, but render a short, unambiguous prefix in the prompt. The display form should be long enough that two active sessions are unlikely to share it, and the UI must make collisions impossible to miss if they occur. Do not derive the ID from the current time, a queue position, a repository name, or the process ID alone.

A useful internal record looks like this:

```json
{
  "session_id": "ses_7TQ4N8M2KDXP6R9V",
  "display_id": "7TQ4N8M2",
  "process": {
    "pid": 84172,
    "executable": "/usr/local/bin/agent-cli",
    "signing_authority": "Example Engineering (Team ABCD1234)"
  },
  "task": {
    "label": "Prepare the staging release notes",
    "workspace": "/Users/maya/work/app"
  },
  "started_at": "2026-07-22T16:42:11Z"
}
```

The task label should come from a human-visible instruction or a deliberate session title, not from a model-generated sentence that changes with every tool call. A model can propose the label, but the system should freeze it when the session starts. If a session shifts from “prepare release notes” to “rotate the production webhook credential,” the reviewer deserves an explicit task change or a new session. Quietly rewriting the label makes the history harder to read and gives the run a way to borrow legitimacy from an earlier, safer task.

Use a lifecycle with hard edges:

- Create the session before any credentialed action.
- Attach every request, approval, denial, cancellation, and result to that session.
- Close the session when the initiating process exits or loses its valid connection.
- Revoke the session immediately when the reviewer chooses revoke.
- Reject pending approvals after close or revoke, even if a card remains visible for a moment.

The last point prevents a subtle but common race. A reviewer sees a card from run A. Run A exits. A new run B begins, asks for a similar action, and the old card still accepts input. If the approval service attaches that click to “the latest request for this credential,” the reviewer has approved B while looking at A. Bind the card to one immutable request ID and one session ID. When either becomes invalid, the only safe button is dismiss.

Avoid labels such as “Session 1,” “Agent run,” or “Current task.” They work until the second run begins. A label can be friendly, but the interface needs an identifier that remains meaningful after a process restarts, a laptop sleeps, or an incident review happens days later.

## Put the right facts in the first two lines

The first two lines of an approval prompt should let a reviewer distinguish this request from every other pending request without opening details. Put the requested action first, then state the initiating process and session directly below it.

A workable card has this shape:

```text
Allow POST to api.example.internal/v1/releases?
agent-cli, signed by Example Engineering, session 7TQ4N8M2
Task: Prepare the staging release notes

Creates a release record named "2026.07.22-rc3"
Credential: release-service-write
[Review request]                         [Deny] [Allow]
```

The action line says what will happen and where. The attribution line says who asked. The task line says what work the reviewer should associate with that request. The preview gives enough consequence to make an initial judgment. That ordering is intentional.

Do not lead with “Approval requested” or “Agent wants to use a secret.” Neither phrase helps a reviewer sort a busy queue. Do not lead with the credential name either. Credentials are implementation details. A reviewer usually knows whether a request belongs to the right release run before they know whether `release-service-write` is the appropriate stored key.

For SSH, the equivalent prompt should name the remote host and the command class, not merely “SSH access.” For example:

```text
Allow SSH command on build-staging-03.example.internal?
agent-cli, signed by Example Engineering, session 7TQ4N8M2
Task: Verify the staging migration

Runs: /usr/local/bin/check-migration --database app_staging
Credential: deploy-ssh
[Review command]                         [Deny] [Allow]
```

If the system cannot produce a safe, readable preview, it should say so. “Command arguments unavailable” is better than inventing a friendly sentence that hides shell expansion, an indirect script, or uninspected input. The reviewer can then open the exact command or deny the request.

A good prompt does not ask the reviewer to infer the initiating run from a timestamp. Time belongs in the details and the activity record. Two agents can issue requests within the same second, and people do not reliably use timestamps to match a card to a terminal. Use time as supporting evidence, never as the label that carries attribution.

Also avoid putting the session ID in a faint footer. The identifier is not diagnostic debris. In a parallel queue, it is the field that stops a reviewer from treating two nearly identical cards as interchangeable.

## A queue needs grouping without false merging

Parallel work creates a visual sorting problem. Grouping cards by session can help, but grouping becomes dangerous when the UI collapses meaningful differences between requests.

A queue should let the reviewer see all pending actions from one session together while preserving one approval decision per action when the actions have different effects. A release assistant that needs three read-only API calls may reasonably show a compact group. A session that wants to read an issue, write a deployment record, and run a remote migration should show three separate decisions because the consequences differ.

The wrong pattern is a global banner that says:

```text
agent-cli requests access to 5 services
[Allow all]
```

That button asks the reviewer to approve a collection before they have attributed each item to a run or examined its effects. It also rewards queue fatigue. The more a reviewer sees that banner, the more likely they are to develop a habit of clearing it to get back to work.

A better queue uses session headers as context, not as broad authorization:

```text
Session 7TQ4N8M2 · agent-cli · Prepare the staging release notes
2 pending requests

  GET api.example.internal/v1/builds/rc3             [Allow]
  POST api.example.internal/v1/releases              [Review]

Session C5J1W6PA · agent-cli · Investigate test failure #1842
1 pending request

  SSH build-staging-03.example.internal              [Review]
```

The reviewer can visually scan by run, but each row still makes a separate claim. If the system supports a per-session authorization state, show it in the header with clear scope: “This session is authorized until it exits.” Do not make that state look like approval of every per-call action. Per-session authorization permits the run to ask for actions; it does not turn unexpected actions into expected ones.

Ordering also affects errors. A purely chronological queue can interleave requests from five runs so thoroughly that a reviewer cannot hold the context of any one run. A purely session-grouped queue can bury an urgent request behind a noisy session. Give the reviewer both views: a default grouped view for attribution and a time-sorted view for incident response. Keep the session badge visible in either view.

Do not merge cards merely because they share a destination and credential. Two separate runs writing to the same endpoint create exactly the ambiguity the queue must solve. Similarity is a reason to make the run identity more prominent, not a reason to combine the approvals.

## Approval must bind to the exact request the reviewer inspected

A prompt can identify the correct run and still authorize the wrong request if the approval applies to a mutable template rather than an immutable action. The reviewer must approve a specific request representation, not a future request that happens to reuse the same route, command, or credential.

Create a canonical action record before the card appears. For HTTP, include at least the method, normalized origin and path, selected headers by name, credential alias, and a digest of the body. For SSH, include the normalized host identity, requested account, port, command bytes, and credential alias. Store the full protected material inside the trusted component; the agent should not receive it just because the reviewer needs a prompt.

Then bind approval to the canonical record:

```json
{
  "approval_id": "apr_K9H2D7LQ",
  "session_id": "ses_7TQ4N8M2KDXP6R9V",
  "action_digest": "sha256:2a13c4e0...",
  "expires_at": "2026-07-22T16:47:11Z",
  "decision": "allow_once"
}
```

At execution time, recompute the digest from the action that will actually leave the machine. If it differs, reject the approval and create a new card. Do not quietly treat a changed request as “close enough.” A changed URL parameter can redirect a payment, a changed header can alter account scope, and a changed shell argument can turn an inspection command into a write.

This is where many otherwise thoughtful systems fail. They show a reviewable summary, approve “use credential X for endpoint Y,” then let the agent make a second request under that broad grant. The reviewer has not reviewed the second request. The system has converted one explicit decision into an invisible policy rule.

Use short expiry for a pending approval. The purpose is not to punish a reviewer who takes a coffee break. It is to stop an old decision from being applied after the surrounding context has changed. If the agent still needs the action later, it can make a fresh request with the same process and session details. The reviewer will see whether it still belongs.

NIST material on human involvement in automated decisions warns about automation bias and low-friction approval loops that normalize alert fatigue. The practical implication here is blunt: making approvals easy to click does not make them meaningful. The interface must make the decision small enough to inspect and specific enough to be accountable.

## The detail view should settle disputes, not create them

A compact approval card cannot carry every byte of a request. It does need a detail view that answers the questions a careful reviewer asks when the action is unfamiliar, costly, or suspicious.

For an HTTP action, show the complete method and URL, redacted headers by default, body fields with clear treatment of secrets, the credential alias, the initiating process identity, the session ID, the task label, and the request creation time. If the body is binary or too large to render, show its size, content type, and digest. Do not pretend a one-line prose summary is an adequate substitute.

For SSH, show the host, user, port, host verification state where available, command exactly as it will execute, working directory if relevant, and any environment values that affect behavior. A command preview must preserve quoting and argument boundaries. Rendering an array as a loosely joined shell string can make a safe command look unsafe or, worse, make an unsafe command look harmless.

There is a sharp distinction between redaction for safety and omission for convenience. Redact a bearer token, private key, password, or sensitive body value from a prompt. Do not redact the path, target account, remote host, command, or changed field merely because the details make the card longer. Those are often the facts that tell a reviewer whether the request belongs to this run.

Use a stable request fingerprint in the detail view. A reviewer who messages a teammate should be able to say, “I denied request `apr_K9H2D7LQ` from session `7TQ4N8M2`,” and the teammate should find exactly one record. Never make people describe an event as “the second deployment prompt around 4:40.” That language works until an incident requires precision.

The review surface should also state what approval will and will not authorize. For example:

```text
Allow once
This decision authorizes only this POST request with digest 2a13c4e0…
It does not authorize later calls from session 7TQ4N8M2.
```

That sentence earns its space. It stops a reviewer from assuming a broad grant and stops an implementer from later widening the scope behind the same button label.

## Cancellation and revocation need visible consequences

A reviewer will eventually approve the wrong card or notice that a run has gone off course. Recovery must be fast, visible, and tied to the same identifiers used during approval.

The UI needs separate actions for denying one request and revoking one session. Deny means “do not perform this action.” Revoke means “this running process has lost its authorization, cancel its pending approvals, and deny later actions.” These actions have different consequences, so they must not share one ambiguous “Stop” button.

When a reviewer revokes a session, show a confirmation that names the process, session ID, and task:

```text
Revoke session 7TQ4N8M2?
agent-cli is running “Prepare the staging release notes.”
Pending requests from this session will be cancelled. New protected actions
from this process will be denied until it starts a new session.
[Keep session] [Revoke session]
```

If a request is already executing, report that honestly. A revocation can prevent future privileged actions, but it may not undo an HTTP request that a remote service has already accepted or halt a remote command that has already started. The activity record should show whether the action was pending, sent, completed, failed, or cancelled. Do not write “revoked” in a way that implies the external effect vanished.

This is another reason to show the session ID in every card. During a stressful incident, the reviewer needs a direct bridge from a suspicious prompt to the control that stops that run. If the queue says only “agent-cli,” revocation risks killing the useful run while leaving the bad one active.

A session journal and an activity journal should agree on identifiers. One record tells you that process session `7TQ4N8M2` began, received authorization, and was revoked. The other tells you which individual action it attempted before and after each state change. If those records use unrelated labels, responders waste time reconstructing joins that the product should have made explicit.

## Test for the wrong-run approval before users do it

You can test this failure without a red team or a production outage. Run two instances of the same agent client against the same workspace and ask them to make similar protected calls. The test is successful only if a reviewer can identify each request correctly while looking at a busy queue.

Use this exercise:

1. Start run A with the task label “Inspect failed staging build.”
2. Start run B with the task label “Publish staging release record.”
3. Have both runs request the same credential within a few seconds.
4. Make their first requests similar, such as two calls to the same API origin.
5. Change one action so that approving it for the other run would have a visible, undesirable effect in a test environment.

Then ask someone who did not build the UI to approve only run B’s request. Do not tell them which card is B by queue position. Watch what they use. If they rely on timestamps, card order, or their memory of which terminal they opened first, the interface has failed. If they can point to the process authority, session ID, task, destination, and action without opening a separate screen, you have a usable starting point.

Test stale approval behavior too. Create a card for run A, end run A, start run B, and click the old card. The system must reject the click and explain that the request is no longer active. Repeat the test after changing a request body or SSH argument after review. The system must require a new approval rather than applying the old one to the changed action.

Test names that collide. Two tasks can both be called “Fix CI.” Two repositories can have the same directory name. Two branches can share a release number. Friendly text helps people, but the full design must still work when friendly text is ambiguous.

Sallyport’s per-session authorization gives a natural place to establish this boundary: the first call from a new agent process identifies that run for approval, while per-call keys can require a separate approval for each use. Keep the session label and process authority visible when those controls appear, or parallel runs will collapse back into one indistinct “agent.”

## The audit trail must reproduce the approval decision

An audit trail is useful only when it can answer, after the fact, what the reviewer saw and what the system executed. Recording “user approved API access” is not enough. It leaves the central dispute unresolved: access for which run, for which action, and with what displayed context?

For each decision, record the immutable identifiers and presentation facts:

```json
{
  "event": "approval_granted",
  "approval_id": "apr_K9H2D7LQ",
  "session_id": "ses_7TQ4N8M2KDXP6R9V",
  "process_identity": "agent-cli / Example Engineering (Team ABCD1234)",
  "task_label": "Prepare the staging release notes",
  "action_digest": "sha256:2a13c4e0...",
  "displayed_action": "POST api.example.internal/v1/releases",
  "decision_scope": "once",
  "recorded_at": "2026-07-22T16:44:03Z"
}
```

Store the canonical action record separately or alongside it, with appropriate encryption and access controls. The audit event should tell an investigator what the card said; the canonical record should prove what bytes and destination the trusted component used. If the two differ, treat that as a security defect, not a logging detail.

A hash-chained log helps detect rewriting of history, but it does not repair vague event content. You can cryptographically prove that “approval granted” was not altered and still be unable to tell which request received it. Integrity and attribution solve different problems. You need both.

Sallyport projects both session-level and individual-call records from one encrypted, hash-chained audit log, and its `sp audit verify` command verifies the chain offline over ciphertext. That is useful because reviewers can retain evidence that the log has not been rewritten without exposing the stored secrets to the verification step.

The final design test is simple. Pick any completed action and work backward: can an investigator identify the process, the specific session, the task label shown at the time, the exact request reviewed, the reviewer’s decision scope, and the outcome? Then work forward from a session: can they see every pending, denied, approved, cancelled, and executed action in order? If either direction requires guesswork, the approval queue still invites the wrong-run mistake.

Parallel agents will make approval queues normal. The answer is not to bury reviewers under more cards or give them one broad “allow all” control. Give each running process a durable session identity, put it beside the action where people actually look, and bind every click to the request the reviewer inspected. That is how an approval means something when several agents are active at once.
