8 min read

Can an approval audit trail prove who approved an agent action?

Build an approval audit trail that links click or Touch ID decisions to the user context, agent session, credential use, and executed call.

Can an approval audit trail prove who approved an agent action?

An approval record that says only "approved" does not answer the question that matters after an agent touches production: who allowed which action, under what authority, and what happened next? It records a comforting moment in a user interface, then leaves an investigator to infer the rest.

An approval audit trail has to preserve the chain from an agent process to a human decision, from that decision to a credential use, and from the credential use to a completed HTTP request or SSH command. If those are separate records without durable links, a reviewer can tell a plausible story. They cannot prove one.

This distinction becomes painful when the action succeeded but nobody remembers approving it, when an agent restarted halfway through a task, or when someone asks whether a click and a Touch ID confirmation carried the same meaning. They do not. Treating them as interchangeable produces a log that looks complete until the first serious review.

An approval event must answer more than "yes"

A useful approval event says what the human approved, why the system asked, how the decision was made, and where the grant stops. The visible button click is only one field in that event.

Capture these facts when the decision occurs:

  • A unique approval identifier and an event timestamp with an offset.
  • The approval method, such as click or touch_id.
  • The local account or other known approver identity, plus the evidence used to make that attribution.
  • The scope of the grant: a session or one credential use for one call.
  • The request that caused the prompt, identified by a stable request or call identifier.

Do not write user=alex merely because a machine has an account named Alex. That may be the best available attribution, and it is still worth recording, but call it what it is: local account context. If a biometric prompt succeeded, record that a biometric enrolled on that device authorized the event. Those statements are stronger than "someone named Alex approved it" because they do not pretend the audit log knows more than it does.

NIST SP 800-171 Rev. 3 gives a sensible starting point for audit content: timestamps, source and destination addresses, user or process identifiers, event descriptions, applicable access controls, and outcomes. It also notes that detailed records may include privileged commands and individual identities behind shared accounts. That is a good floor for agent actions, not a finished design. An agent approval flow needs the relationships between the decision, the credential, and the call as first-class data.

The easy mistake is to record an approval as an attribute on the final action, such as approved=true. That collapses an event into a label. You lose the time between the request and the decision, the source of the decision, the scope of the consent, and any later revoke. It also makes it impossible to distinguish a user approval from a default allow, a cached grant, or an automation rule.

A decision should have its own record even when the answer is no. A rejected prompt can explain why an agent failed to deploy. An expired prompt can explain why the agent retried. A vault lock can explain why the system refused to make a network call at all. These are materially different events, and a later reviewer should not have to deduce the difference from an absent success record.

Five identities keep the timeline honest

A complete timeline needs five separate identities. Combining them saves columns in a table and destroys meaning in an investigation.

The first is the agent process. Record a process or run identifier, the executable or code-signing authority that launched it, its start time, and its end time. The human should be able to answer, "Which running program asked for this?" A project name or a chat transcript is not enough. Two copies of the same coding agent can run at once, and one may be benign while the other is pointed at a different repository.

The second is the session. A session is the bounded relationship between one agent process and the gateway. It must have its own identifier because a process may make many calls, and because session authorization often applies across several calls. When the process exits, the session should end. If a new process starts later, it should create a new session even if it has the same executable, the same local account, and the same task description.

The third is the approver context. This includes the device account, any authenticated application user, and the approval method. Do not make the approver field carry facts it cannot support. local_account=maya, method=touch_id, and device_id=... are clear. human=maya claims more. In some environments that claim is reasonable; in others, a shared workstation or an unlocked desktop breaks it immediately.

The fourth is the credential reference. This identifies the authority the gateway used, not the secret itself. A stable opaque credential ID, a human-readable label, the channel, and the credential type are normally enough for activity review. A bearer token is not an audit field. An SSH private key fingerprint can itself become sensitive context, so decide whether your investigators need it before spreading it through ordinary logs.

The fifth is the executed operation. For HTTP, this is the resolved destination identity, request method, normalized path, selected nonsecret request facts, response status, and timing. For SSH, it is the host identity, remote account, command or a digest of the approved command, exit status, and timing. The event must say what ran, not merely what was requested.

These identities form a graph, not a flat row:

agent_process
  -> session
    -> approval_decision
      -> credential_use
        -> executed_call

A flat activity screen can present that graph as one line when the user needs speed. Keep the underlying links anyway. The display is for people scanning a day of work. The identifiers are for the person who has to explain one call six weeks later.

A click and Touch ID are different evidence

A click records interaction with an approval control in the current user interface. Touch ID records a successful biometric authorization through the operating system, in addition to the interaction that initiated it. Both can authorize an action. They should not share a vague value such as approved_manually.

Use an explicit method field with a controlled set of values. For example:

{
  "approval_id": "apr_01J8K4VY5Q",
  "occurred_at": "2026-07-22T14:18:06.184Z",
  "decision": "approved",
  "method": "touch_id",
  "approver": {
    "local_account": "maya",
    "identity_assurance": "device_account_and_biometric"
  },
  "scope": "credential_use",
  "session_id": "ses_01J8K4TE0M",
  "requested_call_id": "call_01J8K4VPM2"
}

The exact field names are not sacred. The separation is. method tells you how the approval completed. identity_assurance tells you what the system can responsibly say about the person. scope tells you what the decision authorized. requested_call_id ties the approval to a request that existed before the human saw a prompt.

A click can be the right choice for low-friction confirmation, especially when a person is already watching an agent work. Touch ID adds a stronger local confirmation step for a sensitive operation, but it does not magically supply a corporate identity, a reason for the decision, or an endorsement of every later call. If a team needs a named employee approval with an external identity provider, it needs a flow that records that identity provider's assertion. Do not silently borrow that assurance from a local biometric event.

The converse error is just as bad: treating Touch ID as decorative. If an action required biometric approval and the log reduces that event to approved=true, the record cannot show that the stricter control actually ran. The review loses evidence that could separate a deliberate confirmation from an accidental click on a broad session prompt.

Record unsuccessful biometric attempts carefully. An audit trail usually needs to know that the requested action did not receive approval, but it rarely needs every operating-system level authentication failure. A useful event is decision=denied_or_cancelled, method=touch_id, and a reason such as user_cancelled where the platform makes that distinction available. Do not turn an action gateway into a biometric telemetry collector.

Session authorization grants a bounded agent process permission to operate after a human reviews the process identity. Per-call approval grants consent for one credential use and one operation. Calling both "approval" without recording scope makes the later timeline misleading.

Consider an agent process that starts at 09:00. The gateway shows an authorization card that identifies the process's code-signing authority. A developer clicks approve. At 09:20, the agent makes an HTTP request with a credential whose settings do not require per-call confirmation. That call may be allowed because the session remains authorized. The correct timeline shows two separate facts:

  1. At 09:00, the developer approved session ses_... for the lifetime of that process.
  2. At 09:20, that session used credential cred_... to make call call_....

It should not create a fictional 09:20 user approval. The developer did not see and approve that exact call. The earlier grant covered it.

Now change one setting: the credential requires approval on every use. At 09:20, the gateway asks again and the developer approves with Touch ID. The new event must point to call_..., state scope=credential_use, and include method=touch_id. The session approval remains relevant because it explains why the agent could reach the credential request. It does not replace the second decision.

This distinction matters most when an agent makes a surprising call late in a session. If the log says "approved" beside it, the reviewer needs to know whether that means a human approved the executable thirty minutes earlier or approved this exact credential use three seconds earlier. Those facts have very different implications for prompt design, credential settings, and incident response.

Do not solve the ambiguity by making every call require approval. That recommendation is popular because it sounds safe and it produces a satisfying number of records. It also trains people to approve repetitive prompts without reading them, then leaves them unable to tell the one exceptional call from the routine ones. Put per-call approval on credentials whose use needs fresh human confirmation. Keep session authorization on by default to bind the agent process to an accountable approval boundary.

A revoke also needs scope. If an operator revokes a session, write the revoke event against the session and record the effective time. Do not overwrite the old approval. If a user disables or removes a credential, record that change separately. An audit timeline should show why a later call was denied without rewriting history to make the former grant disappear.

Credential records must identify authority without exposing it

Require fresh confirmation
Per-call keys require a click or Touch ID for each individual credential use.

The credential-use record is where many teams make a dangerous trade: they add secret material to make an investigation easier. That is a bad bargain. Logs are copied, indexed, exported, and retained longer than the process that generated them. A secret in an activity journal turns every log reader into a credential holder.

Give every stored credential an opaque immutable identifier, such as cred_01J8K.... Pair it with a label that helps a human recognize purpose, such as payments-readonly or staging-deploy. Record the channel and injection mode, for example http_bearer, http_custom_header, or ssh_key. That gives an investigator enough context to ask the right question without copying the key into the record.

A practical credential-use event can look like this:

{
  "credential_use_id": "use_01J8K4WHD7",
  "occurred_at": "2026-07-22T14:18:06.221Z",
  "credential": {
    "id": "cred_01J7ZB7F8P",
    "label": "inventory-production",
    "channel": "http",
    "injection": "bearer"
  },
  "session_id": "ses_01J8K4TE0M",
  "approval_id": "apr_01J8K4VY5Q",
  "call_id": "call_01J8K4VPM2",
  "secret_exposed_to_agent": false
}

The secret_exposed_to_agent field may seem redundant when the gateway design guarantees it. Keep it if the timeline can include multiple execution paths or migrations. It makes the security property inspectable in the same record as the action. If every supported path has the same guarantee, the field can be implicit in the system design and documented once.

Separate credential selection from credential use. An agent can request a credential by label, but no credential has been used until the gateway begins the outbound operation. This matters for denials. If Touch ID is cancelled before the request leaves the machine, write an attempted call and a denied approval event. Do not write a successful credential-use event. Otherwise your audit count will claim that a production credential was used when it was not.

For SSH, avoid treating a host alias as the complete destination identity. prod-db is readable, but aliases can change. Record the configured target and the host identity evidence your connection flow verifies. If the agent asked for prod-db but the resolved target differed, that difference belongs in the execution record. It is exactly the sort of detail that matters after a bad deployment.

The executed call is the evidence that the action happened

Approval proves consent. Credential selection proves intended authority. Only an execution record tells you whether the gateway attempted the outside-world operation and what result came back.

For HTTP calls, record the operation in a normalized form. Keep the request method, destination origin or service identity, canonical path, selected query-field names when useful, response status, start and finish timestamps, and a result reference. Decide deliberately which request and response fields are safe to retain. Authorization headers, cookies, token-shaped values, full request bodies, and raw response bodies should not land in a general activity timeline.

A request digest can help prove that an approved payload and an executed payload matched, but only if you define its input exactly. Hashing a JSON body without canonicalizing field order creates false mismatches. Hashing a body that contains a small predictable value can still help an attacker confirm guesses. Use a digest for integrity correlation when the payload is already protected elsewhere, not as a universal substitute for content handling.

For SSH, log the remote account, destination identity, command representation, exit status, and start and end times. A full command line can contain secrets in environment assignments, temporary URLs, or arguments. One sensible compromise is to store a safe rendered command for routine review and a protected full representation or digest for investigation. Do not claim a digest is readable evidence. It tells you that two values match; it does not tell a reviewer what the command did.

RFC 5424 separates a timestamp and message identity from structured data because parsers need dependable fields rather than prose they have to guess at. Its timestamp format also carries a time offset and permits fractional seconds. You do not need to emit syslog, but the design lesson holds: keep event types and correlation fields structured, then reserve human text for explanation.

Use distinct event types. call.requested, call.dispatched, call.completed, and call.failed_before_dispatch say more than an overloaded call event with a status field that changes meaning. The extra records let you answer whether a network timeout happened after credential injection, whether a local validation blocked the request first, and whether the remote service returned a response.

Time alone cannot establish order across machines. Use UTC timestamps with offsets and retain a monotonic sequence number inside each local audit log. If the remote API returns its own request ID, store it as a remote correlation value. That gives an investigator a way to compare the local timeline with a vendor's records without pretending the clocks match perfectly.

A broken timeline hides in ordinary success logs

Make vault state matter
When the vault is locked, every action is denied until the vault gate is opened.

Picture a deployment agent that receives session approval at 10:02. It reads a repository, prepares a release, then calls a production deployment endpoint at 10:17. The endpoint accepts the request. At 10:18, the developer notices the wrong environment was selected.

A weak log contains this:

10:02 approved agent
10:17 deployment API call succeeded

That log answers almost nothing. Was the 10:17 call covered by the 10:02 approval? Did the credential demand a second prompt? Which process made the call? Did the agent use the intended deployment credential or a broader token? Did the system send the request to production, or did a redirect or configuration error move it there? Did the human click approve, use Touch ID, or never see an action-specific prompt?

A useful timeline instead reads like this:

10:02:11  session.opened       ses_71  process=proc_44 signer=known_authority
10:02:14  approval.approved    apr_02  method=click scope=session session=ses_71 account=maya
10:17:03  call.requested       call_88 POST deploy.example/release target=production session=ses_71
10:17:04  credential.selected  use_53  credential=cred_prod_deploy call=call_88
10:17:04  call.dispatched      call_88 destination=deploy.example
10:17:06  call.completed       call_88 status=202 remote_request=req_914

This record may establish that the agent had valid session authorization but did not receive a call-specific approval. That is not proof that the deployment was desired. It is evidence about how the control operated. The team can then decide whether the production credential should require per-call approval, whether the prompt needs to display the target environment more plainly, or whether the agent should not have access to that credential at all.

Now add per-call confirmation. The correct additional entry is not another generic approved line. It should state the call it approved and its scope:

10:17:04  approval.approved    apr_03  method=touch_id scope=credential_use
          session=ses_71 call=call_88 credential=cred_prod_deploy account=maya

If the agent retries after a timeout, give the retry a new call ID. It may reuse an existing session grant, but a per-call credential rule should create a new approval requirement for the retry. Recording the retry as though it were the original call produces a false impression that one confirmation covered two external actions.

Audit integrity needs a separate claim

Put SSH behind approval
SSH commands run through the bundled stateless sp-ssh helper instead of exposing private keys.

An audit log can be complete enough to explain a sequence and still be easy to alter. It can be append-only in intention and still allow an administrator or malware with local access to remove the inconvenient lines. Treat content and integrity as separate properties.

A hash-chained journal links each record to the preceding record through a cryptographic digest. Change an old record and the later chain no longer verifies. This is useful because an exported activity screen can be challenged against the underlying journal rather than accepted on trust. It does not prove that the original system recorded every event, that a compromised writer did not create false records, or that a valid approval was a good decision. Those are different claims that require different controls.

Verify at the boundary where evidence leaves the system. An investigator should be able to take the encrypted record stream, run an offline integrity check, and learn whether the sequence is intact without exposing credentials just to validate a chain. The verification output should identify the checked range, the chain status, and the first failing sequence if verification fails.

Sallyport projects its Sessions and Activity journals from one write-blind encrypted, hash-chained audit log, and sp audit verify can verify the chain offline over ciphertext without a vault key. That arrangement matters because the session decision and the individual operation remain views of the same evidence, not independently editable stories.

Do not use integrity verification as a reason to keep excessive data forever. Retention, access control, and redaction still matter. A perfectly preserved log full of secrets is an incident waiting for a convenient search query. Define who can inspect raw records, who can export them, how long records remain available, and which fields are safe in routine views.

Build the timeline around joins, then test the ugly cases

A schema review should start with a blunt question: can an investigator start with any executed action and walk backward to the approval without guessing? If not, add the missing identifier before you polish the activity screen.

Run a small test matrix against the implementation. You do not need a large simulation. You need cases that expose scope and ordering errors:

  • Start a new agent process, approve its session with a click, and make a low-risk call.
  • Use a credential that requires per-call approval, authorize it with Touch ID, and confirm that the call points to that approval.
  • Cancel the biometric prompt and confirm that no successful credential-use record appears.
  • End the agent process, start it again, and verify that the new process cannot inherit the old session grant.
  • Force a remote failure after dispatch and check that the timeline distinguishes dispatch from completion.

Inspect the result from both directions. Begin with the approval and list every action that relied on it. Then begin with the executed call and trace the process, session, decision, and credential. The first view finds grants that were broader or longer-lived than expected. The second finds calls with broken or ambiguous evidence.

Keep the language in the interface as strict as the data model. "Session approved by click" is clear. "Production deployment credential approved with Touch ID for this call" is clear. "Approved" is a decorative status word that asks the reader to invent the most important details.

The first time someone asks who approved an agent action, do not hand them a screenshot with a green badge. Hand them a timeline that shows the process, the decision method, the scope, the credential authority, the exact call, and the result. Anything less may be convenient during a demo. It will not hold up when the action matters.

FAQ

What should an AI agent approval log include?

It should show the request, the decision, the identity evidence available for the approver, the credential selected, the actual operation, the result, and the links between those records. A timestamp alone cannot establish that sequence. If the entries cannot be joined by stable identifiers, you have several logs, not one audit trail.

Does Touch ID prove which person approved an action?

Only if the system has an explicit identity binding that can support that claim. Touch ID normally proves that an enrolled biometric authorized use of the device, while the local account and device context identify the session. Record those facts separately rather than turning a biometric event into an unsupported personal attribution.

Is session approval the same as approving each agent call?

No. Session approval says a particular agent process may continue using the approved session until it exits. A per call approval says the human confirmed this exact credential use and operation. They have different scope, different expiry, and different audit meaning.

How do you audit credential use without logging secrets?

Record a stable credential reference, its type, the channel, the policy state that required approval, and whether the secret was injected by the gateway. Do not log API keys, private keys, authorization headers, or even redacted values that preserve enough structure to help an attacker.

What is the right way to log an executed HTTP or SSH call?

Record the method, destination identity, approved request summary, response status, execution timestamps, and a result reference. For SSH, capture the host identity, account, command or approved command digest, exit status, and relevant output reference. Keep sensitive request bodies and output out of the broad activity view unless an investigation needs them.

Should denied agent actions appear in the audit trail?

A denied request still records attempted access and often explains why an agent did not complete work. Log the requested operation, the session and process that requested it, the denial reason, and whether a human rejected or never received the prompt. Do not create a credential-use record when no credential was actually used.

How should audit records handle a restarted agent?

Give each agent process run a new session identifier, even if the executable, user, and project are the same. A session approval belongs to one live process context and should end when that process exits. Reusing a broad identifier turns a bounded grant into a vague permission history.

Does a hash chained log make an audit trail tamper proof?

No. Hash chaining makes changes detectable when a verifier has the required log sequence and trust context, but it does not prove that every possible event was recorded or that an approved action was wise. It gives you evidence of record continuity and alteration, which is a narrower and still useful claim.

How do I investigate a suspicious agent action?

Start from the executed call, then follow its credential-use identifier to the approval event and its session identifier to the process record. Check the timestamps, the approval scope, and whether the actual call stayed inside the approved summary. If any link is missing, state the gap instead of reconstructing certainty from nearby entries.

How can I test whether my approval records are complete?

The fastest useful test is to make one low-risk HTTP request with a fresh agent process, approve it once with a click, then repeat it after enabling per call approval and approve with Touch ID. Export or inspect the timeline and confirm that it distinguishes session authorization from call authorization, names the method, and links both cases to the executed request.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov