InitRunner

Audit Trail

InitRunner logs every agent run to a local SQLite database. Audit records capture what happened, how much it cost, and whether it succeeded, so you have a complete history of agent behavior. For distributed tracing and performance analysis, see Observability.

What Gets Logged

Every agent run produces an audit record with these fields:

FieldTypeDescription
run_idstrUnique run identifier (12-character hex)
agent_namestrName from metadata.name
timestampdatetimeUTC timestamp of run start
user_promptstrInput prompt (subject to redaction)
outputstrAgent output (subject to redaction)
tokens_inintInput tokens consumed
tokens_outintOutput tokens consumed
cost_usdfloat | nullEstimated USD cost for the run (via genai-prices). null when pricing data is unavailable for the model/provider
tool_callsintNumber of tool calls made during the run
duration_msintWall-clock duration in milliseconds
successboolWhether the run completed without error
errorstr | nullError message if the run failed
trigger_typestr | nullHow a daemon run was initiated: cron, file_watch, webhook, telegram, discord, slack, heartbeat, scheduled. null for manual run and serve invocations
principal_idstr | nullIdentity of the trigger source (e.g. telegram:12345, discord:67890, slack:12345). null for manual runs. Independent of agent principals.
thinking_tokensintThinking/reasoning tokens reported by the model. Defaults to 0
reasoning_tokensintReasoning tokens from the final streaming event. Mirrors thinking_tokens on the non-streaming path. Defaults to 0
event_timelinelist | nullRedacted per-run timeline of tool calls, tool results, and thinking deltas. Stored as event_timeline_json. See Run-Event Timeline
judge_verdictslist | nullVerified-reflexion judge verdicts. Empty for non-reflexion runs. See Judge Verdicts

The thinking_tokens and reasoning_tokens counts feed the run-cost aggregates described in Cost Tracking.

Principal Tracking

Every audit record stores a principal_id field that tracks the trigger source identity. This is independent of agent principals and is preserved across execution paths. Triggers set it to a namespaced value, for example telegram:12345, discord:67890, or slack:12345. Manual runs leave it null.

The field is written on every record, but the export command and the audit API responses do not expose it. To filter by principal today, query the SQLite database directly:

sqlite3 ~/.initrunner/audit.db \
  "SELECT run_id, agent_name, timestamp FROM audit_log WHERE principal_id = 'telegram:12345' ORDER BY timestamp DESC"

Run-Event Timeline

Since v2026.5.5, when audit is enabled every initrunner run persists a structured run-event timeline. The timeline records what happened during the run as an ordered list of entries. It is captured even without a live dashboard or streaming consumer: when no live event stream exists, the timeline is reconstructed from the run's final message history.

A CLI run persists three entry types:

TypeSourceDescription
thinking_deltaModel thinking outputA chunk of the model's thinking, capped at 200 characters
function_tool_callTool invocationA tool call with an args preview, capped at 120 characters
function_tool_resultTool returnA tool result preview in content_preview, capped at 120 characters

The live-stream path adds a fourth type, tool_call_delta, for incremental tool-call arguments. CLI runs that reconstruct the timeline from message history do not emit it.

Every free-text value is secret-scrubbed and length-bounded before it is written, and the timeline keeps at most the 500 most recent entries. The timeline is stored on the audit record as event_timeline_json and served decoded as event_timeline by the drill-down API.

[
  {"type": "thinking_delta", "content_delta": "I should check the file first"},
  {"type": "function_tool_call", "tool_name": "read_file", "args_preview": "{\"path\": \"config.yaml\"}"},
  {"type": "function_tool_result", "content_preview": "name: example"}
]

--no-audit skips timeline capture entirely. With no audit logger, neither the live timeline nor the message-history reconstruction runs, so non-audited runs add no overhead.

Legacy rows written before v2026.5.5 have an empty or absent timeline.

Judge Verdicts

Verified-reflexion runs that have success_criteria configured persist per-round judge verdicts on the audit record. Each entry has this shape:

{"round": 1, "all_passed": false, "criteria_results": [{"criterion": "...", "passed": false}]}
FieldTypeDescription
roundintReflection round number
all_passedboolWhether every criterion passed in that round
criteria_resultslistPer-criterion results for the round

Verdicts are empty for non-reflexion runs and for reflexion runs without success criteria. See Reasoning for how reflection rounds and success criteria are configured.

Per-Run Drill-Down API

The dashboard fetches a single run's full detail through GET /api/audit/{run_id}. The response is the base audit record plus the parsed event_timeline and judge_verdicts arrays. It returns 404 with Run not found when the run_id is unknown.

# Single run with timeline and judge verdicts
GET /api/audit/{run_id}

curl http://localhost:8000/api/audit/{run_id}

This contrasts with the list endpoint GET /api/audit, which returns records without the timeline or verdicts. The base URL is set by NEXT_PUBLIC_API_URL and defaults to http://localhost:8000. The dashboard Audit page consumes this endpoint to render a per-run view.

Storage

Audit records are stored in a SQLite database:

  • Default path: ~/.initrunner/audit.db
  • Environment variable: INITRUNNER_AUDIT_DB (overridden by --audit-db)
  • Custom path: --audit-db ./custom-audit.db
  • Disable entirely: --no-audit
# Default audit database
initrunner run role.yaml -p "Hello"

# Custom audit database path
initrunner run role.yaml -p "Hello" --audit-db ./my-audit.db

# Disable audit logging
initrunner run role.yaml -p "Hello" --no-audit

The same flags work with initrunner run --daemon and initrunner run --serve.

Export

Export audit records as JSON or CSV for analysis, reporting, or ingestion into external systems.

initrunner audit export
FlagTypeDefaultDescription
--agentstr(all)Filter by agent name
--run-idstr(all)Filter by run ID
--trigger-typestr(all)Filter by trigger type (cron, file_watch, webhook, etc.)
--sincestr(none)Start date (ISO 8601, e.g. 2025-01-01)
--untilstr(none)End date (ISO 8601)
--limitint1000Maximum records to export
-f, --formatstr"json"Output format: json or csv
-o, --outputstrstdoutOutput file path

Examples

# Export all records as JSON
initrunner audit export

# Export last 7 days for a specific agent as CSV
initrunner audit export --agent monitor-agent --since 2025-01-08 -f csv -o report.csv

# Export only cron-triggered runs
initrunner audit export --trigger-type cron --limit 500

# Export to a file
initrunner audit export -o audit-export.json

Tamper-Evident Chain

Since v2026.4.15, every audit record is signed with HMAC-SHA256 over the previous record's hash. The result is a tamper-evident chain: changing or removing a row in the middle of the log breaks every signature after it. Signing happens inside BEGIN IMMEDIATE, so concurrent writers serialize through SQLite's RESERVED lock instead of forking the chain.

As of v2026.5.5, the chain also covers the new thinking_tokens, reasoning_tokens, event_timeline_json, and judge_verdicts fields. An idempotent migration backfills these columns on existing databases when the audit database is opened. Legacy rows verify as before, defaulting to 0 for the integer columns and empty or NULL for the timeline and verdicts.

Key Storage

The HMAC key is loaded in this order:

  1. INITRUNNER_AUDIT_HMAC_KEY env var (64-character hex, decodes to 32 bytes)
  2. ~/.initrunner/audit_hmac.key (32 raw bytes, mode 0600)
  3. Auto-generated on first signed write and saved to the file above

A copied audit database without the key cannot be verified. Verification never auto-creates a key, so an unrecognised database fails cleanly instead of silently re-signing under a fresh chain.

Verifying the Chain

initrunner audit verify-chain
initrunner audit verify-chain --audit-db ./custom-audit.db

The command walks every signed row and reports:

FieldDescription
Total rowsRecords in the database
Unsigned (legacy)Rows written before the chain feature was enabled
VerifiedRows whose signature matched
Tip id / Tip hashLast signed row and its hash (truncated to 16 chars)
Pruned gapsHoles left by audit prune (informational, not breaks)

Exit code is 0 on success and 1 on any break or missing key. Common failure reasons:

ReasonMeaning
key_missingNo env var and no key file. Set INITRUNNER_AUDIT_HMAC_KEY or place a key at ~/.initrunner/audit_hmac.key.
key_invalidEnv var is not valid 64-char hex.
hash_mismatchA row was modified after it was signed.
prev_hash_mismatchA row in the middle of the chain was deleted or rewritten.

Pruning leaves pruned_gaps rather than breaks because both audit prune and verify-chain recognise gaps from id renumbering as expected.

Security Events

Since v2026.4.16, the runtime writes a separate security_events table alongside the main audit log. The table captures low-level events that do not belong in per-run records (sandbox launches, circuit-breaker state changes) but still need a trail.

Query the table with:

initrunner audit security-events
initrunner audit security-events --event-type sandbox.exec
initrunner audit security-events --agent code-runner --limit 200
Event typeWritten bydetails contents
sandbox.execRuntime sandbox backends (bwrap, docker, ssh)backend, argv0, rc, duration_ms
circuit_*Daemon circuit breaker on state transitionsThe old and new breaker state

Each row stores timestamp, event_type, agent_name, and a free-text details string, and is attributed to the role's agent_name. The security_events table is not part of the HMAC chain, so verify-chain covers only audit_log.

Pruning

Remove old audit records to manage database size.

Manual Pruning

initrunner audit prune
initrunner audit prune --retention-days 30 --max-records 50000
FlagTypeDefaultDescription
--retention-daysint90Delete records older than this
--max-recordsint100000Keep at most this many records (oldest removed first)

Automatic Pruning

Configure auto-pruning via the security policy in your role YAML:

security:
  audit:
    retention_days: 30
    max_records: 50000
FieldTypeDefaultDescription
retention_daysint90Delete records older than this many days
max_recordsint100000Maximum audit records to retain

Auto-pruning runs at daemon startup and periodically during long-running daemons.

Redaction

Audit logs can contain sensitive information. InitRunner supports two redaction mechanisms to sanitize records before they are written.

PII Redaction

Enable built-in PII pattern detection:

security:
  content:
    pii_redaction: true

This detects common PII patterns in both prompts and outputs before writing to the audit database. Each match is replaced with [REDACTED]:

PatternExample
Email addressesuser@example.com
Social Security Numbers123-45-6789
Phone numbers+1-555-123-4567
API keyssk-abc123...

Custom Redaction Patterns

Add regex patterns to redact domain-specific sensitive data:

security:
  content:
    redact_patterns:
      - "\\b[A-Z]{2}\\d{6}\\b"        # internal account IDs
      - "\\btoken_[a-zA-Z0-9]+\\b"     # internal tokens

Custom patterns are applied in addition to PII redaction (if enabled). Matches are replaced with [REDACTED].

Viewing Audit Logs

Beyond the CLI export command, audit logs are accessible through:

  • Dashboard: the Audit page offers search, pagination, and CSV/JSON export
  • Direct SQLite access: query ~/.initrunner/audit.db with any SQLite client
# Quick peek at recent records
sqlite3 ~/.initrunner/audit.db "SELECT agent_name, trigger_type, success, duration_ms FROM audit_log ORDER BY timestamp DESC LIMIT 10"

On this page