# Write Blind Audit Logs for AI Agents That Agents Cannot Edit

An AI agent can write a polished explanation of its work after it has made a bad call. That explanation is not evidence. If the agent can see, select, revise, or avoid the records that reviewers use to judge it, the audit trail becomes another output surface the agent can optimize.

Write blind audit logs solve a narrow but important problem: the process that requests an external action should not control the record of that action. The action system records what it observed, returns the action result the agent needs, and keeps the review record outside the agent's reach. This does not make an agent honest. It makes a dishonest, confused, or compromised agent less able to curate the story after the fact.

That distinction matters most when an agent can deploy code, call paid APIs, change cloud settings, send mail, or run SSH commands. In those cases, a chat transcript is weak evidence. A transcript records claims and selected tool output. A record generated at the enforcement point can record the request, authorization, execution, and result even when the agent would prefer silence.

## The agent must not control both action and evidence

An agent must have fewer powers over evidence than it has over the task. Giving it a file called `audit.jsonl` and telling it to append records creates an activity diary, not an audit log. It can forget a line, redact an inconvenient argument, write a success before the call fails, or create a second copy that a reviewer never sees.

The problem appears even with agents that follow instructions. An agent often decides which tools to invoke, which exception to catch, and what context to retain. A prompt injection can tell it to avoid logging a request. A bug can send a request through an uninstrumented library. A malicious dependency can write directly to the network. If the same process owns the action and the record, every one of those failures can leave a tidy but incomplete trail.

Separate these capabilities explicitly:

- The agent asks for an action and receives a bounded result.
- The executor holds the credential, sends the request, and creates the event record.
- The journal accepts records through a path the agent cannot read or modify.
- A reviewer reads and verifies records through a separate interface.

This is a capability boundary, not a promise about behavior. The agent should not have file access to the journal, a query API for its entries, credentials that can rewrite the journal, or a way to set the event timestamp. If it needs to explain its reasoning, store that explanation in a separate run artifact and label it as agent supplied material. Do not mix it with the execution record.

Teams often blur intent and evidence. Intent is what the agent says it meant to do: “restart the staging service.” Evidence is what an execution component observed: the agent requested a specific command at a given time, an approval decision occurred, the executor connected to a named host reference, and the command exited with a particular status. Both are useful. They answer different questions, and they need different trust levels.

## Write blind means separate capabilities, not delayed viewing

Write blindness means the actor can cause a record to exist without gaining access to the stored record or its future review context. It does not mean the agent receives no feedback. An SSH task still needs stdout, stderr, and an exit status. An HTTP task still needs a response status and body or a bounded extraction from it. Removing results makes an agent useless without making the evidence stronger.

The boundary has four practical parts. First, the agent uses a request interface, not a credential. Second, the executor creates a record from its own observations rather than trusting a JSON blob supplied by the agent. Third, the agent cannot list, fetch, modify, or delete records. Fourth, a reviewer can inspect the record later without depending on the agent's memory or chat history.

A delayed-read design does not meet that standard. Suppose an agent writes events into a database and receives a promise that it cannot query the table until the session ends. It still knows the schema. It can tailor argument strings, choose omissions, try transaction failures, and use differences in error timing to learn whether a record landed. It may even hold database credentials that another code path can abuse.

Write blindness is stronger when the event path has no response that exposes journal state. The executor can acknowledge the requested action, but it should not return event IDs, sequence numbers, reviewer annotations, risk labels, or a confirmation that a particular record became permanent. Those details give the actor material to optimize against.

This does not require treating every agent as hostile. It recognizes that an agent can become unsafe for many mundane reasons: an injected instruction, a runaway loop, a tool bug, a stale dependency, or an operator who grants too broad a task. The design should survive more than good intentions.

## An append-only log alone does not solve this

Append-only storage detects some kinds of tampering. It does not prove that the recorder saw every relevant action, nor does it stop an agent from arranging misleading records before storage. Teams frequently buy immutable storage and then leave direct network credentials in the agent's environment. That leaves the central gap untouched.

Consider two deployment paths. In the first, an agent reads an API token from an environment variable and calls the deployment service directly. A separate library attempts to log the request. The agent can bypass the library, fork another process, or use a different HTTP client. The log may remain perfectly append-only while the deployment never appears in it.

In the second, the agent can ask an executor to deploy, but it never receives the token. The executor owns the sole route that can attach the credential. It records the request before transmission and updates the event after the response. The journal can only describe actions that crossed the executor, but the agent has no authorized route around it.

RFC 6962, the Certificate Transparency specification, describes a Merkle hash tree that supports efficient proofs that a leaf belongs to a tree and that a newer tree extends an older one. That is a useful integrity pattern. It does not claim that a tree proves complete capture of real-world events. A deployment request that never entered the tree cannot appear in an inclusion proof.

The same limitation applies to a linear hash chain. A record can include the digest of its predecessor. If someone edits, removes, or reorders stored records, verification fails at the affected point. Yet a clean chain can still cover only a carefully selected subset of actions. Put the recorder in the action path before you celebrate the chain.

There is a second limitation: administrators can also threaten the trail. If an operator can alter the log database and reset the chain root without an external witness, the design mainly protects against casual changes. Offline verification, protected checkpoints, and access separation give reviewers a better basis for trust. None of these measures turn a log into a complete account of everything that happened on a machine.

## Put the recorder on the execution path

The recorder must observe the action where credentials attach and the external request leaves the controlled system. Logging at the agent framework layer is too early. Logging only at a remote service is often too late and may omit authorization context. The execution gateway can see both the request it accepted and the outcome it received.

For HTTP, that gateway should accept a constrained request, select a stored credential, attach the credential itself, send the request, and return an allowed portion of the response. The agent should not submit raw `Authorization` headers. It should refer to a credential by an internal name that the gateway resolves.

A request shape can be small enough to inspect and record:

```json
{
  "action": "http.request",
  "credential_ref": "deploy-api",
  "method": "POST",
  "url": "https://deploy.example.internal/releases",
  "headers": {"content-type": "application/json"},
  "body": {"revision": "a18f3c"}
}
```

The executor should reject caller supplied credential headers for this route. That prevents an agent from smuggling another token into the request while leaving a misleading `credential_ref` in the journal. It should also normalize the URL before it records it, because a raw URL can contain userinfo, encoded path tricks, or an accidental secret in a query parameter.

For SSH, the same rule applies. The agent asks to run a command against a host reference. The executor chooses the configured identity, host verification behavior, and connection method. If the agent can read a private key and invoke its own SSH client, the audit system can no longer claim to cover its remote actions.

Do not confuse a gateway with a general-purpose proxy. A proxy tries to observe broad traffic. An action gateway offers a defined set of actions and holds the authority to perform them. That narrower shape makes it easier to state what the record means: this executor accepted this action under this authorization and obtained this result.

## Record an event before the outside world can answer

A trustworthy record needs a start event before the executor sends the action, plus a completion event after it receives an outcome. If you only write a record after success, a timeout, crash, process kill, or network break can make a serious request disappear from history.

Use one action identifier generated by the executor, not by the agent. The start record should contain the session identifier, actor process identity, action type, normalized target, safe request digest, authorization decision, and a timestamp generated by the recorder. Mark its state as `started`.

After the executor receives a response or failure, it writes a completion record that references the same action identifier. It should include a result category such as `succeeded`, `rejected`, `timed_out`, `transport_error`, or `executor_error`, along with a response digest and bounded metadata. A reviewer can then distinguish “the gateway rejected it” from “the remote system may have received it but no response returned.”

That last distinction has operational consequences. Imagine an agent sends `POST /releases` and loses its connection after the remote service accepts the request. Retrying blindly can produce two releases. Calling the first attempt a failure because the client saw no response is wrong. The journal should show that the executor attempted the call, that the transport failed after transmission began, and that the final remote state remains unknown until another source confirms it.

A minimal pair of records might look like this:

```json
{"seq":481,"action_id":"01HV...","state":"started","session":"run-72","type":"http.request","target":"deploy.example.internal/releases","request_sha256":"...","authorized":true,"previous_sha256":"..."}
{"seq":482,"action_id":"01HV...","state":"completed","result":"transport_error","sent":true,"response_sha256":null,"previous_sha256":"..."}
```

The fields above illustrate structure, not a universal schema. Keep the sequence value and digest under recorder control. The agent can provide a task label, but label it as untrusted input and bound its length. Otherwise, an agent can put secrets, instructions to reviewers, or misleading prose into a field that looks official.

A crash between the two records leaves an unfinished action. Preserve it. An unfinished record is evidence that needs investigation, not clutter to clean up during recovery. On restart, the system can mark it as unresolved if it has a reliable way to do so, but it must not rewrite history into a neat success or failure.

## The schema must preserve intent and result

A journal that stores only “command executed” answers almost nothing during an incident. A journal that stores every byte of every request and response creates a sensitive data dump. The useful middle ground records enough structured context to identify the action and evaluate the authorization, while it minimizes secrets and uncontrolled payloads.

Record the identity of the calling process in terms the operating system can establish. On macOS, code-signing authority provides a better session anchor than a self-reported agent name. Process IDs alone recycle. A display name can lie. Record enough session context that a reviewer can tell whether a new process started the run and whether approval applied to that process.

For an HTTP action, preserve the method, normalized authority and path, selected credential reference, allowed request headers, request body digest, decision outcome, and response status when available. For an SSH action, preserve the host reference, remote account reference if applicable, command or a command digest according to sensitivity, selected identity reference, host verification result, exit status, and output digest.

Do not store raw credentials. Do not let a bearer token sneak into a URL, a custom header, command arguments, or captured output. Redaction after storage is weaker than preventing collection because the secret has already entered backups, replicas, or reviewer exports.

Digests need care. A digest proves equality only against material a reviewer already possesses. It does not tell a human what happened. For a deployment payload, a stored digest plus a repository revision may work well. For a destructive database command, a normalized command representation may be necessary because the exact instruction is the evidence reviewers need.

Keep a separate field for the policy or approval decision that permitted the action. A later reviewer should be able to answer: did the executor allow this because the session had approval, because the operator approved this individual use, or because the vault was open? Do not collapse those into a vague `allowed: true` if the human control model depends on the distinction.

## Review needs separate access and independent verification

Reviewers need more than a searchable table. They need to establish whether the record sequence is internally consistent and whether the system has preserved its boundaries. The person who investigates a run should not need the agent to summarize its own behavior first.

Hash chaining gives a concrete check. Each record includes a digest of the previous stored record, and the recorder computes a digest of the current canonical record. A verifier reads the sequence in order, recomputes each digest, and checks that each predecessor reference matches. Changing an old target, removing an embarrassing failure, or swapping two records breaks the later chain.

Canonicalization matters. If one component hashes JSON text while another parses and reserializes it, harmless whitespace or field ordering can cause a verification failure. Define exact field ordering, character encoding, null handling, timestamp precision, and digest algorithm. Then version the record format. A verifier should know whether it can validate a given version rather than guessing.

Verification should work away from the process that created the records. If checking integrity requires the same running service, a compromised service can lie about a broken chain. Copy the encrypted record stream to a reviewer machine or a protected archive, then run the verifier against that copy.

Sallyport projects its Sessions and Activity journals from one encrypted, hash-chained audit log, and `sp audit verify` can verify that chain offline over ciphertext without a vault key. That separation is the right direction: the evidence check should not require exposing the secrets that enabled the action.

Integrity verification answers a limited question: do these records form the sequence the verifier expects? It does not establish that every server response was truthful, that the target system had no other access path, or that an administrator never replaced the entire journal with an older valid copy. Keep protected checkpoints or send signed roots to an independent retention system if rollback matters in your environment.

## Failure patterns that quietly ruin the trail

The most damaging designs usually look reasonable in a demo. They break under a retry, an outage, or an agent that uses the same authority in an unexpected way.

The first failure is logging from the agent SDK. It is popular because it takes a few lines of code and gives developers a familiar trace view. It cannot provide an execution record when the agent obtains credentials, invokes another program, or calls a destination that the SDK does not wrap. Treat SDK traces as debugging data.

The second is using a single success record. A record written only after a clean response erases ambiguity. Network operations have ambiguous outcomes, and remote commands can alter state before a connection closes. Record start and completion separately, then preserve unfinished starts.

The third is putting secrets in the audit payload. Teams sometimes justify this by saying reviewers need full reproduction. Reviewers rarely need a bearer token to understand an action, and copied secrets turn the journal into a high value target. Store credential references and request digests instead.

The fourth is giving reviewers a mutable search index and treating it as the source of truth. Search indexes drop fields, expire documents, and permit repairs. Keep a durable event stream as the authoritative record. Build a search view from it, and let a reviewer return to the original record when the view looks suspicious.

The fifth is allowing a broad escape hatch. A command such as “run arbitrary shell with the production environment” defeats careful credential handling and event modeling. Sometimes an operator needs exceptional access. Make that access a separately approved action with an explicit record, rather than hiding it inside the normal agent route.

## Human approval and write blindness solve different risks

Approval can stop an unsafe action before it occurs. Write blindness preserves evidence after an action request occurs. Teams need both when an agent operates with consequential credentials.

A session approval is appropriate when a human trusts a specific agent process to perform a bounded period of work. Per action approval is appropriate for a credential or operation that deserves human attention every time. Neither approval type substitutes for an execution record. An approved session can still make an accidental call, and an agent can still misdescribe what it did.

The approval screen should identify the calling process in a form a human can assess. A self-declared label like “release assistant” is weak. An established code-signing authority gives an operator a firmer signal about which program requested power. The approval decision should become part of the event context, but the agent should not control how the system describes that identity.

Keep the vault gate distinct from approval. A locked vault should deny actions even if an earlier session approval exists. That creates a simple emergency stop: lock the authority, and the executor stops using stored credentials. The journal should record denials too. A sequence of denied requests can show an agent looping, probing, or continuing after an operator revoked access.

Do not turn this into a giant rule language unless you genuinely need one. Rules that nobody can inspect during an incident give false comfort. A small set of clear controls, a constrained action interface, and records that the agent cannot curate often beat a pile of conditional exceptions.

## Build the boundary around the action that can hurt you

Start with the external action whose misuse would force an unpleasant investigation. It may be a production SSH command, a release API call, an account change, or a paid request. Remove the credential from the agent environment, force that action through an executor, and test the failure paths before connecting more tools.

Run this short test against the design:

1. Request an action, then terminate the agent process while the request is in flight. Confirm that the journal retains a started event.
2. Make the remote destination return an error and confirm that the completion record distinguishes rejection, remote failure, and transport failure.
3. Attempt the same action with a caller supplied credential header or private key. Confirm that the executor rejects it.
4. Give the agent its normal interfaces and attempt to list, alter, or delete audit records. Confirm that it has no route to do so.
5. Copy the stored journal elsewhere and verify its chain without relying on the executor's live status.

Those tests find design gaps that a happy path demo hides. They also force a precise answer to an uncomfortable question: which actions can the agent still perform outside the recorded gateway? If the answer includes production authority, the record is incomplete by design. Say so plainly, close the route, or narrow the claims you make about the audit trail.

A useful audit record starts before the request leaves, ends with the best observed outcome, and remains outside the agent's control. Build that boundary before the first incident asks you to trust an agent's version of events.
