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

Security5 of 10

execute_code

About 4 minutes to read

On this page

execute_code runs a Python snippet. On KOTOBA_SANDBOX=local that is the host, in the working directory, with the scrubbed environment described in Secrets. On docker it is the container. On none the tool is not offered.

It goes through the approval gate like shell does, with two differences worth understanding before you save a grant for it.

Arbitrary Python is never auto-safe on the host

There is no equivalent of the 29-command read list. On local, every snippet asks, every time, unless you have saved a grant.

Under docker this inverts, exactly as it does for shell: the container is the boundary, so a snippet runs with no card and no grant. Measured with the docker backend and nothing saved, plain computation and an unused import run unasked, while a snippet matching the pre-screen below still raises a card.

A grant is saved under one fixed name

The whole snippet is the approval label, so nothing is approved unseen — but the grant is saved under the single family token execute_code, never per snippet. It is one standing permission covering all future Python, not a permission for the code you were looking at.

The card says so. Its family button reads "Always allow her to run Python", and underneath: "Any code she writes then runs on this computer without asking, until you revoke it in Settings."

This is also the one grant that survives on Windows, because it is not a shell command line and none of the POSIX reading that breaks there was ever applied to it.

What the grant still re-prompts for

Before the gate is consulted, the source is scanned against seven patterns. A match forces a card even under a saved grant. Read out of core/approval.py:

LabelWhat it looks for
fs-destroyshutil.rmtree, os.remove/unlink/rmdir, .unlink(, .rmdir(, and the from … import spellings
shell-spawnos.system, os.popen, subprocess.*, os.exec*, os.posix_spawn, pty.spawn, ctypes, importlib, and aliased imports of os or subprocess
raw-socketsocket.socket, a .connect((…)) call
dynamic-evaleval(, exec(, __import__(
fs-writeopen(…, <a mode containing w/a/x/+>), os.open with a writing flag, .write_text, .write_bytes, shutil.copy*/move, os.rename/replace/chmod/chown
data-outpost/put/patch on the common HTTP clients, a get whose URL is built rather than written literally, urlopen(… data=…), smtplib, ftplib, paramiko
secret-read.ssh, id_rsa, id_ed25519, /etc/shadow, /etc/passwd, .aws/credentials, .netrc, .keystore_key, .env

Measured with an execute_code family grant saved:

SnippetResult
plain computationruns, no card
writing a file, appending to a dotfileasks (fs-write)
shutil.rmtreeasks (fs-destroy)
subprocess.run(...), import os as o, ctypes, importlibasks (shell-spawn)
a bare import subprocess, with no calldoes not ask — the pattern needs the use, not the import
eval(...)asks (dynamic-eval)
opening a raw socketasks (raw-socket)
an HTTP post, or a get with an f-string URLasks (data-out)
reading an SSH key or a .envasks (secret-read)

The reasoning behind two of those choices is worth knowing. Plain fetching stays unflagged — it is common, legitimate, and the environment is scrubbed. Sending does not, because scrubbing the environment cannot stop code that reads a secret off disk and posts it, and a get whose URL is concatenated carries whatever was concatenated into it. Writing is flagged because write_file is the tool meant for writing, and it is visible in the Files panel.

The honest limit

This is a pattern denylist over Python source. Everything the section above says it catches, it catches. It does not analyse the program; it reads it.

Two examples of what that means, from the code's own notes: os is matched by name, so aliasing the import was a way past every spelling under it — which is why the aliased import is itself flagged now, rather than being handled properly. open()'s mode is matched across balanced parentheses because the path argument is usually a nested call that a simpler pattern stopped short of. Both are patches to a text matcher. Neither turns it into an analysis.

A saved grant leaves the working directory behind. The path jail that keeps a saved cat grant honest does not apply here — that check is only made for shell command families derived from a first token. Measured with an execute_code grant saved: reading a file by absolute path outside the working directory, listing another directory's contents, and a plain outbound requests.get all run with no card, because none of them matches a pattern.

So the honest statement of what an execute_code grant is: standing permission to run Python that computes, reads any file your account can read whose path does not match the secret-read pattern, and makes plain outbound GET requests — without being asked again. Everything the seven patterns name still asks. That is a real narrowing and it is not a sandbox.

If that is broader than you want, revoke it: /approvals rm execute_code in the terminal, or Settings → Security in the web app.

With no gate wired at all

In a sessionless or scripted call where no approval channel exists, a snippet matching any of the seven patterns is refused rather than run unattended, and answers in plain language. Code matching none of them runs.