audit-logging — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited audit-logging (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
The compliance backbone — every agent action gets a log entry, every significant decision gets a record.
| Standard | What we use |
|---|---|
| OCSF | Event class structure, severity levels |
| OpenTelemetry | trace_id / span_id for workflow correlation |
| ISO 27001:2022 A.8.15 | Mandatory fields: who, what, when, where |
Each log entry is one JSON object per line, appended to .compliance/audit.log. Full schema: skills/audit-logging/audit-event.schema.json.
{
"event_id": "uuid-v4",
"timestamp": "ISO 8601",
"trace_id": "hex-32",
"event_class": "session | tool_call | decision | data_access | error",
"activity": "start | read | write | execute | classify | scan | block | approve | assess",
"severity": "INFO | LOW | MEDIUM | HIGH | CRITICAL",
"outcome": "success | failure | blocked | deferred",
"summary": "Human-readable one-liner"
}| Class | Emitted for | Activity |
|---|---|---|
session | A session boundary: startup, resume, /clear, compaction, fork | start |
tool_call | A tool request and its result | read, write, execute |
decision | An Agent Decision Record — requires the decision block | block, approve, assess |
data_access | Classified data read or written | read, write, classify, scan |
error | A failure worth auditing on its own | any |
execute is the conservative label for tools that are neither a clean read nor a clean write — shell commands, sub-agents, MCP calls — and the default for any tool not otherwise recognised. Never downgrade an unknown tool to read.
Add only when relevant — keep log entries lean:
| Field | When to include |
|---|---|
span_id | Multi-step workflows (hex-16) |
parent_span_id | Nested operations |
agent_id | Multi-agent setups |
resource | File/field/API being accessed — include classification if known |
tool | Tool name + exit code for tool_call events |
decision | Required for event_class: "decision" — see ADR below |
metadata | Anything else worth preserving |
An audit event with event_class: "decision" and a decision block. Created when the agent:
"decision": {
"action": "block | approve | defer",
"reasoning": "Why this action was taken",
"policy_ref": "GDPR Art. 9 | NIS2 Art. 21(2)(h) | ...",
"human_override": false
}.compliance/audit.log (project root)audit-YYYY-MM-DD.log)| Entity type (NIS2) | Minimum | Recommended |
|---|---|---|
| Essential | 18 months | 24 months |
| Important | 6 months | 12 months |
| Voluntary | 3 months | 6 months |
When writing code, include structured logging for: auth events, authorization, data access, data modification, external API calls, errors, config changes, batch jobs. Use structured key-value pairs — never string interpolation. Never log secrets, PII, or session tokens.
For the full operations table, correct/wrong code examples per language, what-not-to-log rules, and language-specific logger recommendations, see references/logging-guide.md.
Parts of this skill do not depend on the agent remembering to act. complisec ships hooks/hooks.json, which Claude Code auto-discovers when the plugin is installed — no settings.json edit, nothing for the user to copy by hand.
| Hook | Event written | Guarantee |
|---|---|---|
SessionStart | session / start | Every session boundary — startup, --continue/--resume, /clear, compaction, fork |
PreToolUse | tool_call, outcome: "deferred" | Every tool request, whatever becomes of it |
PostToolUse | tool_call, outcome: "success" | Every successful tool result, with exit_code where the tool reports one |
PostToolUseFailure | tool_call, outcome: "failure" | Every failed tool call. PostToolUse does not fire for these, so without it a failure would sit in the log as a request that never resolved |
PermissionDenied | tool_call, outcome: "blocked" | A tool call refused by auto mode's classifier or auto-deny logic. The only producer of blocked — but see the limitation below |
The request and its result share a span_id derived from the tool use id, so the two join without either hook process keeping state.
An unresolved `deferred` means the call did not run. Most requests resolve to success, failure or blocked. The ones that do not were refused before execution, and not every refusal path emits an event: a refusal by an explicit permissions.deny rule fires no hook at all — verified by observation, not assumed — so the unresolved request is the only trace it leaves. Read a dangling deferred as "requested, never executed"; it is a real signal, not a gap in the log.
deny_reason explains why a call was blocked, and it is the most useful part of a block event — but it can quote the command it refused, which this hook otherwise never logs. It is off by default. Where the org's risk appetite allows it, turn it on in the env block of .claude/settings.json:
{ "env": { "COMPLISEC_AUDIT_DENY_REASON": "1" } }Tool input is never logged. The hooks record the tool name, the target file_path, the permission mode, and the tool_use_id — nothing else from tool_input. A shell command line or a file payload can carry a credential, and this log is append-only with a retention floor measured in months. The tool_use_id joins the event to the transcript, which is where the detail already lives. Hold the same line in any event you write by hand.
The audit trail is opt-in per project: the hooks write only where .compliance/ exists, so a globally installed complisec does not drop an audit log into unrelated repositories. When it is not active, the SessionStart hook says so in your context instead of failing quietly — surface that to the user.
The SessionStart hook also puts the session trace_id in your context. It is derived from the Claude Code session id, so it is stable across /clear and compaction, and every hook reaches the same value without shared state.
Follow this process every session:
session/start event and placed the session trace_id in your context. Adopt that `trace_id` verbatim for every event you write — minting a second one splits the session's evidence across two traces. Generate one yourself only if no hook context was provided (a platform without hook support).span_id (16-char hex) for each discrete operation.tool_call event by hand (a platform without hook support), map the tool to read, write, or execute, and default anything unrecognised to execute..compliance/audit.log.trace_id, request_id) where the framework supports it{"event_id":"a1b2c3d4-...","timestamp":"2026-03-13T10:15:00Z","trace_id":"abc123...","event_class":"tool_call","activity":"scan","severity":"INFO","outcome":"success","summary":"Scanned src/ for sensitive data patterns","tool":{"name":"data-sensitivity","exit_code":0}}{"event_id":"e5f6g7h8-...","timestamp":"2026-03-13T10:15:05Z","trace_id":"abc123...","event_class":"decision","activity":"block","severity":"HIGH","outcome":"blocked","summary":"Blocked commit containing BSN","resource":{"type":"file","id":"src/users.py","classification":"RESTRICTED"},"decision":{"action":"block","reasoning":"File contains plaintext BSN (national ID)","policy_ref":"GDPR Art. 87","human_override":false}}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.