Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

Terminal and platforms9 of 10

Platform support

About 11 minutes to read

On this page

Kotoba runs on Linux, macOS and Windows. One thing does not run on Windows: the interactive terminal. Everything else does, natively, and this page says exactly which is which and why.

The matrix

LinuxmacOSWindows
kotoba setupyesyesyes
kotoba doctoryesyesyes
kotoba --once "…"yesyesyes
kotoba serveyesyesyes
kotoba discordyesyesyes
kotoba --versionyesyesyes
The web appyesyesyes
kotoba — the interactive terminalyesyesno
kotoba --sessions / --settingsyesyesno — they open a panel of that terminal

On Windows, bare kotoba does not refuse. It starts the backend and the web UI and opens your browser. See Windows today.

Why the interactive terminal is POSIX-only

It is built on the POSIX terminal layer, and Windows ships neither of the two modules it needs: termios and tty. Three separate things in the terminal depend on them.

Raw and cbreak mode. A card is answered by one keypress with no Enter, and a card sits on the last row of a live region — a line editor echoing what you type there costs a row of scroll per press and takes the header up with it. So the keyboard is read in cbreak, through termios.tcgetattr / tty.setcbreak / termios.tcsetattr, with TCSANOW rather than the default TCSAFLUSH because that one discards whatever you typed while she was working.

The terminal probes. At launch she asks the terminal what it can do, in one raw-mode window: OSC 11 for the background colour, DA1 for sixel support, CPR to find out whether ambiguous-width characters draw as one cell or two, and TIOCGWINSZ or XTWINOPS 16 for the pixel size of a cell. Every one of those is a write followed by a timed read in raw mode — and whatever you typed during a probe is carried forward instead of being eaten.

Reading the keyboard through the event loop. For the length of a turn nothing else reads stdin, so the file descriptor is registered with loop.add_reader and the keystrokes arrive as loop callbacks. That is a selector-loop facility.

The decision is asked of the import system, not of sys.platform:

python
TERMINAL_MODULES = ("termios", "tty")

def terminal_ui_available() -> bool:
    return not missing_terminal_modules()

There is one copy of that question, because two things read the answer — the entry point, which decides whether to open her terminal at all, and kotoba doctor, which explains why it did not. Two copies would eventually say different things on the same machine.

This is not the cli extra. prompt_toolkit, rich and pillow install perfectly on Windows, so an extras check reports everything present while nothing can draw. The two questions have two answers and two sentences: on the same kotoba doctor report, optional extras says ok cli installed and what she needs says warn terminal no interactive terminal …. Neither is wrong.

It is planned, not abandoned. The project's roadmap lists it under Next: "The full terminal on Windows. The commands run there now, but the interactive session does not — it needs a POSIX terminal. Making it native is a piece of work, not a patch."

What kotoba doctor says

The first two rows of the report are the platform and the terminal, in that order, because the first decides what the rest means. The platform row is this machine in the words a bug report needs — system, release, architecture and Python version, in the shape Windows 11 (AMD64), Python 3.12.4 — and it is never a failure, because no operating system is a problem to fix.

The terminal row underneath it is the one that answers the question:

  ok    terminal    her interactive terminal can run here

Where the layer is missing:

  warn  terminal    no interactive terminal — it needs the POSIX terminal layer (termios, tty),
                    which this system does not have, and the `cli` extra below cannot supply it.
                    `kotoba` with no arguments starts the backend and the web UI instead and opens
                    your browser; `kotoba --once`, `setup`, `doctor`, `serve` and `discord` are
                    unaffected.

A warn, never a fail — every other way in works, and the message names them rather than leaving you with a dead end.

Where the product itself behaves differently

Five things below the surface answer to the platform.

1. File permissions: modes on one side, ACLs on the other

os.chmod(path, 0o600) on Windows toggles the read-only bit and writes no ACL at all. The database, the keystore's master key and every atomic write would have had no protection outside %USERPROFILE%, whose inherited ACL is what makes that easy to miss.

So on Windows each of those files is restricted with icacls <path> /inheritance:r /grant:r <SID>:(F) — full control for one account, with the inherited entries dropped rather than copied. icacls ships with Windows and needs no extra dependency. On POSIX nothing changes: the chmod is the whole of it.

The account is named by SID, resolved once per process from whoami /user, not built out of the environment. On a standalone machine that has a workgroup, USERDOMAIN is the workgroup and not the computer, so WORKGROUP\user mapped to no account and every grant failed silently.

A failed grant is logged and never raised — she must still be able to save a setting on a machine that cannot run icacls. Which is exactly why the doctor row asks by doing it, on a throwaway file beside the real ones:

PlatformWhat the permissions row says
POSIX, files at 06000600 on the master key, the database and every atomic write
POSIX, a file left loosethe master key and the database are not private to this account — kotoba.db is 0644
Windows, grant tookgranted to *S-1-5-21-… alone, and to nobody else
Windows, grant refusedthe master key and the database are not private to this account — icacls refused …

2. The approval gate reads a different shell

The gate parses a command line to decide whether it is a plain read inside her workdir. All of that reading is POSIX: shlex in POSIX mode, an sh metacharacter set, a POSIX tool allowlist, / as the separator. POSIX shlex eats backslashes, so cat C:\Users\me\.ssh\id_rsa arrives as one token with no separator in it — and reads as a harmless file inside the workspace. cat is a real PowerShell alias for Get-Content.

So on a Windows host running the local sandbox, every automatic route is closed and the card is always shown. Measured, with the platform answer forced:

POSIX hostWindows host, sandbox local
cat file.txt auto-runsyesno
ls auto-runsyesno
a saved cat family grant auto-approvesyesno
a saved exact-line grant auto-approvesyesno
an allowlisted command bypasses the cardyesno
a offered on a shell cardyesno
t offered on a shell cardyesno
execute_code family grant auto-approvesyesyes

execute_code is the exception on purpose: its action is Python, handed over under its own family name, so none of the POSIX reading that breaks here was ever applied to it. Locking it would be a different change. Its own dangerous-code check is spelt for both separator styles, so open(r"C:\Users\me\.kotoba\.keystore_key","rb") is caught as a secret read and still draws a card under a saved grant.

The destructive-pattern set speaks the Windows dialect too, so the card can say what it is showing: rd /s /q, Remove-Item -Recurse -Force, del C:\*, format C:, diskpart, reg delete … /f, vssadmin delete shadows /all, iwr … | iex, Stop-Computer, Start-Process … -Verb RunAs.

This is keyed on where the command will actually run, not on this process's platform. With sandbox docker on a Windows host the command lands in a Linux container, the POSIX reading is the correct one, and the low-friction path is right again. With sandbox none nothing runs anywhere.

The sentences move with it:

POSIXWindows, local
the /settings note on sandboxshe runs on this machine — a plain read in her workdir passes, the rest asksshe runs on this machine — she asks first
/approvals with nothing savedonly a plain read in her workdir runs without a cardevery command asks first
after /approvals rm cata plain read like that still runs unasked inside her workdirshe'll ask before that again
the card's note when a is withhelddepends on the lineOn Windows I can't read a command line the way the shell will, so every one of them asks.

One sentence does not move with the platform: the clause under the launch header. It is a fixed string chosen by window width alone, so on a narrow window a Windows host is told on this machine — reads pass, the rest asks, which is not true there. Reported.

3. File locking

Her small shared files — settings, memory, the MCP config, the file-library index — are written under an exclusive lock, because two turns plus a panel click are three writers in one process. On POSIX that is flock. Windows ships no flock, so it is msvcrt.locking on a byte range, seeking to 0 first because msvcrt locks at the current position. The lock lives in a .lock sibling that nothing else ever opens for its contents, which is why msvcrt's mandatory locking costs nothing here.

A platform with neither degrades to no lock rather than spending fifteen seconds retrying against a filesystem that will never answer.

4. Colour

Everywhere but Windows, writing an escape code paints. A Windows console understands the codes but arrives with them switched off, so the answer is also the act of asking: SetConsoleMode is called once on the standard output handle with the virtual-terminal-processing flag (0x0004), and a console that refuses gets plain text rather than ←[32mok printed at somebody on their first run. Windows Terminal has it on already; the classic console does not — and kotoba doctor is exactly the command somebody runs there when nothing else works.

5. Starting and stopping child processes

kotoba serve runs two children and has to be able to kill both.

  • Naming npm. On Windows npm is npm.cmd, a batch file, and CreateProcess runs only real executables — so the bare name that works everywhere else failed there even after npm was found, because shutil.which resolves it through PATHEXT and CreateProcess will not. The command processor is named instead: cmd /c npm run dev. npm stays unqualified, because cmd /c strips the outer quotes off its first token and a real path like C:\Program Files\nodejs\npm.cmd comes apart at the space.
  • Groups. POSIX gets a new session; Windows gets CREATE_NEW_PROCESS_GROUP, which a console Ctrl+C is likewise not delivered into.
  • Teardown. POSIX: SIGTERM to the session, then SIGKILL. Windows: CTRL_BREAK_EVENT to the group, then taskkill /F /T /PID, which walks the tree by parent id. Ending cmd.exe alone would leave the Next worker it started holding port 3000.
  • SIGHUP. serve catches SIGTERM as well as Ctrl+C, so a supervisor cannot leave two orphaned servers behind. The signal names are looked up one at a time and skipped where the platform has none — building (SIGTERM, SIGHUP) as a tuple reads SIGHUP, which does not exist on Windows.

Smaller ones

  • /open hands a path to xdg-open, macOS open, or os.startfile on Windows. Only the first two are spawned as a child; the Windows call takes the path alone.
  • The keystore's master key and the log file are opened with O_BINARY where the flag exists. os.open is text mode on Windows, so without it every 0x0A in the key reached disk as 0x0D 0x0A — which cost an install written by an older build the first secret it ever saved, and ended every log line with \r\r\n.
  • Reading a secret at a terminal hides the echo through termios on POSIX and through the console API on Windows. There, msvcrt reads the console rather than stdin, so with a pipe it would wait for a keypress nobody is going to make — the check is stdin.isatty() instead, and where nothing can hide an echo the promise is withdrawn rather than made falsely.

What is not platform-specific

  • ElevenLabs is required for voice on every platform. There is no local speech engine in version
    1. It is on the roadmap, after launch.
  • The database, the memory, the tools and the model all behave the same.

How much of this has actually run on Windows

Be exact about this, because it matters.

The Windows behaviour above is pinned by tests, and simulated: a meta-path hook blocks the POSIX modules CPython does not ship on win32 (fcntl, termios, tty, pty, pwd, grp, resource), entry points run in a subprocess so the hook cannot outlive its test, and the platform-dependent branches are driven by forcing the module flag rather than by patching os.name. That proves the Windows branch is the one taken and does what it says. It is not the same as a run on a real Windows machine.

There is also a real Windows leg in CI — windows-latest, Python 3.13, running the whole Python suite alongside the two Ubuntu legs. This page does not claim any particular result from it.