Authensor

Designing Tamper-Proof Audit Logs for AI Agents

Authensor Team · 2026-02-13

Designing Tamper-Proof Audit Logs for AI Agents

Every action an AI agent takes through SafeClaw is logged to an append-only JSONL file with a SHA-256 hash chain. This post explains why we chose this design, how it works, and why it matters for teams that need to prove what their agents did.

The Problem: Agent Actions Need a Paper Trail

When an AI agent deletes a file, installs a package, or makes a network request, you need a record of exactly what happened, when, and what the policy decided. This is not just good practice. For teams operating under SOC 2, HIPAA, the EU AI Act, or any framework that requires audit evidence, it is a requirement.

A simple log file is not enough. Log files can be edited. Lines can be deleted or modified. An adversary -- or an agent that gained more access than expected -- could tamper with the log to hide what happened. You need a way to detect tampering after the fact.

Why Hash Chains

A hash chain is a simple cryptographic structure. Each entry in the log includes the SHA-256 hash of the previous entry. This means:

The cost of this integrity guarantee is one SHA-256 computation per log entry -- microseconds on modern hardware. There are no additional dependencies, no blockchain, no distributed consensus. Just crypto.createHash('sha256') from Node.js standard library.

How SafeClaw's Audit Chain Works

Every gateway decision calls appendEntry() in src/audit.js. Here is what happens:

  • The entry object is constructed with: timestamp, tool name, action type, resource, outcome (allow/deny), receipt ID, task ID, profile name, decision source, and risk signals.
  • The function reads the hash of the last line in the audit file (cached in memory for performance; computed on cold start).
  • The prevHash field is set on the entry. For the very first entry, prevHash is the string GENESIS.
  • The entry is serialized to JSON and appended to ~/.safeclaw/audit.jsonl with mode 0o600 (owner-read-write only).
  • The SHA-256 hash of the serialized line is computed and cached for the next entry.
  • The result is a file where every line is cryptographically linked to the line before it. Here is what two entries look like in practice:

    ``json

    {"timestamp":"2026-02-13T10:00:01Z","toolName":"Read","actionType":"safe.read.file","resource":"/src/index.js","outcome":"allow","source":"local_prefilter","riskSignals":[],"prevHash":"GENESIS"}

    {"timestamp":"2026-02-13T10:00:03Z","toolName":"Bash","actionType":"code.exec","resource":"npm test","outcome":"allow","receiptId":"r_abc123","source":"authensor","riskSignals":[],"prevHash":"a1b2c3d4e5f6..."}

    `

    The second entry's prevHash is the SHA-256 hash of the first entry's complete JSON line. If you change a single character in the first entry, the hash changes, and verification fails.

    Verification

    Running safeclaw audit verify re-walks the entire chain:

  • Start with expected hash = GENESIS
  • For each line: parse the JSON, check that prevHash matches the expected hash, compute the SHA-256 of the line, set that as the expected hash for the next line
  • Report the result: total entries, chained entries, and any errors
  • The verification function also handles gracefully upgrading from pre-chain entries (entries written before the hash chain feature was added in v0.6.0). Entries without a prevHash field are skipped for verification but still contribute their hash to the chain going forward.

    Design Decisions We Made

    JSONL, not a database. We chose JSONL because it is append-only by nature, human-readable, trivially parseable, and does not require any dependencies. Every line is a self-contained JSON object. You can
    grep the audit log, pipe it through jq, or import it into any analytics tool. Local-only, not replicated. The audit file lives on your machine at ~/.safeclaw/audit.jsonl. It is not sent to Authensor or any remote service. This keeps your action history private and eliminates network dependencies for audit writes. File permissions. The audit file is written with mode 0o600 -- only the file owner can read or write it. This prevents other users on the same system from viewing or modifying the audit trail. Rotation support. For long-running deployments, rotateLog() renames the current file to audit.jsonl.1 and starts a fresh chain from GENESIS. The rotated file retains its chain integrity and can be verified independently. Never blocking. Audit write failures are caught and silently ignored. The gateway's primary job is gating actions. If the disk is full or the file system has an error, the gateway continues to enforce policy. Losing an audit entry is less harmful than blocking a legitimate action because the audit write failed.

    What Gets Recorded

    Every audit entry includes a source field that tells you how the decision was made:

    | Source | Meaning |

    |--------|---------|

    | local_prefilter | Safe read operation, allowed without contacting the control plane |

    | authensor | Decision came from the Authensor control plane |

    | offline_cache | Control plane was unreachable; used a cached allow decision |

    | fail_closed | Control plane was unreachable and no cache existed; action denied |

    | workspace_deny | Action targeted a path outside the configured workspace boundary |

    This source tracking means you can audit not just what happened, but how the system decided. If you see a cluster of fail_closed entries, you know your control plane had a connectivity issue. If you see offline_cache entries, you know which decisions were made from cached data.

    Compliance and Export

    The audit ledger is designed to serve as compliance evidence. The combination of tamper-proof hash chaining, per-entry timestamps, decision sources, and receipt IDs creates a complete, verifiable record of every AI agent action in your environment.

    For details on how the audit trail maps to specific compliance frameworks, see our documentation on SOC 2, HIPAA, and EU AI Act requirements.

    The full audit implementation is open source at github.com/AUTHENSOR/SafeClaw in src/audit.js. It is 165 lines of code, zero dependencies beyond Node.js crypto and fs`, and thoroughly tested.