> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-matrix-channel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit Logging

> How Comis records security-relevant events for monitoring and compliance

Every security-relevant action in Comis is recorded as an audit event. Whether an agent sends a message, accesses a secret, triggers a jailbreak detection, or has an action denied, the event is logged with a timestamp, the acting agent, the action taken, and the outcome. These records give you visibility into what your agents are doing and help you meet compliance requirements.

## What Gets Logged

Comis generates audit events for a wide range of security-relevant actions. These include agent executions, tool invocations, secret access, message sends, approval decisions, and any action that is denied or times out. Every event is structured as JSON, making it machine-readable from the start.

Each audit event captures a set of fields that tell you exactly what happened:

| Field          | Description                                     | Example                         |
| -------------- | ----------------------------------------------- | ------------------------------- |
| Timestamp      | When the event occurred (ISO 8601)              | `2026-03-12T14:30:00.000Z`      |
| Event ID       | Unique identifier (UUID v4)                     | `a1b2c3d4-...`                  |
| Tenant ID      | Your installation identifier                    | `default`                       |
| Agent ID       | Which agent performed the action                | `customer-support`              |
| User ID        | Which user triggered the action (if applicable) | `user-123`                      |
| Action         | What was done                                   | `file.delete`, `message.send`   |
| Classification | Action risk level                               | `read`, `mutate`, `destructive` |
| Outcome        | What happened                                   | `success`, `failure`, `denied`  |
| Trace ID       | Request correlation identifier                  | `trace-abc-123`                 |
| Duration       | How long the action took                        | `150ms`                         |

Not every field is present in every event. For example, background tasks may not have a User ID, and read-only actions may not include a Duration.

### Common Audit Actions

Here are some of the actions you will see most frequently in your audit logs:

| Action             | Classification | What It Means                                             |
| ------------------ | -------------- | --------------------------------------------------------- |
| `message.send`     | `mutate`       | An agent sent a message to a channel                      |
| `tool.execute`     | varies         | An agent used a tool (classification depends on the tool) |
| `secret.access`    | `read`         | An agent accessed an encrypted secret                     |
| `file.delete`      | `destructive`  | An agent deleted a file                                   |
| `approval.grant`   | `mutate`       | An operator approved a pending action                     |
| `approval.deny`    | `mutate`       | An operator denied a pending action                       |
| `injection.detect` | `read`         | A potential jailbreak attempt was detected                |

For the complete list of all action classifications (178 actions across 21 categories), see the [Action Classifier Reference](/reference/action-classifier).

### Example Audit Log Entry

A real (sanitized) audit event — one scrubbed line from
`~/.comis/logs/security-audit.jsonl` (the same shape appears as a `.audit()`-level
line in `~/.comis/logs/daemon.*.log` and as a row in the `obs_audit_events` table):

```json theme={}
{
  "level": 30,
  "time": "2026-04-25T14:30:00.000Z",
  "module": "audit",
  "audit": true,
  "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "tenantId": "default",
  "agentId": "code-assistant",
  "userId": "user-123",
  "actionType": "file.delete",
  "classification": "destructive",
  "outcome": "success",
  "traceId": "trace-7e8f9a0b1c2d",
  "duration": 142,
  "metadata": {
    "toolName": "file_delete",
    "path": "build/old.tar.gz",
    "approvalId": "appr-abc-123"
  },
  "msg": "Audit event"
}
```

Note that the `metadata` object never contains the secret values themselves --
only the *names* of secrets that were accessed and the paths involved. The
`msg` field is constant (`"Audit event"`) so structured queries key off the
`audit` boolean and the typed fields above.

## Viewing Audit Logs

There are four ways to access your audit events, depending on your needs.

### Query the durable audit (CLI / RPC / agent tool)

The audit is a **durable, queryable** store (the `obs_audit_events` SQLite table).
Query it directly — filter by kind, classification, agent, tenant, outcome, and a
time window — through any of three admin-gated surfaces backed by the same
`obs.audit.query` RPC:

```bash theme={}
# CLI — the durable security-decision audit log (admin-scoped)
comis security audit-log --kind secret_access --agent customer-support --limit 20
comis security audit-log --outcome denied --format json
```

The same query is available to an admin agent via the `obs_query` tool
(`{action:"audit", filters:{kind?, classification?, agentId?, tenant?, outcome?,
since?, until?, limit?}}`) and over JSON-RPC as `obs.audit.query`. All three
return **content-free** rows — ids, the closed-enum fields, and a scrubbed `refs`
blob; never a secret value. This is the best option for "what security-relevant
actions ran, by whom, and how many" — including after a restart.

### Web Dashboard

The easiest way to view audit events. Open the [Security dashboard](/web-dashboard/security-view) and click the Audit Log tab. You can filter by agent, action, classification, and time range.

The dashboard displays events in reverse chronological order, so you always see the most recent activity first. Click any event row to expand it and see the full event details including the trace ID, which you can use to correlate the event with other log entries from the same request.

This is the best option for quick investigations or spot-checking agent behavior.

### Structured Log Files

Audit events are written to the dedicated `~/.comis/logs/security-audit.jsonl`
file — one scrubbed JSON line per event (`0600`, rotated under
`observability.logRotation`) — and ALSO appear as `.audit()`-level (35) lines in
the standard structured log (`~/.comis/logs/daemon.*.log`). Each is a single JSON
line, easy to parse with standard tools:

```bash theme={}
# Tail the dedicated audit trail (the durable, greppable JSONL)
tail -20 ~/.comis/logs/security-audit.jsonl

# Or filter the audit-level lines out of the structured daemon log
grep '"level":35' ~/.comis/logs/daemon.*.log | tail -20
```

This is useful when you need to search through events on the command line or when
the web dashboard is not available. For a structured, filterable query, prefer
`comis security audit-log` (above) — it reads the same events from the durable
`obs_audit_events` table.

### Log Aggregation

For production deployments, forward Comis logs to your preferred log aggregation service -- Datadog, Elasticsearch, Grafana Loki, Splunk, or similar. The structured JSON format makes it straightforward to create dashboards, alerts, and retention policies based on audit events.

Common aggregation patterns include:

* **Alert on denied actions** -- Get notified when agents are blocked from performing actions
* **Track destructive action frequency** -- Monitor how often agents attempt high-risk operations
* **Compliance reporting** -- Generate periodic reports of all security-relevant activity

<Tip>
  Start with the web dashboard for quick checks. Move to log aggregation when you need historical analysis, alerts, or compliance reporting.
</Tip>

## Event Aggregation (Sliding-Window Dedup)

When the same event fires many times in quick succession -- for example, a user repeatedly triggering jailbreak detection -- Comis aggregates these events using a **sliding 60-second window per event source**. Instead of flooding your logs with hundreds of identical entries, you see a single aggregated event with a count. This keeps your audit log readable without losing information.

Events are bucketed by **source type** -- `user_input`, `tool_output`, `external_content`, `memory_write` -- so a flood of jailbreak detections in user input does not suppress a separate flood in memory writes. The first event in a window opens a bucket and arms a `setTimeout`; subsequent events in the same window increment the count and add their unique patterns (up to 10 per summary). When the timer fires, a single `security:injection_detected` summary event is emitted with the accumulated count and patterns.

Aggregated summaries include:

* The source type (user\_input, tool\_output, external\_content, memory\_write)
* A count of how many times the event occurred in the window
* Up to 10 unique patterns that triggered the detection
* The time window over which events were grouped

Timers use `unref()` so a pending aggregation does not block daemon shutdown, and the aggregator's `flush()` method emits all pending summaries immediately on demand. This deduplication is automatic and requires no configuration.

## Storage and Retention

Every audit event is persisted to a **durable, queryable** store and also
emitted on the in-memory bus for real-time consumers. The two durable sinks that
survive a daemon restart are the obs\_audit\_events SQLite table (the queryable
store of record) and the security-audit.jsonl file (the greppable on-disk trail);
the daemon log carries the same event as a structured `.audit()`-level line but is
not the store of record:

| Location                                              | Format                                                                                                                                 | Use case                                                                                                                                             |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`obs_audit_events` table** (SQLite, in `~/.comis/`) | One row per event (content-free: ids, closed-enum kind/classification/outcome/severity, a scrubbed `refs` blob — never a secret value) | The durable store of record; survives restart; queryable via `obs_query {action:"audit"}`, the `obs.audit.query` RPC, and `comis security audit-log` |
| **`~/.comis/logs/security-audit.jsonl`**              | One scrubbed JSON line per event (`0600`, rotated under `observability.logRotation`)                                                   | Greppable on-disk audit trail; survives restart; the 6th observability stream                                                                        |
| **`~/.comis/logs/daemon.*.log`**                      | A `.audit()`-level (35) structured line per event (Pino)                                                                               | Rides the standard structured log; ingested by log-aggregation tools                                                                                 |
| **In-memory event bus**                               | TypedEventBus emission                                                                                                                 | Real-time consumption by the web dashboard, RPC subscribers, and the audit aggregator                                                                |

The `obs_audit_events` rows and the `security-audit.jsonl` lines are the
authoritative durable record — they persist across restarts independent of any
external log stack. Comis does not impose a retention policy on the JSONL/log
files beyond the size-based rotation configured under `observability.logRotation`;
SQLite-row retention follows `observability.persistence.retentionDays`. The
aggregator's in-memory state is only the current sliding window; once a summary
fires, the bucket is freed.

## Trace Correlation

Every audit event includes a Trace ID that connects it to the full request lifecycle. If an agent execution generates multiple audit events -- for example, reading a secret, calling a tool, and sending a message -- all of those events share the same Trace ID. This makes it straightforward to reconstruct what happened during a single agent execution, even when reviewing logs from a production system with many agents running simultaneously.

When investigating an incident, start with the audit event that caught your attention, note its Trace ID, and filter your logs for that ID to see the complete picture.

```bash theme={}
# Find all audit events for a specific trace (the durable JSONL audit trail)
grep 'trace-abc-123' ~/.comis/logs/security-audit.jsonl
```

This is especially valuable when debugging approval workflows, where a single user action may trigger classification, confirmation, approval, and execution events across multiple agents.

## Log Redaction

Comis automatically scrubs sensitive values from all log output, including audit events. If an API key, password, JWT token, or other credential accidentally appears in a log message, it is replaced with `[REDACTED]` before being written. This prevents credentials from ending up in log files, log aggregation dashboards, or error reports.

Log redaction works alongside audit logging -- audit events record what actions were taken without exposing the sensitive data involved in those actions. For example, an audit event records that an agent accessed a secret named `OPENAI_API_KEY`, but the actual key value never appears in the log.

## Credential Broker Events

The credential broker emits 7 typed events on the daemon event bus. All event payloads are redaction-by-construction — no secret value ever appears in any event field.

| Event                           | Key payload fields                                                            |
| ------------------------------- | ----------------------------------------------------------------------------- |
| `broker:session_opened`         | `sessionId`, `agentId`, `host`, `presetId?`, `timestamp`                      |
| `broker:session_closed`         | `sessionId`, `agentId`, `durationMs`, `reason`, `timestamp`                   |
| `broker:request`                | `sessionId`, `host`, `path`, `method`, `timestamp`                            |
| `broker:injected`               | `sessionId`, `host`, `ruleKind`, `timestamp`                                  |
| `broker:denied`                 | `sessionId`, `host`, `reason`, `statusCode`, `timestamp`                      |
| `broker:credential_unavailable` | `sessionId`, `secretRef`, `agentId`, `timestamp`                              |
| `broker:egress_blocked`         | `sessionId`, `targetHostHash` (SHA-256 hex — NOT plaintext host), `timestamp` |

The `broker:egress_blocked` event deliberately carries `targetHostHash` (SHA-256 hex) rather than the plaintext hostname — even blocked egress targets are not logged in plaintext (redaction-by-construction).

The `secret:accessed` event is also emitted by the broker for each per-request SecretManager resolution, separately from the `broker:*` taxonomy. Fields: `secretName`, `agentId`, `outcome` (success/denied/not\_found), `timestamp`. This lets you audit which secrets an agent touched. The durable audit trail records `success` reads (a secret was actually accessed) and `denied` reads (a blocked access — the security signal); a routine `not_found` read (an optional-key probe that resolved to nothing — no access occurred, and re-emitted every turn) is **not** persisted, so the trail stays legible instead of being dominated by per-turn probe noise.

<Info>
  Source: `packages/infra/src/credential-broker/broker-events.ts`
</Info>

For the full broker architecture and configuration, see [Credential Broker →](/security/credential-broker).

## Configuration

Audit logging is controlled by two settings in your configuration:

```yaml title="~/.comis/config.yaml" theme={}
security:
  auditLog: true         # Enable audit event logging (default: true)
  logRedaction: true     # Scrub credentials from log output (default: true)
```

| Setting                 | Default | Description                           |
| ----------------------- | ------- | ------------------------------------- |
| `security.auditLog`     | `true`  | Enable audit event logging            |
| `security.logRedaction` | `true`  | Scrub credentials from all log output |

<Info>
  Audit logging is enabled by default. You generally should not disable it, especially in production. If you need to reduce log volume, consider using log aggregation with filtering rather than turning off audit events entirely.
</Info>

## Related

<CardGroup cols={2}>
  <Card title="Security Dashboard" icon="desktop" href="/web-dashboard/security-view">
    View audit logs in the web UI
  </Card>

  <Card title="Defense in Depth" icon="shield-halved" href="/security/defense-in-depth">
    All security layers that generate audit events
  </Card>

  <Card title="Hardening" icon="lock" href="/security/hardening">
    Verify audit logging in the hardening checklist
  </Card>

  <Card title="Logging" icon="file-lines" href="/operations/logging">
    General logging configuration
  </Card>
</CardGroup>
