> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rafter.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit Log

> Stable JSONL schema for the Rafter security audit log

# Audit Log

Rafter writes a security audit log to `~/.rafter/audit.jsonl` in [JSONL](https://jsonl.org/) format (newline-delimited JSON). Every security-relevant action — policy enforcement decisions, secret detections, overrides — is recorded as a single JSON object per line.

Both the Node and Python CLIs write to the same file using the same schema. The Node CLI is the reference implementation; the Python CLI currently emits `command_intercepted` and `secret_detected` events only.

## File Location

```
~/.rafter/audit.jsonl
```

The directory `~/.rafter/` is created automatically on first use.

***

## Schema

Every audit log entry contains these fields:

### Base Fields

| Field       | Type   | Required | Description                                               |
| ----------- | ------ | -------- | --------------------------------------------------------- |
| `timestamp` | string | yes      | ISO 8601 UTC timestamp (e.g., `2026-02-20T10:30:45.123Z`) |
| `sessionId` | string | yes      | Unique per CLI invocation. Format: `{epoch_ms}-{random}`  |
| `eventType` | string | yes      | Event type identifier (see [Event Types](#event-types))   |
| `agentType` | string | no       | AI agent platform: `"openclaw"` or `"claude-code"`        |

### `action` Object

Present on most events. Contains context about what triggered the event.

| Field       | Type   | Required | Description                                    |
| ----------- | ------ | -------- | ---------------------------------------------- |
| `command`   | string | no       | Shell command string                           |
| `tool`      | string | no       | Tool name (e.g., `Write`, `Bash`)              |
| `riskLevel` | string | no       | `"low"`, `"medium"`, `"high"`, or `"critical"` |

### `securityCheck` Object

Always present. Records the outcome of the security evaluation.

| Field     | Type    | Required | Description                          |
| --------- | ------- | -------- | ------------------------------------ |
| `passed`  | boolean | yes      | Whether the security check passed    |
| `reason`  | string  | no       | Human-readable explanation           |
| `details` | object  | no       | Structured metadata (event-specific) |

### `resolution` Object

Always present. Records what action was taken.

| Field            | Type   | Required | Description                                               |
| ---------------- | ------ | -------- | --------------------------------------------------------- |
| `actionTaken`    | string | yes      | `"blocked"`, `"allowed"`, `"overridden"`, or `"redacted"` |
| `overrideReason` | string | no       | User-provided justification (only on `policy_override`)   |

***

## Event Types

### `command_intercepted`

Emitted when a shell command is evaluated against the security policy.

| Field                    | Value                                                        |
| ------------------------ | ------------------------------------------------------------ |
| `action.command`         | The shell command string                                     |
| `action.riskLevel`       | Dynamically assessed: `low`, `medium`, `high`, or `critical` |
| `securityCheck.passed`   | `true` if allowed, `false` if blocked                        |
| `securityCheck.reason`   | Why the command was blocked/flagged                          |
| `resolution.actionTaken` | `"allowed"`, `"blocked"`, or `"overridden"`                  |

**Example:**

```json theme={null}
{
  "timestamp": "2026-02-20T10:30:45.123Z",
  "sessionId": "1740047445123-k8f2m",
  "eventType": "command_intercepted",
  "agentType": "claude-code",
  "action": {
    "command": "git push --force",
    "riskLevel": "high"
  },
  "securityCheck": {
    "passed": false,
    "reason": "High-risk command requires approval"
  },
  "resolution": {
    "actionTaken": "blocked"
  }
}
```

### `secret_detected`

Emitted when a secret is found in files, staged content, or tool output.

| Field                    | Value                                    |
| ------------------------ | ---------------------------------------- |
| `action.riskLevel`       | Always `"critical"`                      |
| `securityCheck.passed`   | Always `false`                           |
| `securityCheck.reason`   | `"{secret_type} detected in {location}"` |
| `resolution.actionTaken` | `"blocked"` or `"allowed"`               |

The audit log **never contains the raw secret value**—only the type (e.g., "AWS Access Key") and location (e.g., "staged files", "config.js").

**Example:**

```json theme={null}
{
  "timestamp": "2026-02-20T10:25:12.456Z",
  "sessionId": "1740047445123-k8f2m",
  "eventType": "secret_detected",
  "agentType": "openclaw",
  "action": {
    "riskLevel": "critical"
  },
  "securityCheck": {
    "passed": false,
    "reason": "AWS Access Key detected in config.js"
  },
  "resolution": {
    "actionTaken": "blocked"
  }
}
```

### `content_sanitized`

Emitted when sensitive patterns are redacted from output.

| Field                    | Value                                            |
| ------------------------ | ------------------------------------------------ |
| `securityCheck.passed`   | Always `false`                                   |
| `securityCheck.reason`   | `"{n} sensitive patterns detected"`              |
| `securityCheck.details`  | `{ "contentType": "...", "patternsMatched": n }` |
| `resolution.actionTaken` | Always `"redacted"`                              |

**Example:**

```json theme={null}
{
  "timestamp": "2026-02-20T11:00:00.000Z",
  "sessionId": "1740047445123-k8f2m",
  "eventType": "content_sanitized",
  "securityCheck": {
    "passed": false,
    "reason": "3 sensitive patterns detected",
    "details": {
      "contentType": "shell_output",
      "patternsMatched": 3
    }
  },
  "resolution": {
    "actionTaken": "redacted"
  }
}
```

### `policy_override`

Emitted when a user explicitly overrides a security policy (e.g., `--force` flag).

| Field                       | Value                                  |
| --------------------------- | -------------------------------------- |
| `action.command`            | The overridden command (optional)      |
| `action.riskLevel`          | Always `"high"`                        |
| `securityCheck.passed`      | Always `false`                         |
| `securityCheck.reason`      | `"Security policy overridden by user"` |
| `resolution.actionTaken`    | Always `"overridden"`                  |
| `resolution.overrideReason` | User-provided reason string            |

### `scan_executed`

Reserved for future use. Will be emitted when file scans are performed.

### `config_changed`

Reserved for future use. Will be emitted when security configuration is modified.

***

## Redaction Behavior

The audit log is designed to be safe to retain and share:

* **Secret values are never logged.** `secret_detected` events record the secret type and file location, not the secret itself.
* **Content is not stored.** `content_sanitized` events record pattern counts and content types, not the raw content.
* **Commands are logged verbatim.** `command_intercepted` (policy enforcement) events include the full command string. If commands contain sensitive arguments, they appear in the log.

***

## Size and Rotation

* **No automatic rotation.** The log file grows unbounded until cleanup runs.
* **Time-based retention:** Entries older than `retentionDays` are purged when `cleanup()` is called.
* **No automatic scheduling.** Cleanup must be triggered manually or via the API.
* **Default retention:** 30 days.

***

## Configuration

Configure audit logging in `~/.rafter/config.json` under `agent.audit`, or in `.rafter.yml` under `audit`:

| Key             | Type    | Default  | Description                                            |
| --------------- | ------- | -------- | ------------------------------------------------------ |
| `logAllActions` | boolean | `true`   | Master switch. If `false`, no events are written.      |
| `retentionDays` | number  | `30`     | Days to retain entries before cleanup purges them.     |
| `logLevel`      | string  | `"info"` | Stored in config but not currently used for filtering. |

**Config file example:**

```json theme={null}
{
  "agent": {
    "audit": {
      "logAllActions": true,
      "retentionDays": 90,
      "logLevel": "info"
    }
  }
}
```

**Policy file example (`.rafter.yml`):**

```yaml theme={null}
audit:
  retention_days: 90
  log_level: info
```

***

## Webhook Notifications

When configured, the audit logger sends a POST request to a webhook URL for events at or above a minimum risk level. Works with Slack incoming webhooks, Discord webhooks, and generic HTTP endpoints.

### Configuration

| Key                                | Type   | Default  | Description                                                           |
| ---------------------------------- | ------ | -------- | --------------------------------------------------------------------- |
| `agent.notifications.webhook`      | string | —        | Webhook URL to POST notifications to                                  |
| `agent.notifications.minRiskLevel` | string | `"high"` | Minimum risk level to trigger notification (`"high"` or `"critical"`) |

### Webhook Payload

```json theme={null}
{
  "event": "command_intercepted",
  "risk": "high",
  "command": "git push --force",
  "timestamp": "2026-02-21T10:30:45.123Z",
  "agent": "claude-code",
  "text": "[rafter] high-risk event: command_intercepted — git push --force",
  "content": "[rafter] high-risk event: command_intercepted — git push --force"
}
```

The `text` field provides Slack compatibility. The `content` field provides Discord compatibility. Both contain a human-readable summary.

Webhook delivery is fire-and-forget with a 5-second timeout. Failures are silently ignored to avoid disrupting audit logging.

### Setup

```bash theme={null}
# Configure webhook URL
rafter agent config set agent.notifications.webhook https://hooks.slack.com/services/T.../B.../xxx

# Only notify on critical events
rafter agent config set agent.notifications.minRiskLevel critical

# Disable notifications
rafter agent config set agent.notifications.webhook ""
```

***

## Querying the Audit Log

Use `rafter agent audit` to view and filter entries:

```bash theme={null}
# Show last 10 entries (default)
rafter agent audit

# Show last 50 entries
rafter agent audit --last 50

# Filter by event type
rafter agent audit --event secret_detected

# Filter by agent
rafter agent audit --agent claude-code

# Entries since a specific date
rafter agent audit --since 2026-02-01

# Combine filters
rafter agent audit --event command_intercepted --agent openclaw --last 100
```

Or query the JSONL file directly with standard tools:

```bash theme={null}
# Count blocked commands
grep '"actionTaken":"blocked"' ~/.rafter/audit.jsonl | wc -l

# Find all secret detections with jq
jq 'select(.eventType == "secret_detected")' ~/.rafter/audit.jsonl

# Events from the last 24 hours
jq --arg cutoff "$(date -u -v-1d +%Y-%m-%dT%H:%M:%S)" \
  'select(.timestamp > $cutoff)' ~/.rafter/audit.jsonl
```

***

## MCP Access

The `read_audit_log` MCP tool exposes audit log entries to MCP clients:

```json theme={null}
{
  "tool": "read_audit_log",
  "arguments": {
    "event_type": "secret_detected",
    "limit": 20
  }
}
```

See [MCP Integration](/guides/agent-security/mcp-integration) for setup.
