Security5 of 10
execute_code
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:
| Label | What it looks for |
|---|---|
fs-destroy | shutil.rmtree, os.remove/unlink/rmdir, .unlink(, .rmdir(, and the from … import spellings |
shell-spawn | os.system, os.popen, subprocess.*, os.exec*, os.posix_spawn, pty.spawn, ctypes, importlib, and aliased imports of os or subprocess |
raw-socket | socket.socket, a .connect((…)) call |
dynamic-eval | eval(, exec(, __import__( |
fs-write | open(…, <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-out | post/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:
| Snippet | Result |
|---|---|
| plain computation | runs, no card |
| writing a file, appending to a dotfile | asks (fs-write) |
shutil.rmtree | asks (fs-destroy) |
subprocess.run(...), import os as o, ctypes, importlib | asks (shell-spawn) |
a bare import subprocess, with no call | does not ask — the pattern needs the use, not the import |
eval(...) | asks (dynamic-eval) |
| opening a raw socket | asks (raw-socket) |
an HTTP post, or a get with an f-string URL | asks (data-out) |
reading an SSH key or a .env | asks (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.
