# Configuration reference

> Dotdir resolution, flag then TAPES_ then config.toml precedence, dotted keys, defaults, credentials.toml, and the cassettes array.

- 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

- `pkg/config/types.go`
- `pkg/config/defaults.go`
- `pkg/config/flags.go`
- `pkg/config/viper.go`
- `pkg/dotdir/manager.go`
- `cmd/tapes/config/set.go`
- `docs/configuration.md`

---

---
title: "Configuration reference"
description: "Dotdir resolution, flag then TAPES_ then config.toml precedence, dotted keys, defaults, credentials.toml, and the cassettes array."
---

`tapes` is the server binary. It loads persistent settings from `config.toml` in a resolved `.tapes/` directory, then overlays `TAPES_*` environment variables and bound CLI flags. Secrets belong in `credentials.toml` (or the provider’s own environment variable), not in `config.toml`. `tapesctl` is a separate client and does not share this precedence chain.

## Dotdir resolution

The server looks for a `.tapes/` directory in this order:

1. `--config-dir <directory>` (creates the directory at `0755` if missing)
2. `./.tapes/` in the current working directory
3. `~/.tapes/`
4. no directory — built-in defaults apply; `tapes config set` cannot save until a directory exists

A project-local `./.tapes/` wins over `~/.tapes/`. That is the usual reason a global setting appears ignored.

:::files
.tapes/
  config.toml         # 0600; server settings
  credentials.toml    # 0600; created by tapes auth
  telemetry.json      # optional CLI telemetry identity
  postgres/           # local Postgres data from tapes local up
:::

<ParamField body="config-dir" type="string">
Persistent flag on every `tapes` command. Absolute path that replaces the usual `./.tapes/` then `~/.tapes/` walk. Empty means walk the default order.
</ParamField>

Create a project-local directory:

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

`tapes init` always creates `./.tapes/` in the current working directory and writes `config.toml` (`0600`). `--preset` accepts `openai`, `anthropic`, `ollama`, or an `http://` / `https://` URL that returns raw TOML (`version` must be `0` or omitted). `tapes local up` also writes the resolved local Postgres DSN and Ollama embedding target into the same resolved `.tapes/config.toml`.

## Precedence

For every setting bound into Viper (serve flags, `tapes config get`/`list`, logging, telemetry, update checks):

1. CLI flag, if the flag was registered on that command and changed
2. `TAPES_*` environment variable
3. `config.toml` value
4. `NewDefaultConfig()` built-in default

Dots become underscores after the `TAPES_` prefix:

| Config key | Environment variable | Typical flag |
| --- | --- | --- |
| `proxy.listen` | `TAPES_PROXY_LISTEN` | `--proxy-listen` / `-p` |
| `proxy.provider` | `TAPES_PROXY_PROVIDER` | `--provider` |
| `proxy.upstream` | `TAPES_PROXY_UPSTREAM` | `--upstream` / `-u` |
| `storage.postgres_dsn` | `TAPES_STORAGE_POSTGRES_DSN` | `--postgres` |
| `api.listen` | `TAPES_API_LISTEN` | `--api-listen` / `-a` |
| `ingest.listen` | `TAPES_INGEST_LISTEN` | `--ingest-listen` / `-i` |
| `embedding.provider` | `TAPES_EMBEDDING_PROVIDER` | `--embedding-provider` |
| `cassettes` | `TAPES_CASSETTES` | `--cassettes` |
| `telemetry.disabled` | `TAPES_TELEMETRY_DISABLED` | `--disable-telemetry` |
| `update.disabled` | `TAPES_UPDATE_DISABLED` | `--disable-update-check` (hidden) |
| `logging.level` | `TAPES_LOGGING_LEVEL` | `--log-level` |

<CodeGroup>

```bash title="Flag"
tapes serve --provider anthropic --upstream https://api.anthropic.com
```

```bash title="Environment"
export TAPES_PROXY_PROVIDER=anthropic
export TAPES_PROXY_UPSTREAM=https://api.anthropic.com
tapes serve
```

```toml title="config.toml"
[proxy]
provider = "anthropic"
upstream = "https://api.anthropic.com"
```

</CodeGroup>

`tapes config get` and `tapes config list` bind the same `TAPES_` prefix, so they show the environment overlay, not only the file. `tapes config set` writes `config.toml` only.

Some serve knobs are **flag-only** (not Viper keys, not `tapes config set`):

| Flag | Default | Effect |
| --- | --- | --- |
| `--embed-spans` | `true` | In-process embed worker on `tapes serve`; disable with `--embed-spans=false` |
| `--cassette-refresh` | `30s` | How often the API re-fetches cassette OpenAPI documents |

## Manage dotted keys

```bash
tapes config list
tapes config get proxy.provider
tapes config set proxy.provider anthropic
tapes config set embedding.dimensions 768
```

Keys use dotted notation that matches TOML sections. Flat names (`provider`, `embedding_dimensions`) are rejected. Unknown keys fail with `unknown config key` and the valid-key list.

`tapes config set` preserves `version` and the top-level `cassettes` array. It cannot create or edit `cassettes`. Values are written through Viper so strings coerce (for example `embedding.dimensions` to `uint`). `config.toml` is rewritten at `0600`. Empty values print as `<not set>`.

## Key catalog

Defaults come from `NewDefaultConfig()`. `storage.postgres_dsn`, `proxy.project`, `vector_store.target`, OpenCode, telemetry, and update fields start empty / false.

### Storage and listen addresses

| Key | Default | Purpose |
| --- | --- | --- |
| `storage.postgres_dsn` | unset | Capture and derived PostgreSQL DSN. Required by `tapes serve derive-worker` and `tapes serve embed-worker`. `tapes local up` writes `postgres://tapes:tapes@localhost:5432/tapes?sslmode=disable` (port follows `--postgres-port`) |
| `proxy.listen` | `:8080` | Capture proxy listen address |
| `api.listen` | `:8081` | Read API listen address |
| `api.web_ui` | `false` | Minimal browser UI at `/` on the read API |
| `ingest.listen` | `:8082` | Private ingest listen address |
| `proxy.project` | unset | Session project tag. When empty at serve/derive time, the git repo name is used |
| `vector_store.target` | unset | pgvector DSN. When empty at serve time, copied from `storage.postgres_dsn` |
| `vector_store.provider` | unset | Accepted by `tapes config get`/`set`/`list`. Not a field on the persisted `Config` struct and not read by `tapes serve` |

Logged DSNs mask URL user passwords and `password=` parameters as `xxxxx`.

### Proxy and client URLs

| Key | Default | Purpose |
| --- | --- | --- |
| `proxy.provider` | `ollama` | `anthropic`, `openai`, or `ollama` |
| `proxy.upstream` | `http://localhost:11434` | Upstream provider base URL |
| `client.proxy_target` | `http://localhost:8080` | Persisted proxy URL (full `scheme://host:port`) |
| `client.api_target` | `http://localhost:8081` | Persisted read API URL. `tapes status --api-target` binds this key |

Capture and read commands that used these keys have moved to `tapesctl`. See [CLI reference](/cli-reference).

### Embeddings

| Key | Default | Purpose |
| --- | --- | --- |
| `embedding.provider` | `ollama` | `ollama` or `openai` |
| `embedding.target` | `http://localhost:11434` | Embedding service URL |
| `embedding.model` | `embeddinggemma` | Embedding model |
| `embedding.dimensions` | `768` | Vector width. Must match the model; an existing table with a different width fails embed-worker startup |

After flags, env, and file merge, `ResolveEmbeddingConfig` rewrites inherited Ollama-shaped values when `embedding.provider` is `openai`:

| Provider | Target | Model | Dimensions |
| --- | --- | --- | --- |
| `ollama` (empty fields filled) | `http://localhost:11434` | `embeddinggemma` | `768` |
| `openai` (if those fields were still the Ollama defaults) | `https://api.openai.com` | `text-embedding-3-large` | `1024` |

Explicit target, model, or dimensions are kept. OpenAI keys come from `OPENAI_API_KEY` or `credentials.toml`, never from `config.toml`. See [Configure embeddings](/configure-embeddings).

### OpenCode, logging, telemetry, updates

| Key | Default | Allowed values |
| --- | --- | --- |
| `opencode.provider` | unset | Saved OpenCode provider |
| `opencode.model` | unset | Saved OpenCode model |
| `logging.level` | `info` | `debug`, `info`, `warn`, `error` |
| `logging.format` | `auto` | `auto`, `console`, `text`, `json` |
| `logging.color` | `auto` | `auto`, `always`, `never` |
| `telemetry.disabled` | `false` | Also skipped in CI (`CI`, `GITHUB_ACTIONS`, and similar) |
| `update.disabled` | `false` | Disables the startup update check |

`--debug` is deprecated; use `--log-level=debug`. Invalid logging values fail config load and `tapes config set` with `invalid logging config`.

`version` in TOML must be `0` or omitted. Any other version is `unsupported config version`.

## Cassettes array

`cassettes` is a **top-level string array** of exact OpenAPI document URLs for already-running cassette processes. Tapes does not start those processes.

```toml
# .tapes/config.toml
version = 0
cassettes = ["http://127.0.0.1:9999/openapi"]
```

Equivalent overlays:

```bash
tapes serve --cassettes=http://127.0.0.1:9999/openapi
tapes serve api --cassettes=http://127.0.0.1:9999/openapi
TAPES_CASSETTES=http://127.0.0.1:9999/openapi tapes serve
```

`--cassettes` is repeatable. `TAPES_CASSETTES` is CSV (quoted commas allowed). Resolution is flag, then `TAPES_CASSETTES`, then the TOML array.

<Warning>
`cassettes` is not a dotted `tapes config set` key. `SetConfigValue` preserves an existing array and will not write one.
</Warning>

Each entry must:

- use `http` or `https`
- include a host (`http://:8080/openapi` is rejected)
- omit URL userinfo
- omit a fragment
- be unique (duplicates report `cassettes[N]: duplicates cassettes[M]`)

Query strings are allowed. The removed `[[cassettes]]` table shape (`manifest`, `url`, `enabled`) is rejected. Unknown fields under `cassettes` fail parse. Unreachable sources stay retryable at runtime and do not block process start. Refresh interval is `--cassette-refresh` (default `30s`). See [Cassettes](/cassettes) and [Run a cassette locally](/run-a-cassette).

## Credentials

Store provider keys with `tapes auth`. The capture proxy is transparent: it forwards whatever the agent sent. These keys are for **server-side** embedding and skill generation.

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

Supported providers and env overrides:

| Provider | File field | Environment (wins if set) |
| --- | --- | --- |
| `openai` | `[providers.openai] api_key` | `OPENAI_API_KEY` |
| `anthropic` | `[providers.anthropic] api_key` | `ANTHROPIC_API_KEY` |

`credentials.toml` lives next to `config.toml` in the resolved `.tapes/` directory (`0600`). If no `.tapes/` exists, `tapes auth` creates `~/.tapes/`. When the matching environment variable is already set, the file key is ignored.

<Warning>
Do not put API keys in `config.toml`. Cassette URLs must not include userinfo either.
</Warning>

For OpenAI, a service account key (`sk-svcacct-...`) is recommended. `tapes auth` warns on `sk-proj-...` prefixes.

## Init presets

| Preset | `proxy.provider` | `proxy.upstream` | Embedding |
| --- | --- | --- | --- |
| `ollama` | `ollama` | `http://localhost:11434` | Ollama defaults (`embeddinggemma` / `768`) |
| `openai` | `openai` | `https://api.openai.com` | OpenAI defaults (`text-embedding-3-large` / `1024`) |
| `anthropic` | `anthropic` | `https://api.anthropic.com` | Left at package defaults until you set `embedding.*` |

Listen addresses stay `:8080` / `:8081`. Client targets stay `http://localhost:8080` and `http://localhost:8081`.

## Worker and API-only keys

These participate in flag → `TAPES_` → TOML when the matching command binds them. They are **not** `tapes config set` keys.

### `tapes serve derive-worker`

Empty duration flags fall through to package defaults.

| Flag | Viper / env key | Default |
| --- | --- | --- |
| `--poll-interval` | `derive_worker.poll_interval` / `TAPES_DERIVE_WORKER_POLL_INTERVAL` | `5s` |
| `--debounce` | `derive_worker.debounce` | `20s` |
| `--sweep-interval` | `derive_worker.sweep_interval` | `1h` |
| `--sweep-window` | `derive_worker.sweep_window` | `24h` (negative sweeps all history) |
| `--max-derive-lag` | `derive_worker.max_derive_lag` | `45s` |
| `--metrics-listen` | `derive_worker.metrics_listen` | empty (disabled) |
| `--wait-for-db` | `derive_worker.wait_for_db` | `false` (fail fast) |

### `tapes serve embed-worker`

| Flag | Viper / env key | Default |
| --- | --- | --- |
| `--interval` | `embed_worker.interval` | `1m` |
| `--metrics-listen` | `embed_worker.metrics_listen` | empty |
| `--wait-for-db` | `embed_worker.wait_for_db` | `false` |
| `--batch-size` | `embed_worker.batch_size` | `0` (built-in page size) |
| `--max-text-bytes` | `embed_worker.max_text_bytes` | `0` (built-in cap; negative disables) |
| `--org` | `embed_worker.org` | empty (all orgs) |

### `tapes serve api`

<ParamField body="skill-model" type="string">
Chat model for `POST /v1/skills/generate`. Viper key `skill.model` / `TAPES_SKILL_MODEL`. Empty keeps the generator’s per-provider chat default. Not an embedding model.
</ParamField>

Standalone subcommands remap listen flags: `tapes serve api --listen`, `tapes serve proxy --listen`, `tapes serve ingest --listen` still bind `api.listen`, `proxy.listen`, and `ingest.listen`. Split-process flags and DSNs: [Split the stack](/split-the-stack).

## Example `config.toml`

```toml
version = 0

cassettes = ["http://127.0.0.1:9999/openapi"]

[storage]
postgres_dsn = "postgres://tapes:tapes@localhost:5432/tapes?sslmode=disable"

[proxy]
provider = "ollama"
upstream = "http://localhost:11434"
listen = ":8080"

[api]
listen = ":8081"
web_ui = false

[ingest]
listen = ":8082"

[client]
proxy_target = "http://localhost:8080"
api_target = "http://localhost:8081"

[vector_store]
target = "postgres://tapes:tapes@localhost:5432/tapes?sslmode=disable"

[embedding]
provider = "ollama"
target = "http://localhost:11434"
model = "embeddinggemma"
dimensions = 768

[logging]
level = "info"
format = "auto"
color = "auto"

[telemetry]
disabled = false

[update]
disabled = false
```

## The client file (`tapesctl`)

`tapesctl` does not read the server’s project-local `.tapes/` or this precedence chain. It keeps one file, `~/.tapes/config.toml`, and one key:

```toml
# ~/.tapes/config.toml
tapes-url = "http://localhost:8081"
```

```bash
tapesctl config set tapes-url http://localhost:8081
tapesctl config get tapes-url
tapesctl config path
```

Precedence is `--tapes-url`, then `TAPES_URL`, then that file. With none of the three, a command that needs a server fails and names the three sources. Capture commands want ingest on `:8082`; read commands want the API on `:8081` — configure one URL and pass `--tapes-url` for the other. See [Read API vs ingest](/read-vs-ingest).

## Errors and checks

| Symptom | Cause |
| --- | --- |
| Expected `~/.tapes` value ignored | `./.tapes/` exists in the current directory |
| `unknown config key` | Key not in the dotted catalog; `cassettes` cannot be `config set` |
| `cannot save empty target path` | No `.tapes/` resolved; run `tapes init` or pass `--config-dir` |
| `unsupported config version` | `version` is not `0` |
| `unknown cassette config fields` / URL validation | Bad `cassettes` entries (scheme, host, userinfo, fragment, duplicates, old table shape) |
| `parsing TAPES_CASSETTES as CSV` | Malformed CSV in the env overlay |
| `invalid logging config` | Level, format, or color not in the allowed sets |
| Derive/embed worker: `requires a postgres DSN` | Empty `storage.postgres_dsn` and no `--postgres` |
| OpenAI embed/skill calls fail with no key | Neither `OPENAI_API_KEY` nor `tapes auth openai` |

Verify the active file and overlay:

```bash
tapes config list
tapes status
```

<Check>
`tapes config list` prints `Config file: …/config.toml` when a directory resolved, or `No config file found. Using defaults.`
</Check>

## Related pages

<CardGroup>
  <Card title="CLI reference" href="/cli-reference">
    Command and flag inventory for `tapes`. `tapesctl` is the client.
  </Card>
  <Card title="Configure embeddings" href="/configure-embeddings">
    Switch `embedding.provider`, store keys with `tapes auth`, run or disable the embed worker.
  </Card>
  <Card title="Cassettes" href="/cassettes">
    Admission, path rewrite under `/v1/cassettes/{name}`, operator-owned lifecycle.
  </Card>
  <Card title="Split the stack" href="/split-the-stack">
    Per-process flags, DSNs, and failure domains.
  </Card>
  <Card title="Installation" href="/installation">
    Bootstrap local PostgreSQL and Ollama; `tapes local` writes the DSN into config.
  </Card>
</CardGroup>
