Skip to content

Configuration

How tapesctl resolves a server URL, the config.toml schema, and every file and directory it reads or writes.

tapesctl has separate read-API and ingest-API settings in one file. This page covers how they resolve, what else lives beside them, and where diagnostics go.

Both endpoints have local defaults:

purpose default flag environment config key
read API http://localhost:8081 --api-url TAPES_API_URL api-url
ingest API http://localhost:8082 --ingest-url TAPES_INGEST_URL ingest-url

For either endpoint, an explicit flag beats its environment variable, which beats its configured value, which beats the localhost default.

There is no project-local layer. No .tapesrc, no directory walk, no per-repository override. One user-level file, one variable, one flag.

Three mechanics that are easy to get wrong:

  • Leaf position beats global position. tapesctl --api-url A sessions list --api-url B uses B.
  • The configured value is installed as the flag’s default, rather than resolved by hand. So precedence is the argument parser’s own, and a default does not count as a user-supplied argument — which is why a bare tapesctl still prints help on a configured machine instead of complaining about a missing subcommand.
  • The global --api-url deliberately carries no environment binding. The parser counts an environment-sourced value as user-supplied, so binding TAPES_API_URL at the top level would make a bare tapesctl answer error: requires a subcommand on any machine with the variable exported. The per-command declarations carry the binding instead, so the fallback still works everywhere it matters.

Cassette discovery resolves the same three sources itself, because it runs before arguments are parsed. See Cassettes.

Configure a remote deployment once when its ports differ from the defaults:

Terminal window
tapesctl config set api-url https://tapes.example/api
tapesctl config set ingest-url https://tapes.example/ingest

See The two ports for which command is on which side.

The path is ~/.tapes/config.toml, resolved once and nowhere else. Ask for it rather than assuming:

Terminal window
tapesctl config path
/Users/you/.tapes/config.toml

config path prints the path whether or not the file exists.

The schema has one key per endpoint:

~/.tapes/config.toml
api-url = "http://localhost:8081"
ingest-url = "http://localhost:8082"
key type meaning validation
api-url string read API must parse as a URL and use scheme http or https
ingest-url string ingest API must parse as a URL and use scheme http or https

Setting either:

Terminal window
tapesctl config set ingest-url http://localhost:8082

Validation happens at write time rather than on every command afterwards, and a rejected value writes nothing:

tapesctl: unknown config key "tapes-erl" (known keys: api-url)
tapesctl: invalid tapes URL
tapesctl: api-url must be an http or https URL; "ftp" is not a scheme this client can call

The file is deliberately not under $XDG_CONFIG_HOME. It sits beside ~/.tapes/logs, ~/.tapes/skills, and ~/.tapes/codex-app so there is one directory to inspect, back up, or delete.

Rules that are invisible from the help text

Section titled “Rules that are invisible from the help text”
  • Unknown keys are preserved, not refused. Reading ignores them, and writing edits the TOML document in place rather than re-serializing it — so comments, ordering, your formatting, and keys a newer tapesctl wrote all survive a config set.
  • A malformed file fails only the config commands. They surface a parse error; every other command loads with a fallback, warns at -v, and continues with an empty configuration.
  • config set never reads before it writes, so it can repair a known key holding a wrong-typed value. Structurally broken TOML is still refused rather than clobbered.
  • config get can print nothing from a file that is not empty. Only keys that are both known and set are listed. A file containing only a key this build has never heard of produces empty output — the forward-compatibility rule working as designed, and indistinguishable from an empty file.
  • A known-but-unset key prints nothing and exits 0, so $(tapesctl config get api-url) is empty rather than an error a script has to special-case.

One rule governs everything here: while a harness holds the terminal, nothing may reach stdout or stderr. A stray log line lands in the middle of a TUI frame.

So diagnostics go to a file when, and only when, the command hands over the terminal and verbosity is at its default. In practice that is start without -v. Every other command — sync, capture, the read commands — logs to stderr as usual.

~/.tapes/logs/start-YYYYMMDD-HHMMSS-<pid>.log

Files are created 0600 and appended to, never truncated. The path is printed before the harness launches and again when it exits.

Pass -v to start to stream to stderr instead of a file, accepting what that does to the display. That is the documented way to watch a capture live.

Level precedence is RUST_LOG, then the -v count, then info. A set-but-empty RUST_LOG is treated as unset. An unparseable one prints tapesctl: ignoring invalid RUST_LOG <directive> (<err>) to stderr and falls back.

There is no stderr fallback when the log file cannot be opened. The run prints tapesctl: diagnostics disabled — no log file (<err>) once, then discards events. A corrupted TUI is judged more costly than a lost debugging session.

What tapesctl writes:

path written by
~/.tapes/config.toml config set
~/.tapes/logs/start-*.log start, at default verbosity
~/.tapes/codex-app/handoff.json plugin install codex-app
~/.tapes/codex-app/plugin/ plugin install codex-app
~/.pi/agent/extensions/tapes-gateway.ts plugin install pi
~/.config/opencode/plugins/tapes-gateway.ts plugin install opencode
~/.codex/config.toml (or $CODEX_HOME/config.toml) plugin install/uninstall codex-app, patched in place
<platform cache>/tapesctl/cassettes/<key>.json cassettes, help, and bare invocations — the only shapes that run cassette discovery

Skill documents, log files, and installed plugin files are written 0600.

What it reads but never writes:

path read by
~/.claude/projects/ the transcript tailer and sync
~/.claude/sessions/<pid>.json Claude attribution
$CODEX_HOME/sessions, or ~/.codex/sessions Codex attribution
variable read by
TAPES_API_URL read commands and cassette discovery
TAPES_INGEST_URL start, capture, sync
TAPES_UPSTREAM start, capture
TAPES_WEB_URL start, capture
TAPES_ORG_ID start, capture
TAPES_AUTH_SUBJECT start, capture, sync
RUST_LOG logging, all commands
TAPESCTL_CACHE_DIR the cassette surface cache
CODEX_HOME plugin install/uninstall codex-app, capture codex-app, start codex
USER, then USERNAME the default --auth-subject: local:<user>, else local:unknown
OPENAI_API_KEY start codex upstream selection

There is no telemetry variable, because there is no telemetry. tapesctl reports nothing about you anywhere.

The parent environment is inherited wholesale by a launched harness. start clears nothing, so a variable set in your shell reaches the harness unchanged.

Because the global flag propagates into every leaf’s help, --api-url is rendered for commands that never make an HTTP call: config set, config get, config path, version, and plugin uninstall. It is inert in all of them.

The reverse is worth stating too: plugin install and plugin uninstall write local files from bytes the binary already carries and fetch nothing.