Telemetry · Logs
Logs — audit-log entries, dual-written
Audit entries are dual-written: OTEL Logs export to <endpoint>/v1/logs AND a local JSONL file at collector.logs.path (default ~/.nio/audit.jsonl). The JSONL line is the entry verbatim; the OTEL LogRecord uses body = JSON.stringify(entry) plus a flat attribute set for indexing.
Entry types
Discriminated by the event field. Five shapes — four named entry types plus the catch-all "hook event" record.
event: "guard"
Guard decision per PreToolUse / PostToolUse
| Field | Type | Notes |
timestamp | string | ISO-8601 |
platform | string | claude-code / hermes / openclaw |
session_id | string? | host session id |
cwd | string? | working dir |
tool_name | string | host tool name |
action_type | string? | exec_command / write_file / network_request / read_file |
tool_input_summary | string | redacted ≤200-char summary of tool input |
decision | string | allow / deny / ask |
risk_level | string | low / medium / high / critical |
max_finding_severity | string | highest finding severity |
risk_score | number | 0–1 final score |
risk_tags | string[] | rule IDs hit (deduped) |
phase_stopped | number | null | which Phase 0–6 produced the decision |
scores | Record<string, number> | per-phase score (runtime, static, behavioural, llm, external, final) |
phases | AuditPhaseMap? | per-phase {score, finding_count, duration_ms} |
top_findings | AuditFindingSummary[] | up to 5: {rule_id, severity, category, title, confidence} |
explanation | string? | human-readable reason |
initiating_skill | string? | which skill scope the action originated from |
event_type | "pre" | "post"? | which hook side fired |
event: "session_scan"
Skill scan (on-demand or session-start)
| Field | Type | Notes |
timestamp | string | ISO-8601 |
platform | string | host |
session_id | string? | host session id |
skill_name | string | scanned skill / dir |
risk_level | string | aggregated severity |
risk_tags | string[] | rule IDs hit |
finding_count | number? | total findings |
event: "lifecycle"
Subagent / agent / session lifecycle
| Field | Type | Notes |
timestamp | string | ISO-8601 |
platform | string | host |
session_id | string? | host session id |
lifecycle_type | string | subagent_spawning / subagent_ended / agent_end / session_start / session_end |
details | Record<string, unknown>? | platform-specific (e.g. OpenClaw: {subagent_id, run_id}) |
event: "config_error"
Config load failure
| Field | Type | Notes |
timestamp | string | ISO-8601 |
config_path | string | path that failed to load |
error_message | string | parser / IO error |
event: <hook event>
Collector hook record (one entry per dispatched hook event)
Discriminator is the canonical hook event name itself: UserPromptSubmit · PreToolUse · PostToolUse · TaskCreated · TaskCompleted · Stop · SubagentStop · SessionStart · SessionEnd.
| Field | Type | Notes |
timestamp | string | ISO-8601 |
platform | string | host |
session_id | string? | host session id |
cwd | string | null | working dir |
transcript_path | string? | Claude Code-only — path to session transcript JSONL |
tool_name | string? | for PreToolUse / PostToolUse |
tool_use_id | string? | for PreToolUse / PostToolUse |
tool_summary | string? | for PreToolUse / PostToolUse |
task_id | string? | for TaskCreated / TaskCompleted |
task_summary | string? | for TaskCreated |
OTEL LogRecord projection
The flat attribute set used for OTEL Logs indexing. Same key names as the matching trace span attributes wherever a concept overlaps (tool name, conversation id, guard decision, …) — same query keys work across logs and traces.
body = JSON-stringified entry (full content of the JSONL line)
severityNumber / severityText derived from risk_level: low→INFO, medium→WARN, high→ERROR, critical→FATAL; INFO when no risk_level
| Attribute | Description | Captured at | Platforms |
gen_ai.tool.name | Host tool name; same key as the tool-span attribute | PreToolUse · PostToolUse · guard decision | all |
gen_ai.tool.call.id | Host tool-call id; same key as the tool-span attribute | PreToolUse · PostToolUse | all |
gen_ai.conversation.id | Host session id; same key as the turn-span attribute | every audit entry with a session | all |
session.id | Mirror of gen_ai.conversation.id for OTel base-spec consumers | every audit entry with a session | all |
nio.guard.decision | Guard verdict — allow / deny / ask | guard decision | all |
nio.guard.risk_level | Guard risk level — low / medium / high / critical | guard decision | all |
nio.guard.risk_score | Guard risk score, 0–1 | guard decision | all |
nio.guard.risk_tags | Comma-joined rule IDs that fired | guard decision | all |
nio.tool_summary | One-line summary derived from tool input | PreToolUse · PostToolUse | all |
nio.task_id | Task id from the dispatch event | TaskCreated · TaskCompleted | Claude Code + OpenClaw |
nio.task_summary | Derived from task input | TaskCreated | Claude Code + OpenClaw |
nio.platform | Source platform — claude-code / hermes / openclaw | every audit entry | all |
nio.cwd | Working dir at hook fire | every audit entry with cwd | all |
nio.event | Discriminator — hook event name vs guard / lifecycle / scan / config_error | every audit entry | all |
nio.event_type | pre / post for guard entries | guard decision | all |
nio.action_type | exec_command / write_file / network_request / read_file | guard decision | all |
nio.max_finding_severity | Highest finding severity surfaced this run | guard decision | all |
nio.phase_stopped | Which Phase 0–6 produced the decision | guard decision | all |
nio.explanation | Human-readable reason for the verdict | guard decision | all |
nio.transcript_path | Claude Code-only — path to session transcript JSONL | hook events with transcript | Claude Code only |
nio.phases.{name}.score | Per-phase score (Phase 0–6) | guard decision | all |
nio.phases.{name}.finding_count | Per-phase finding count | guard decision | all |
nio.phases.{name}.duration_ms | Per-phase wall-clock cost (ms) | guard decision | all |
Local JSONL path: collector.logs.path (default ~/.nio/audit.jsonl). Rotation kicks in at collector.logs.max_size_mb (default 100 MB) — the live file is renamed to <path>.1.