# Storage, redaction and audit

This page explains where Doberman's decision history lives, what it contains, and how you can forward it somewhere else.

Every decision Doberman makes gets written to a local SQLite database under `.doberman/` in the repo it protects. That directory is never committed (add it to `.gitignore` if you haven't already), and nothing leaves your machine unless you wire up a sink. `doberman log`, `doberman tui`, and `doberman status` all read from this one local store.

## Redaction happens before the write

The redaction boundary sits before the database, not after. By the time a row reaches storage, it has already been reduced to a path class, a reason code, a verdict, and an HMAC-SHA256 fingerprint where a value needs to be recognized again without being stored. The key behind that fingerprint is generated locally, kept at `0600` (or in your OS keyring), and never checked in or logged. A raw secret, a full file path outside its class, a prompt, or file contents never reach the log at all.

> **Never** No raw secret, prompt, or file content is ever written to `.doberman/`, forwarded to a sink, or printed by any surface. If you see one, that is a bug worth reporting.

The same database holds more than the decision log: single-use, action-bound approvals (`approvals.py`), the MCP tool-schema pins that catch a downstream server changing its tool definitions out from under you (`tool_pins.py`), sticky session-taint state (`taint.py`, cleared with the gated `doberman taint clear`), and the cost and device metrics that back `doberman session-summary`.

## The audit sink seam

Everything above stays local by default. If you want a copy elsewhere, Doberman ships a plugin seam: an `AuditSink` registered through the `doberman.audit_sinks` entry point receives the same already-redacted rows the local database gets, nothing more. Two sinks ship built in: a webhook sink and an OpenTelemetry sink, both config-gated and off unless you turn them on.

```mermaid
flowchart LR
  D["Decision"] --> RED["redact:<br/>path classes · reason codes ·<br/>HMAC fingerprints only"]
  RED --> LOG["log.py<br/>record_decision / read_decisions"]
  LOG --> DB[("db.py<br/>SQLite")]
  FP["fingerprint.py<br/>keyed HMAC-SHA256"] --> RED
  TA["taint.py<br/>secret-egress taint state"] --> DB
  AP["approvals.py<br/>single-use, action-bound"] --> DB
  TP["tool_pins.py<br/>MCP schema TOFU pins"] --> DB
  CO["cost.py / device_metrics.py<br/>heartbeat.py"] --> DB
  LOG --> SK["sinks.py<br/>AuditSink seam (entry points)"]
  SK --> WH["WebhookAuditSink"]
  SK --> OT2["OtelAuditSink"]
  classDef amber fill:#b45309,color:#fff,stroke:#b45309
  class RED amber
```

The redaction boundary sits before the write; sinks and the local database receive the same already-redacted rows.

Logging never takes part in the decision: a sink that is slow, unreachable, or misconfigured cannot stall or crash the decision path, and nothing in storage can alter a verdict after the engine has decided it. If you need a plugin seam beyond the two built-in sinks, see [Plugins](https://docs.trydoberman.dev/guides/plugins/); to wire up the OpenTelemetry sink specifically, see [Audit and OpenTelemetry](https://docs.trydoberman.dev/guides/audit-otel/).
