Terminal and platforms9 of 10
Platform support
On this page
- The matrix
- Why the interactive terminal is POSIX-only
- What kotoba doctor says
- Where the product itself behaves differently
- 1. File permissions: modes on one side, ACLs on the other
- 2. The approval gate reads a different shell
- 3. File locking
- 4. Colour
- 5. Starting and stopping child processes
- Smaller ones
- What is not platform-specific
- How much of this has actually run on Windows
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
| Linux | macOS | Windows | |
|---|---|---|---|
kotoba setup | yes | yes | yes |
kotoba doctor | yes | yes | yes |
kotoba --once "…" | yes | yes | yes |
kotoba serve | yes | yes | yes |
kotoba discord | yes | yes | yes |
kotoba --version | yes | yes | yes |
| The web app | yes | yes | yes |
kotoba — the interactive terminal | yes | yes | no |
kotoba --sessions / --settings | yes | yes | no — 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:
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:
| Platform | What the permissions row says |
|---|---|
| POSIX, files at 0600 | 0600 on the master key, the database and every atomic write |
| POSIX, a file left loose | the master key and the database are not private to this account — kotoba.db is 0644 |
| Windows, grant took | granted to *S-1-5-21-… alone, and to nobody else |
| Windows, grant refused | the 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 host | Windows host, sandbox local | |
|---|---|---|
cat file.txt auto-runs | yes | no |
ls auto-runs | yes | no |
a saved cat family grant auto-approves | yes | no |
| a saved exact-line grant auto-approves | yes | no |
| an allowlisted command bypasses the card | yes | no |
a offered on a shell card | yes | no |
t offered on a shell card | yes | no |
execute_code family grant auto-approves | yes | yes |
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:
| POSIX | Windows, local | |
|---|---|---|
the /settings note on sandbox | she runs on this machine — a plain read in her workdir passes, the rest asks | she runs on this machine — she asks first |
/approvals with nothing saved | only a plain read in her workdir runs without a card | every command asks first |
after /approvals rm cat | a plain read like that still runs unasked inside her workdir | she'll ask before that again |
the card's note when a is withheld | depends on the line | On 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
npmisnpm.cmd, a batch file, andCreateProcessruns only real executables — so the bare name that works everywhere else failed there even after npm was found, becauseshutil.whichresolves it through PATHEXT and CreateProcess will not. The command processor is named instead:cmd /c npm run dev.npmstays unqualified, becausecmd /cstrips the outer quotes off its first token and a real path likeC:\Program Files\nodejs\npm.cmdcomes 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_EVENTto the group, thentaskkill /F /T /PID, which walks the tree by parent id. Endingcmd.exealone would leave the Next worker it started holding port 3000. - SIGHUP.
servecatches 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 readsSIGHUP, which does not exist on Windows.
Smaller ones
/openhands a path toxdg-open, macOSopen, oros.startfileon 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_BINARYwhere the flag exists.os.openis text mode on Windows, so without it every0x0Ain the key reached disk as0x0D 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
termioson POSIX and through the console API on Windows. There,msvcrtreads the console rather than stdin, so with a pipe it would wait for a keypress nobody is going to make — the check isstdin.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
- 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.
