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.
Resolving server URLs
Section titled “Resolving server URLs”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 BusesB. - 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
tapesctlstill prints help on a configured machine instead of complaining about a missing subcommand. - The global
--api-urldeliberately carries no environment binding. The parser counts an environment-sourced value as user-supplied, so bindingTAPES_API_URLat the top level would make a baretapesctlanswererror: requires a subcommandon 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:
tapesctl config set api-url https://tapes.example/apitapesctl config set ingest-url https://tapes.example/ingestSee The two ports for which command is on which side.
config.toml
Section titled “config.toml”The path is ~/.tapes/config.toml, resolved once and nowhere else. Ask for it
rather than assuming:
tapesctl config path/Users/you/.tapes/config.tomlconfig path prints the path whether or not the file exists.
The schema has one key per endpoint:
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:
tapesctl config set ingest-url http://localhost:8082Validation 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 URLtapesctl: api-url must be an http or https URL; "ftp" is not a scheme this client can callThe 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
tapesctlwrote all survive aconfig set. - A malformed file fails only the
configcommands. They surface a parse error; every other command loads with a fallback, warns at-v, and continues with an empty configuration. config setnever 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 getcan 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.
Logging
Section titled “Logging”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>.logFiles 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.
Files and directories
Section titled “Files and directories”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 |
Environment variables
Section titled “Environment variables”| 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.
Commands that ignore --api-url
Section titled “Commands that ignore --api-url”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.