Configuration and operations4 of 9
Where everything lives
On this page
Kotoba keeps its state in one directory — ~/.kotoba — and the code resolves that directory in one
place, on purpose. Five modules once counted parents[N] by hand, which is the failure a package move
makes silent: a wrong depth still names a directory that exists, so the database is empty and the
skills are missing with nothing raised.
KOTOBA_HOME moves the whole directory. An empty value is not "the current directory" — that bug once
made a command run inside someone's project take that project for her home.
The layout
| Path | What it is | Its own override |
|---|---|---|
settings.yaml | Your saved settings and disabled toolsets | KOTOBA_SETTINGS |
.keystore_key | The 32-byte master key, mode 0600 | KOTOBA_KEYSTORE_KEY_FILE |
kotoba.db (+ -wal, -shm) | Conversations, saved keys, approvals, reminders, the audit trail | DATABASE_URL |
files/ | The files library — and, by default, her working directory | KOTOBA_FILES_DIR |
files/.index.json | Which files are new or edited. Never served, never deletable | — |
tmp/ | Scratch. Exposed to her shell and code as $TMPDIR; entries older than 24 h are swept every two minutes | KOTOBA_TMP_DIR |
memory/USER.md, memory/topics/*.md | What she remembers about you, as editable markdown | KOTOBA_MEMORY_DIR |
visual-memory/index.json, visual-memory/images/ | Durable image keepsakes | KOTOBA_VISUAL_MEMORY_DIR |
models/ | Installed Live2D models, one folder each. The installer unpacks here and marks the default | KOTOBA_MODELS_DIR |
plugins/ | Community tool plugins | KOTOBA_PLUGINS_PATH |
mcp.yaml | Connected MCP servers. Written when you save one; absent means none configured | KOTOBA_MCP_CONFIG |
pending_mcp.yaml | Servers found but not finished. Never holds a secret | KOTOBA_PENDING_MCP |
browser-profile/ | The profile of the browser she launches herself | KOTOBA_BROWSER_PROFILE |
playwright-output/ | Browser artefacts (screenshots) | KOTOBA_BROWSER_OUTPUT_DIR |
cli.log | The terminal's log, mode 0600, restarted past 2 MiB | KOTOBA_CLI_LOG |
cli_history | Terminal input history | KOTOBA_CLI_HISTORY |
soul/default.md, soul/templates/ | Seeded by kotoba setup and never overwritten afterwards. On a wheel this copy is the one that loads | SOUL_PATH |
Nothing here is created until something needs it, so a fresh install has a much shorter directory than this table.
The one rule that surprises people
A database sitting beside the package wins over the one in your home.
The resolution is exactly this:
- Installed from a wheel: always your home.
- Installed from a clone, and
api/kotoba.dbexists: that file, where it lies. - Installed from a clone with nothing there yet: your home.
So a fresh clone puts the database in ~/.kotoba/kotoba.db, and an older clone that already has one in
api/ keeps using it. Measured on a clone that has one:
db_dir() → <repo>/api
sqlite:///./kotoba.db resolves → <repo>/api/kotoba.db
Two things about why it is this way round.
Why the home is the new default. A database inside the checkout is lost the moment you unpack a new version into a new folder — keys, memory and every conversation — and the folder is precisely what an upgrade replaces. That was measured on a real reinstall which came up with no saved keys.
Why an existing one is never moved for you. Another process may hold it open, and a move that half succeeds costs the history it was meant to protect. Moving to the home is something you do on purpose, by moving the file, with everything stopped.
Why "beside the package" wins rather than "whichever exists". Preferring the home whenever a file
happened to be there was measured switching a live install onto a stray two-turn database while 561
conversations and two saved keys went invisible — and doctor called it ok. A stray in the home is
easy to acquire: a wheel in another virtualenv, a second account, a test that escaped its isolation.
kotoba doctor prints the path it actually opened, and adds "Beside the package, not in your home —
move it there to keep it across upgrades" only for a database that is genuinely in the old place.
DATABASE_URL in detail
A relative path resolves against that database directory, never against the current working directory. The terminal starts in your own project folder, so without this rule it would open — and create — a different, empty database with nothing on screen to say why.
A driver-qualified scheme is the same URL: sqlite:///, sqlite+aiosqlite:/// and
sqlite+pysqlite:/// all land on the same file. Three slashes means the path is relative; the fourth
slash belongs to the path, so sqlite:////abs/a.db is /abs/a.db. sqlite:// and sqlite:///:memory:
mean in-memory.
The personality file
SOUL_PATH defaults to ./soul/default.md, and the default is resolved through a candidate list so it
can never follow your current directory: api/soul/default.md, then the repository's soul/default.md,
then <home>/soul/default.md, then the copy packaged inside the wheel. A wheel install has no
repository, and the packaged copy is byte-identical to the repository's, so it can never shadow an edit.
A path you set is honoured, including relative to where you ran the command — that is the only way
to point at your own file. One caveat worth knowing: a relative SOUL_PATH that does not exist falls
through the candidate list and quietly loads the shipped default. Only an absolute SOUL_PATH that
does not exist raises, listing everything it tried.
File permissions
Kotoba only ever moves toward the more restrictive mode, and never refuses to run over it.
- The database is
touched andchmoded to0600on every connect, so a mode someone widened goes back each boot. SQLite would otherwise create it at0644minus umask — every turn she has ever been told, readable by any local account. - The
-wal,-shmand-journalsidecars are restricted twice: once at connect and again after the migrations, because on a fresh install the migrations are what create them. .keystore_keyis created with0600at creation, viaO_EXCL, so two processes booting together cannot each generate a key and orphan the other's secrets.- Every atomic write —
settings.yaml, the library index, the memory files — is written to a unique temporary at0600and renamed over the target. cli.logis0600.
kotoba doctor has a permissions row that answers by doing it rather than by reading the platform,
because the helper that applies the grant logs its failures and raises nothing. It never fails the run:
Kotoba works either way, and a filesystem with no ACLs would report a problem for ever.
Two directories that are not in your home
Skills are read from the clone's soul/skills, or from the copy inside the wheel. KOTOBA_SKILLS_DIR
overrides. Nothing writes there.
The packaged web UI lives inside the installed package, at <package>/web. KOTOBA_FRONTEND_DIR
points elsewhere — and it wins over whatever the install carries, so a typo in it disables the UI
rather than falling back. Both kotoba serve and kotoba doctor say so by name when the directory it
names holds no app.html.
