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

# Audit log

> Publish transcripts and egress events to your self-hosted backend.

Airlock's audit trail starts as **two local logs** answering different
questions, and `airlock publish` ships **both** to the same self-hosted store —
`airlockd` from the [deploy bundle](/deploy) — where they become one
per-user record.

| Log             | Question it answers                       | Written by                                        |
| --------------- | ----------------------------------------- | ------------------------------------------------- |
| Egress audit    | Which hosts did the sandbox try to reach? | The proxy, to `~/.airlock/logs/<repo>/`           |
| Chat transcript | What did the agent read, say, and do?     | The agent, to `~/.airlock/agent/claude/projects/` |

There is no third-party service in this path: events land in the Postgres
you operate.

## No schema setup

The backend applies its own embedded migrations when it starts. There is no
table to create and no SQL to run — `docker compose up` then
`airlock publish` is the whole setup.

## Identity: login and API tokens

Publishing is authenticated and identity-bound:

1. Your admin creates your account in the [console](/dashboard#user-management)
   (Airlock's own user table — no external IdP required).
2. [`airlock login`](/cli#airlock-login) exchanges the backend URL, your
   email, and your password for an ingest-scoped API token, cached in
   `~/.airlock/auth.json` (mode `0600`). The backend stores only the token's
   hash.
3. Every publish carries the token, and **the server attributes each event to
   the token's user**. A batch claiming any other identity is rejected. A
   token cannot write someone else's history, and an ingest token cannot read
   anyone's timeline.

The api also records a server-assigned `received_at` beside each event's
client-reported timestamp, so backdating is visible rather than silent.

## What gets shipped

`airlock publish` collects two sources into the same event stream:

**Sandbox transcripts** — each user or assistant message in
`~/.airlock/agent/claude/projects/<project>/<session>.jsonl` becomes one event:

| Field        | Contents                                           |
| ------------ | -------------------------------------------------- |
| `dedupe key` | The transcript event's own id — resends are no-ops |
| `session_id` | The transcript's session id                        |
| `connector`  | `sandbox`                                          |
| `action`     | `prompt` (user) or `response` (assistant)          |
| `target_id`  | The repo, derived from the project directory name  |
| `ts`         | Event timestamp from the transcript                |
| `content`    | Rendered plain text (see below)                    |
| `raw`        | The verbatim transcript line, unmodified           |

`content` is a readable flattening of the message. What the dashboard displays
and what detection scans:

* Text blocks are included as-is.
* Tool calls become `[tool_use <name> <first 200 chars of input>]`.
* Tool results become `[tool_result <first 300 chars>]`.
* **Thinking blocks are omitted.**
* The whole field is capped at 8,000 characters.

**Proxy egress logs** — each line in
`~/.airlock/logs/<repo>/audit-*.jsonl` becomes one access event:

| Field                          | Contents                                 |
| ------------------------------ | ---------------------------------------- |
| `user_id`                      | Who was signed in when the proxy started |
| `repo`                         | The repo whose sandbox made the attempt  |
| `host`, `method`, `path`       | What it tried to reach                   |
| `allowed`                      | The verdict                              |
| `bytes_sent`, `bytes_received` | Volume through an allowed tunnel         |
| `duration_ms`                  | How long the connection lived            |

A third kind — **decision events** from agents running outside any sandbox —
arrives over the [push API](/decision-events) rather than through `publish`,
and lands in the same store and timeline.

Together: the conversation in which a document was read, and the network
attempts the same session made.

## Publish

<Steps>
  <Step title="Preview without sending">
    ```sh theme={null}
    airlock publish --dry-run
    ```

    Prints per-source event and session counts and sample rows. Touches no
    network. Also available as `-n`.
  </Step>

  <Step title="Publish">
    ```sh theme={null}
    airlock publish
    ```

    Requires [`airlock login`](/cli#airlock-login) first — without it, publish
    stops with `not logged in`.
  </Step>

  <Step title="Or stream continuously">
    ```sh theme={null}
    airlock publish --watch
    ```

    Polls both sources every 3 seconds and republishes any file whose size
    changed, so the dashboard follows a session while it is still running.
    Also available as `-w`. Ctrl-C to stop.
  </Step>
</Steps>

## Re-running is safe

Ingest is **insert-only and idempotent**: every event carries a per-source
dedupe key, and an event already received is skipped, never rewritten.
Publishing the same session twice produces no duplicates, and a re-publish
cannot alter history.

If a batch fails under `--watch`, that file's checkpoint is left stale
deliberately, so the next tick retries it rather than skipping the events.

## Credentials

Your identity lives in `~/.airlock/auth.json` (mode `0600`), written by
[`airlock login`](/cli#airlock-login): the backend URL and an ingest-scoped
API token. It stays on your host: `publish` uses it directly, it is never
injected into a sandbox, and the sandbox's allowlist does not include the
backend — the agent being audited cannot reach its own audit trail. Connector
credentials stay separately in `~/.airlock/secrets.toml`; see
[configuration](/configuration#secrets).
