Skip to content

Audit log

Every call is recorded in a JSON Lines file (audit= on the Guard, default agentguard.jsonl). Each event stores the SHA-256 hash of the previous event, so editing or deleting a line breaks the chain.

Events

Stage Meaning
rejected Malformed call, unknown tool or halted session; never evaluated
proposed Evaluated: policy_result, risk_score, evaluated_decision, reasons
denied Not executed, with the reason. Queued approvals add approval_request_id and approval_status: pending
authorized Allowed or approved (approved_by, including the grant ID), about to run
executing Execution started
executed Execution returned
observed Verification result, result type, whether output contained a secret
failed Execution or verification failed (exception type only); session halted

Events for one call share an action_id. timestamp is when the event was written; action_timestamp is when the call was proposed. An executing event with no later event can mean a crash mid-call. Arguments are redacted. Full results and raw exception messages are never logged.

Reading logs

agentguard verify-log --audit agentguard.jsonl
agentguard logs --audit agentguard.jsonl --decision deny
agentguard inspect evt_... --audit agentguard.jsonl
agentguard report --audit agentguard.jsonl --dry-run-only

Readers verify every segment and the checkpoint before showing anything. The dashboard shows the same data in a browser.

Configuring the logger

import os

from agentguard.audit.export import HttpExporter, LoggingExporter
from agentguard.audit.logger import AuditLogger

audit = AuditLogger(
    "/var/log/agentguard/audit.jsonl",
    max_bytes=50_000_000,                          # rotate at 50 MB
    signing_key=os.environ["AGENTGUARD_AUDIT_KEY"],
    exporters=[LoggingExporter()],
)
guard = Guard("policy.yaml", audit=audit)

Checkpoint

Appends keep a small checkpoint file next to the log (audit.jsonl.head) with the event count, last hash, file size and the offset of the last event. Each append checks that the file size matches and the last event verifies, instead of re-reading the whole log, so append cost stays flat as the log grows (about 7 ms, mostly two fsync calls). An append from outside AgentGuard, a truncated tail, or an edited last event stops the next call. Edits deeper in the file are caught by full verification (verify-log, the dashboard, read_log). Logs without a checkpoint are verified once and migrated on the next append.

Rotation

With max_bytes, the active file is renamed to audit.000001.jsonl, audit.000002.jsonl, ... before it would exceed the limit. The hash chain continues across segments, and readers verify them all in order. Archive old segments together with the active log.

Signing

With signing_key, each event gets signature: hmac-sha256:<hex> over its hash. Hash chaining alone cannot detect someone who rewrites the whole chain and recomputes every hash; without the key, they cannot recompute the signatures. Verify with the key:

agentguard verify-log --audit audit.jsonl --key-env AGENTGUARD_AUDIT_KEY
agentguard dashboard --audit audit.jsonl --audit-key-env AGENTGUARD_AUDIT_KEY

Keep the key away from the machine's other users and from agents. Anyone with the key can sign events.

Export to OpenTelemetry and SIEMs

Exporters receive each event after it is durably written. Failures are logged and never block the agent; the local log stays the record of truth.

LoggingExporter emits a Python log record per event with flat attributes (agentguard.tool, agentguard.risk_score, agentguard.final_decision, ...) and the full event as JSON in agentguard.event. Attach any handler. For OpenTelemetry:

import logging

from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor

provider = LoggerProvider()
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
logging.getLogger("agentguard.audit").addHandler(LoggingHandler(logger_provider=provider))
logging.getLogger("agentguard.audit").setLevel(logging.INFO)

For syslog-based SIEMs, attach logging.handlers.SysLogHandler instead.

HttpExporter(url, headers=..., transform=..., secret=...) POSTs each event as JSON from a background thread with a bounded queue (dropped events are counted in dropped). For Splunk HEC:

HttpExporter(
    "https://splunk.example:8088/services/collector/event",
    headers={"Authorization": f"Splunk {hec_token}"},
    transform=lambda event: {"event": event, "sourcetype": "agentguard"},
)

Limits

  • Hash chains and signatures prove the log is internally consistent and was written by a key holder. They do not prevent deletion of the whole log; ship events off the machine (exporters) if that matters.
  • Secret redaction is pattern-based. Protect log files and their retention.