OrmAI

Article

Designing audit trails LLMs can't tamper with

When the agent has tools, the audit log becomes a target. Here's how to design one that survives a determined LLM (or a determined attacker reaching the LLM).

Dipankar Sarkar · ·Updated April 15, 2026 auditsecuritytamper-resistancecompliance

In a normal application, the audit log is a write-once side effect that no one but admins reads. In an agent application, the agent itself often has tool access. If those tools include anything close to the audit log — directly or indirectly — the log becomes part of the threat surface.

This article is about designing audit trails that hold up when the LLM is adversarial, prompt-injected, or just misbehaving.

The threat model

What can a misbehaving agent (or an attacker who has prompted it) do to the audit log?

  1. Suppress logging. Avoid taking actions that would be logged.
  2. Mute logging. Take an action that disables future logging.
  3. Forge log entries. Write to the log to create false evidence.
  4. Modify or delete entries. Cover tracks after the fact.
  5. Confuse investigators. Flood the log with noise so real signal is buried.

Standard application logs are vulnerable to all of these if the agent’s tools include log access. Even without explicit log access, indirect paths exist (e.g., an agent that can write to a config file that controls log routing).

Five design properties

A good agent audit log has these properties.

1. Out-of-band of agent tools

The log should not be in any tool the agent can call. Not directly, not transitively. The cleanest pattern: write the log on the application side, immediately after a tool call returns, before any other code runs. The agent never has a tool that could affect the log.

In OrmAI, the audit row is written by the policy compiler before the result is returned to the caller. There is no tool the agent can call that writes to the audit table. The audit table user is INSERT-only at the database level; not even an admin tool can UPDATE or DELETE.

2. Append-only at the storage layer

Database-level enforcement matters. Application code that “always appends” is not the same as a table that can’t be updated. Use:

  • A dedicated DB user for audit writes with no UPDATE/DELETE privileges.
  • Where possible, an append-only log store (Postgres INSERT-only role, S3 with object lock, BigQuery streaming insert).
  • For the highest-stakes systems, a separate database entirely with a different operations team.

3. Hash-chained for tamper detection

Each row includes the hash of the previous row. Tampering with row N invalidates the hash chain for all rows ≥ N. A periodic background job verifies the chain.

OrmAI ships a HashChainSink that wraps any other sink. The chain hash is stored on each row. Verification is a single linear pass over the table.

This doesn’t prevent tampering — it detects it. The deterrent is that any successful tamper attempt becomes evident on the next verification run. For most threat models that’s enough.

4. Structured fields, not free-form strings

A free-form log message ("Agent did X for Y") is hard to query and easy to forge convincingly. A structured row with explicit fields (tool, model, input_sanitized, policy_decision, outcome) is queryable, comparable, and harder to fake at scale.

OrmAI’s audit row is fully structured. The only free-form fields are reason (which the agent supplied) and error (when present). Both are bounded in length.

5. Independent retention policy

The audit log has a different retention need than your operational data. Compliance regimes typically want 1–7 years of audit retention. Application data may rotate faster.

Storing the audit log in your application database mixes those concerns. We recommend either a separate database for audit, or a copy of audit rows to a long-term store (object storage, archive table) on a regular schedule. OrmAI’s retention worker handles this; configure it once.

What about agent self-reporting?

Some teams have the agent write a “what I just did” log entry at the end of each tool call. This is fine for debugging but should never be the audit log. The agent can lie. The agent can omit. The agent can be prompted to omit. Self-reporting captures the agent’s narrative, which is informative but not evidence.

The structured tool-call log is the evidence. Keep them separate.

Cross-correlation with other systems

The audit log is most useful when you can correlate it with other observability:

  • Trace IDs: every audit row carries the trace ID of the originating HTTP request. You can pivot from “this audit row” to “the full request that produced it.”
  • LLM completion IDs: the application records which LLM call (provider request ID) led to which tool call. Joining these lets you see “the prompt that caused this leak.”
  • User session IDs: ties multiple tool calls to a single user interaction.

Plan these correlations before launch. Adding them later requires backfill that’s often impossible.

What to log, what to not log

Log:

  • Every tool call (success, denial, timeout).
  • Sanitized inputs (with raw input hashed for forgery detection, not stored).
  • Policy decision (allowed/denied + reason).
  • Output row count and shape (not full rows).
  • Principal, tenant, trace, session.
  • Approver identity (when applicable).

Don’t log:

  • The full output row (it’s data; it has its own privacy rules).
  • Raw inputs that may contain user prompts (PII).
  • Free-form LLM completions (different log, different scrubbing).
  • Database connection strings, credentials, tokens — ever.

Querying the log under pressure

When something goes wrong and the security team wants answers in under an hour, the log query is the critical artifact. Build (and rehearse) these:

-- "What did the agent do for tenant 42 yesterday?"
SELECT ts, principal, tool, model, outcome
FROM ormai_audit_log
WHERE tenant_id = 42 AND ts BETWEEN '2026-04-14' AND '2026-04-15'
ORDER BY ts;

-- "Did the agent ever read field X on model Y?"
SELECT ts, principal, input_sanitized
FROM ormai_audit_log
WHERE model = 'Customer'
  AND policy_decision->'redacted_fields' @> '["customer.email"]'
  AND outcome = 'success';

-- "Show me every write in the last hour without a reason longer than 10 chars."
SELECT * FROM ormai_audit_log
WHERE tool IN ('db.create','db.update','db.delete')
  AND char_length(coalesce(input_sanitized->>'reason', '')) < 10
  AND ts > now() - interval '1 hour';

-- "Show me the hash-chain validation status for the last 24 hours."
SELECT * FROM ormai_audit_chain_check WHERE checked_at > now() - interval '24 hours';

Save these as views. Hand the views to your security team. The conversation goes from “let me write a script” to “here’s the answer.”

When this matters most

Tamper-resistant audit becomes critical the moment your agent can write. Read-only agents have a leak surface; write-capable agents have a fraud surface. The audit log is your evidence in any post-incident investigation. Treat it accordingly.

For regulated industries (finance, health, legal) the standards are stricter and codified. Talk to compliance early; design the audit pipeline to meet their requirements before launch, not after.


Found a typo or want to suggest a topic? Email [email protected].