C.W.K.
Stream
Lesson 04 of 05 · published

Audit Trails for LLM Actions

~12 min · audit, logging, compliance

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

What an audit trail records

For each agent run: who triggered it (user id, system trigger), when, what prompt, what tools called with what arguments, what tool results returned, what final answer the agent produced, and what permission decisions a human made. Together, these answer 'what happened' months later.

Hooks are your audit hook (literally)

The simplest implementation: a PostToolUse hook that appends a structured line per tool call, plus a UserPromptSubmit hook for the trigger and a final summary on Stop. Each line carries a session id so you can stitch them into a timeline.

Retention vs cost

Audit logs grow. Decide retention policy up front — 30/90/365 days based on regulatory or business need. Cold-storage tiers (S3 Glacier, GCS Coldline) make multi-year retention cheap; hot-storage queries should target the last N days only.

Principle: An action without an audit trail is a fingerprint-free incident. Build the trail first; add the cool features later.

Code

Structured audit logger·python
import json, time, uuid
from claude_agent_sdk import ClaudeAgentOptions

class Audit:
    def __init__(self, path: str, run_id: str | None = None):
        self.path = path
        self.run_id = run_id or str(uuid.uuid4())

    def emit(self, event_type: str, **fields):
        line = {"ts": time.time(), "run_id": self.run_id, "event": event_type, **fields}
        with open(self.path, "a") as f:
            f.write(json.dumps(line) + "\n")

async def make_hooks(audit: Audit):
    async def on_user_prompt(context):
        audit.emit("user_prompt", text_hash=hash(context.prompt))
        return HookOutput(allow=True)

    async def on_tool_post(context):
        audit.emit(
            "tool_use",
            tool=context.tool_name,
            input_keys=sorted(context.tool_input.keys()),
            duration_ms=context.duration_ms,
            error=context.is_error,
        )

    return {"UserPromptSubmit": [on_user_prompt], "PostToolUse": [on_tool_post]}
Stitching events into a session timeline·python
import json
from collections import defaultdict

def session_timeline(audit_path: str):
    runs = defaultdict(list)
    for line in open(audit_path):
        ev = json.loads(line)
        runs[ev["run_id"]].append(ev)
    for run_id, events in runs.items():
        events.sort(key=lambda e: e["ts"])
        print(f"\n== run {run_id} ({len(events)} events) ==")
        for e in events:
            print(f"  {e['ts']:.3f}  {e['event']}")

External links

Exercise

Implement the audit logger pattern in one agent. Run a session, then write a query that answers 'how many tool calls in the last hour, by tool name'. Verify the log answers it cheaply.
Hint
If your log is text-only, switch to JSONL — every observability question gets harder when the format is not structured.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.