# CLI reference

> tapes commands and flags: init, local, serve, status, auth, config, backfill, raw equivalence, dev, and version. tapesctl is the client, not this binary.

- Repository: papercomputeco/tapes
- GitHub: https://github.com/papercomputeco/tapes
- Human docs: https://grok-wiki.com/public/docs/papercomputeco-tapes-020596f21750
- Complete Markdown: https://grok-wiki.com/public/docs/papercomputeco-tapes-020596f21750/llms-full.txt

## Source Files

- `cmd/tapes/tapes.go`
- `cmd/tapes/serve/serve.go`
- `cmd/tapes/config/config.go`
- `cmd/tapes/local/local.go`
- `cmd/tapes/backfill/backfill.go`
- `cmd/tapes/dev/dev.go`
- `docs/cli.md`

---

---
title: "CLI reference"
description: "tapes commands and flags: init, local, serve, status, auth, config, backfill, raw equivalence, dev, and version. tapesctl is the client, not this binary."
---

`tapes` is the server binary. It starts the capture proxy, read API, private ingest API, and workers; owns PostgreSQL; and carries operator tooling (`init`, `local`, `config`, `auth`, `backfill`, `raw`, `dev`). Capturing a session and reading one back belong to [`tapesctl`](https://github.com/papercomputeco/tapesctl). Run `tapes <command> --help` for the flag list that matches the installed binary.

```text
tapes                         tapesctl (separate repo)
─────────────────────         ────────────────────────
init / local / serve          start / capture / sync
status / auth / config        sessions / export / seed
backfill / raw / dev          search / skill / generate
version                       version
owns Postgres + services      HTTP client against :8081 / :8082
```

<Note>
`tapes serve` listens on `:8080` (proxy), `:8081` (read API), and `:8082` (ingest) by default. `tapesctl start` / `capture` / `sync` talk to ingest (`:8082`). List, export, search, and seed talk to the read API (`:8081`).
</Note>

## Command inventory

| Command | Role |
| --- | --- |
| `tapes init [--preset ...]` | Create a project-local `.tapes/` and write `config.toml`. |
| `tapes local [up\|status\|down]` | Docker-bootstrap PostgreSQL and Ollama. Bare `tapes local` is `up`. |
| `tapes serve` | Run proxy, read API, ingest, in-process derive worker, and optional embed worker. |
| `tapes serve proxy\|api\|ingest\|derive-worker\|embed-worker` | Run one process. |
| `tapes status` | Print resolved `.tapes/`, provider, storage, and API reachability. |
| `tapes auth` | Store OpenAI or Anthropic keys in `.tapes/credentials.toml`. |
| `tapes config get\|set\|list` | Read and write dotted keys in `config.toml`. |
| `tapes backfill wire-trace\|transcripts` | Replay capture artifacts through ingest. |
| `tapes raw equivalence` | Read-only proof that stored bytes re-reduce to the stored reduction. |
| `tapes dev` | Corpus fixtures, re-derive, embed backfill, OpenAPI compile. |
| `tapes version` | Print `Version`, `Sha`, and `Built at`. |

## Global flags

These persist on every `tapes` command.

<ParamField body="--config-dir" type="string">
Override the `.tapes/` directory. Resolution otherwise is cwd `.tapes/`, then `~/.tapes/`, then built-in defaults.
</ParamField>

<ParamField body="--log-level" type="string">
Minimum level: `debug`, `info`, `warn`, `error`. Default `info`.
</ParamField>

<ParamField body="--log-format" type="string">
`auto`, `console`, `text`, or `json`. Default `auto`.
</ParamField>

<ParamField body="--log-color" type="string">
`auto`, `always`, or `never`. Default `auto`.
</ParamField>

<ParamField body="--disable-telemetry" type="boolean">
Disable anonymous usage telemetry (`telemetry.disabled` / `TAPES_TELEMETRY_DISABLED`). Telemetry is also skipped in CI and never blocks a command if init fails.
</ParamField>

<ParamField body="--disable-update-check" type="boolean">
Hidden. Disables the 2s-capped pre-run version check (`update.disabled` / `TAPES_UPDATE_DISABLED`).
</ParamField>

`--debug` / `-d` is deprecated; use `--log-level=debug`.

Bound settings use flag → `TAPES_…` environment variable → `config.toml` → default. Dots become underscores: `proxy.listen` is `TAPES_PROXY_LISTEN`.

## `tapes init`

Creates `./.tapes/` in the current working directory and writes `config.toml`. That directory wins over `~/.tapes/` for later commands.

```bash
tapes init
tapes init --preset openai
tapes init --preset anthropic
tapes init --preset ollama
tapes init --preset https://example.com/config.toml
```

<ParamField body="--preset" type="string">
`openai`, `anthropic`, `ollama`, or an `http://` / `https://` URL of raw TOML. Empty writes `NewDefaultConfig()` (provider `ollama`, upstream `http://localhost:11434`).
</ParamField>

| Preset | `proxy.provider` | `proxy.upstream` |
| --- | --- | --- |
| `openai` | `openai` | `https://api.openai.com` |
| `anthropic` | `anthropic` | `https://api.anthropic.com` |
| `ollama` | `ollama` | `http://localhost:11434` |

Unknown names fail with `unknown preset`. A remote URL must return HTTP 200 and parse as TOML.

## `tapes local`

Requires Docker on `PATH`. Starts PostgreSQL (pgvector + pg_duckdb) and satisfies Ollama for embeddings.

| Subcommand | Action |
| --- | --- |
| `tapes local` / `tapes local up` | Create network `tapes-local`, start containers, wait for ready, persist DSN + embedding settings. |
| `tapes local status` | Show `tapes-local-postgres` / `tapes-local-ollama`, or native Ollama at `http://127.0.0.1:<port>`. |
| `tapes local down` | `docker rm -f` both containers. |
| `tapes local down --wipe` | Also delete `.tapes/postgres/` (all captured sessions). |

Persistent flags on the group:

| Flag | Default |
| --- | --- |
| `--postgres-port` | `5432` |
| `--ollama-port` | `11434` |
| `--postgres-image` | `public.ecr.aws/g4e5l3z3/papercomputeco/postgres:17.7-pgduckdb-1.1.1` |
| `--ollama-image` | `ollama/ollama:latest` |
| `--docker-ollama` | `false` — force the Ollama container even if native `ollama` exists |

Local Postgres identity is user `tapes`, password `tapes`, database `tapes`. Data lives under the resolved `.tapes/postgres` directory. Successful `up` writes:

- `storage.postgres_dsn` = `postgres://tapes:tapes@localhost:<port>/tapes?sslmode=disable`
- `vector_store.target` = same DSN
- `proxy.upstream` and `embedding.target` = the resolved Ollama URL
- `embedding.provider` = `ollama`
- `embedding.model` = `embeddinggemma`

Ollama planning: reuse a serving native install; if `ollama` is installed but not listening, print start + `ollama pull embeddinggemma:latest`; otherwise run `tapes-local-ollama` and pull that model. Native is preferred because a container on the same port can shadow loopback IPv6 (`localhost` → `::1`).

<Warning>
`--wipe` is irreversible. It removes the local Postgres data directory, not just the containers.
</Warning>

<RequestExample>
```bash
tapes local up
tapes local status
```
</RequestExample>

## `tapes serve`

Combined local stack. SIGINT / SIGTERM cancel the process; HTTP servers close on the way out.

Default listen addresses:

| Process | Address | Flag on `tapes serve` |
| --- | --- | --- |
| Capture proxy | `:8080` | `--proxy-listen` / `-p` |
| Read API | `:8081` | `--api-listen` / `-a` |
| Ingest | `:8082` | `--ingest-listen` / `-i` |

In-process workers (local convenience only):

- Derive worker at **2s** debounce (standalone default is 20s).
- Embed worker at **10s** interval when `--embed-spans` is true (standalone default is `1m`). Embedding setup failures log and disable search; they do not fail the stack.

<ParamField body="--provider" type="string">
`anthropic`, `openai`, or `ollama`. Config key `proxy.provider`. Default `ollama`.
</ParamField>

<ParamField body="--upstream" type="string">
Upstream LLM base URL (`proxy.upstream`). Default `http://localhost:11434`.
</ParamField>

<ParamField body="--postgres" type="string">
PostgreSQL DSN (`storage.postgres_dsn`). Required for a working stack.
</ParamField>

<ParamField body="--project" type="string">
Session project tag (`proxy.project`). Empty → git repo name.
</ParamField>

<ParamField body="--api-web-ui" type="boolean">
Serve the minimal browser UI at `/` on the read API (`api.web_ui`). Default `false`.
</ParamField>

<ParamField body="--embed-spans" type="boolean">
Run the in-process embed loop. Default `true`. Disable with `--embed-spans=false`.
</ParamField>

<ParamField body="--cassettes" type="string[]">
Full cassette OpenAPI URLs (comma-separated or repeated). Also `TAPES_CASSETTES` or `cassettes = [...]` in TOML. Lifecycle and credentials stay operator-owned.
</ParamField>

<ParamField body="--cassette-refresh" type="duration">
How often to refresh cassette OpenAPI documents. Default `30s`.
</ParamField>

Also accepted: `--vector-store-target`, `--embedding-provider`, `--embedding-target`, `--embedding-model`, `--embedding-dimensions`. Unset `vector_store.target` falls back to `storage.postgres_dsn`. Embedding API keys come from `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` if set, else `.tapes/credentials.toml`.

```bash
tapes serve
tapes serve --provider anthropic --upstream https://api.anthropic.com
tapes serve --api-web-ui
tapes serve --embed-spans=false
tapes serve --cassettes http://127.0.0.1:8091/openapi
```

### Split processes

Standalone subcommands use `--listen` / `-l` instead of the parent `--*-listen` names.

| Command | Listens | Extra flags |
| --- | --- | --- |
| `tapes serve proxy` | `proxy.listen` | `--upstream`, `--provider`, `--postgres`, `--project`. Embedding flags accepted, **no effect**. |
| `tapes serve api` | `api.listen` | `--web-ui`, `--postgres`, embedding, `--skill-model`, `--cassettes`, `--cassette-refresh`. |
| `tapes serve ingest` | `ingest.listen` | `--postgres`, `--project`. Embedding flags accepted, **no effect**. |
| `tapes serve derive-worker` | optional `--metrics-listen` | `--poll-interval` (5s), `--debounce` (20s), `--sweep-interval` (1h), `--sweep-window` (24h; negative = all history), `--max-derive-lag` (45s), `--wait-for-db`. |
| `tapes serve embed-worker` | optional `--metrics-listen` | `--interval` (1m), `--batch-size`, `--max-text-bytes`, `--org`, `--wait-for-db`, embedding pair. |

Derive worker: ingest marks sessions dirty; the worker polls, debounces, and re-derives one session at a time under a per-session advisory lock. Extra replicas are safe. Embed worker is a separate failure domain so a slow embedding backend cannot stall derivation.

`--metrics-listen` serves `/metrics`, `/healthz`, `/readyz`, and `/ping`. SIGTERM/SIGINT drain the in-flight pass (30s); a second signal kills immediately. `--wait-for-db` retries an unreachable DSN instead of failing fast.

## `tapes status`

Prints the resolved `.tapes/` directory, `proxy.provider` → `proxy.upstream`, whether `storage.postgres_dsn` is set, `client.api_target`, and a 3s probe of the read API stats endpoint.

```bash
tapes status
tapes status --api-target http://localhost:8081
```

Unreachable API is non-fatal: the command prints a fail mark and suggests `tapes local up` then `tapes serve`. Reachable output includes session count, turn count, and captured cost.

## `tapes auth`

Stores keys used by **server-side** embedding and skill generation. The capture proxy is transparent: agents bring their own credentials; captured calls are forwarded as sent.

Supported providers: `openai`, `anthropic`. File: `.tapes/credentials.toml`.

```bash
tapes auth openai
tapes auth anthropic
echo "$KEY" | tapes auth openai
tapes auth --list
tapes auth --remove openai
```

Interactive input is hidden. Piped stdin uses the first line. Empty keys are rejected. `sk-proj-...` OpenAI keys print a warning; the help text prefers `sk-svcacct-...` with All permissions.

`tapes auth --list` prints each stored provider and its env override (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`). If the env var is already set, `APIKeyForProvider` does not load the file.

## `tapes config`

Reads and writes dotted keys in `.tapes/config.toml`. Flags still override file values at runtime.

```bash
tapes config list
tapes config get proxy.provider
tapes config set proxy.provider anthropic
tapes config set storage.postgres_dsn 'postgres://tapes:tapes@localhost:5432/tapes?sslmode=disable'
```

`get` / `set` reject unknown keys. `list` prints every valid key; empty values show as `<not set>`.

Valid dotted keys:

`proxy.provider`, `proxy.upstream`, `proxy.listen`, `proxy.project`, `api.listen`, `api.web_ui`, `ingest.listen`, `client.proxy_target`, `client.api_target`, `vector_store.provider`, `vector_store.target`, `embedding.provider`, `embedding.target`, `embedding.model`, `embedding.dimensions`, `opencode.provider`, `opencode.model`, `logging.level`, `logging.format`, `logging.color`, `storage.postgres_dsn`, `telemetry.disabled`, `update.disabled`.

<Note>
`cassettes` is a top-level TOML array / `--cassettes` / `TAPES_CASSETTES`. It is **not** a `tapes config set` key.
</Note>

Defaults that matter at the CLI:

| Key | Default |
| --- | --- |
| `proxy.listen` | `:8080` |
| `api.listen` | `:8081` |
| `ingest.listen` | `:8082` |
| `client.api_target` | `http://localhost:8081` |
| `embedding.provider` / `model` / `dimensions` | `ollama` / `embeddinggemma` / `768` |

## `tapes backfill`

Offline replay through ingest. Writes are idempotent: raw turns dedup on capture id; re-derive rebuilds the projection.

<Warning>
Both subcommands default `--ingest-url` to `http://127.0.0.1:8090`, not the combined-stack `:8082`. Point at the ingest you actually run.
</Warning>

### `tapes backfill wire-trace`

Replay paperd `turn-*` bundles (`request.json` + `response.sse` + `meta.json`) as the envelope `tapes-extproc` would POST to `/v1/ingest`.

| Flag | Default | Required |
| --- | --- | --- |
| `--dir` | — | yes — wire-trace directory |
| `--ingest-url` | `http://127.0.0.1:8090` | no |
| `--session` | all | no — repeatable harness session ids |
| `--dry-run` | `false` | parse/reduce, skip POST |
| `--verbose` / `-v` | `false` | log each turn |

Stdout: `scanned N: posted N, raw-only N, skipped N, failed N`. Non-zero exit if any turn failed.

### `tapes backfill transcripts`

Upload Claude Code `<session>.jsonl` files plus `subagents/` (including `meta.json` fork edges) to `{ingest-url}/v1/ingest/transcript`. Transcripts supply the causal/fork skeleton; wire capture supplies the call inventory.

| Flag | Default |
| --- | --- |
| `--dir` | required — Claude Code project directory |
| `--ingest-url` | `http://127.0.0.1:8090` |
| `--session` | all sessions in the directory |
| `--verbose` / `-v` | `false` |

Stdout: `sessions N, files N: uploaded N, deduped N, failed N`. Unchanged files dedup; grown transcripts append a new version.

## `tapes raw equivalence`

Read-only comparison of stored `raw_response` bytes against a server-side re-reduction of those bytes (`ingest.ReduceStoredRawTurn`). Used to prove the capture ratchet step from `dual` to `raw`.

The pool sets `default_transaction_read_only=on`, uses one connection, and **does not** run migrations. Only `source = 'wire'` rows are scanned. Attribution-repair joins are not applied. Response bodies are never printed — diffs are JSON paths plus value shapes.

```bash
tapes raw equivalence --since 24h --limit 5000
tapes raw equivalence \
  --postgres "postgres://user:pass@127.0.0.1:15432/tapes" \
  --since 24h --json
```

| Flag | Default |
| --- | --- |
| `--postgres` | `storage.postgres_dsn` (required) |
| `--since` | `24h` — Go duration or RFC 3339 instant |
| `--limit` | `1000` — newest first |
| `--session` | all harness session ids |
| `--json` | text report |
| `--max-diffs` | `10` per divergent turn |
| `--max-report` | `20` detailed blocking turns (counts stay exact) |

Missing DSN: `equivalence requires a postgres DSN`. Any blocking class exits non-zero (`raw-response equivalence failed`).

| Class | Blocks ratchet? |
| --- | --- |
| `equivalent` | no — supports the flip |
| `divergent` | yes |
| `undecodable` | yes — bytes would yield no reduction |
| `unreducible` | yes |
| `no_reducer` | yes |
| `skipped_no_raw` | no |
| `skipped_dropped` | no — bytes withheld or over limit |
| `skipped_no_reduction` | no — already raw-only |

Excluded from comparison (always reported):

- `created_at` — stamped at reduction time
- `usage.total_duration_ns` — wall clock only the live stream observer can measure

A window can be fully `equivalent` and still lose duration on the flip if stamp coverage for `usage.total_duration_ns` is `fallback` (no usable `meta.elapsed_seconds`). Check that line before ratcheting.

## `tapes dev`

Maintenance utilities. Not everyday operator workflow.

| Command | Touches DB? | Purpose |
| --- | --- | --- |
| `tapes dev trace-fixtures` | no | Replay gzipped JSONL corpus through the real deriver + API renderers. `--corpus` (repeatable), `--out` directory. Writes `session-traces-<s>.json`, `.slim.json`, `trace-summaries-<s>.json`, `trace-details-<s>.slim.json`. |
| `tapes dev dump-corpus` | yes | Inverse: export `raw_turns` to corpus files. Exactly one of `--session` (file `--out`) or `--all` (directory `--out`). `--org`, `--page-size` (500). |
| `tapes dev rederive` | yes | Same whole-project rebuild as `POST /v1/admin/derive/run`, as a direct DB call. `--postgres` required; `--project` optional. Session identity rows are ingest-written and skipped. |
| `tapes dev embed-spans` | yes | One-shot embed of eligible main-conversation `llm` spans. `--org`, `--batch-size`, embedding flags. Idempotent (span id + content hash). |
| `tapes dev openapi [api\|ingest]` | no | Compile the published contract with per-field Go doc comments. `--format yaml\|json`, `--docs-root` (default `.`; empty skips comments), `--out`. Default surface is `api`. |
| `tapes dev check-invariants <path>...` | no | Structural invariants on composite session-traces JSON. |
| `tapes dev check-openapi <path>...` | no | Type-check the same wire against compiled `SessionTracesResponse`. |

```bash
tapes dev openapi
tapes dev openapi ingest --format json --out ingest-contract.json
tapes dev rederive --postgres "$DSN"
tapes dev embed-spans --postgres "$DSN" \
  --embedding-provider ollama \
  --embedding-target http://localhost:11434 \
  --embedding-model embeddinggemma \
  --embedding-dimensions 768
```

`trace-fixtures` does not need Postgres. `dump-corpus`, `rederive`, and `embed-spans` require `--postgres` or `storage.postgres_dsn`.

## `tapes version`

```text
Version: <utils.Version>
Sha: <utils.Sha>
Built at: <utils.Buildtime>
```

Used as the install smoke test after `curl -fsSL https://download.tapes.dev/install | bash`.

## Client commands live in tapesctl

These commands are **not** on `tapes`:

| Moved to tapesctl | Removed entirely |
| --- | --- |
| `start`, `capture`, `export`, `seed`, `sessions`, `search`, `skill` | `tapes chat`, `tapes checkout` |

Span search remains server-side (`GET /v1/search/spans` and the embed worker). Only the client command moved.

```bash
curl -sSfL https://download.tapes.dev/tapesctl/install | bash
tapesctl start claude --tapes-url http://localhost:8082
tapesctl sessions list --tapes-url http://localhost:8081
tapesctl export <session-id> --detail spans -o session.jsonl
tapesctl seed --tapes-url http://localhost:8081
```

`tapesctl --tapes-url` falls back to `TAPES_URL`, then `tapesctl config set tapes-url`. Capture commands use ingest; read commands use the read API.

## Failure modes

| Symptom | Cause |
| --- | --- |
| `docker is required for 'tapes local'` | Docker not on `PATH`. |
| `tapes status` API unreachable | Nothing listening on `client.api_target` (default `:8081`). |
| Backfill posts to the wrong port | `--ingest-url` defaults to `:8090`; combined `tapes serve` ingest is `:8082`. |
| `equivalence requires a postgres DSN` | No `--postgres` and no `storage.postgres_dsn`. |
| `unknown config key` | Key not in the valid set; `cassettes` cannot be `config set`. |
| `unknown preset` | Not `openai`, `anthropic`, `ollama`, or an `http(s)` URL. |
| `tapes local down --wipe` emptied sessions | Postgres data dir deleted. |
| OpenAI embed / skill calls fail with a project key | Store a service-account key via `tapes auth openai`. |
| Combined serve search empty | `--embed-spans=false`, or embed schema/provider setup logged a warning. |
| `--debug` ignored in favor of `--log-level` | Deprecated; only applies if `--log-level` was not set. |

## Related pages

<CardGroup>
  <Card title="Configuration reference" href="/configuration-reference">
    Dotdir resolution, TAPES_ env names, credentials.toml, and the cassettes array.
  </Card>
  <Card title="Split the stack" href="/split-the-stack">
    Run proxy, api, ingest, derive-worker, and embed-worker as separate processes.
  </Card>
  <Card title="Prove the capture ratchet" href="/capture-ratchet">
    off / dual / raw modes and how equivalence classes gate the flip.
  </Card>
  <Card title="Read API vs ingest" href="/read-vs-ingest">
    Why :8081 is public read and :8082 is the private write path.
  </Card>
  <Card title="Configure embeddings" href="/configure-embeddings">
    ollama vs openai, tapes auth, and the embed worker.
  </Card>
  <Card title="Capture an agent" href="/capture-an-agent">
    Point an agent at the proxy or ingest via tapesctl.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Wrong-port capture, --wipe data loss, missing embed keys.
  </Card>
</CardGroup>
