Persistent Python sessions. Code in, result out.
Variables, connections, and threads survive between calls. Agents send code through one-shot commands. Humans attach a REPL. Same namespace.
pip install pythond # zero dependencies
Python 3.10+. Three entry points:
| Command | Role | Like |
|---|---|---|
pythond | daemon | sshd |
pysh | session client | function call / attach |
pyctl | daemon control | systemctl |
ns = {}
while True:
code = receive()
exec(code, ns) # ns stays alive -- variables survive
send(captured_stdout)
Everything pythond adds is that loop plus delivery: thread-safe stdout capture, REPL semantics (the last expression auto-prints), one subprocess per named session, async cells, and local HTTP so one-shot CLI calls reach the live process. Transport is borrowed, never built — there is no WebSocket stack, no TLS stack, no PTY bridge, and no remote proxy in the codebase.
pythond daemon pysh new work pysh run work "x = 42" pysh run work "x + 1" # 43 pysh attach work
pysh run work "import sqlite3; db = sqlite3.connect('app.db')"
# ... 100 turns later ...
pysh run work "db.execute('SELECT count(*) FROM users').fetchone()"
# (42,)
Connection ≠ state. Every call is a fresh connection to the same live process. The namespace is the workspace.
# fire: thread, shares namespace
pysh fire work "model = train(X, y)"
pysh poll work abc123
# {"cell_id":"abc123", "status":"done", "output":"..."}
pysh run work "model.score(X_test)" # model is there
# fork: child process (POSIX only), killable, pickles vars back
pysh fork work "results = expensive_search(params)"
pysh int work # SIGKILL the fork (POSIX)
Complex code with quotes, f-strings, or SQL? Post the file as the cell. The request body is raw Python source — never JSON-escaped, never shell-quoted.
cat > /tmp/task.py << 'EOF'
import pandas as pd
df = pd.read_csv("data.csv")
print(f"rows: {len(df)}, cols: {list(df.columns)}")
EOF
pysh run work @/tmp/task.py
State lives in the remote daemon, not in the connection, so one-shot calls over ssh are enough:
ssh server pysh run work "x = 42" ssh server pysh run work "x + 1" # 43 (remote state) ssh -t server pysh attach work # interactive
Per-call latency? That is what ssh ControlMaster is for:
# ~/.ssh/config
Host server
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
Need a TLS endpoint anyway? Terminate it with nginx or caddy in front of the loopback port. pythond does not ship a TLS stack.
pysh speaks plain HTTP over a local socket; so does curl:
curl --unix-socket $XDG_RUNTIME_DIR/pythond/pythond.sock \
--data-binary '1 + 1' http://pythond/run/work # 2
curl --unix-socket ... --data-binary @task.py http://pythond/run/work
GET /ls text listing
POST /new/<name> create session
POST /run/<name> body=code raw output; X-Pythond-Exec-Error: 1 on traceback
POST /fire/<name> body=code {"cell_id": ..., "status": "fired"}
POST /fork/<name> body=code {"cell_id": ..., "status": "forked"}
GET /poll/<name>[?cell=ID] JSON cell result
GET /status/<name> JSON health
GET /vars/<name> JSON namespace names
POST /complete/<name> body JSON completion matches
POST /int/<name> JSON interrupt report
GET /pickle/<name>[/<var>] pickled var (or whole picklable namespace)
POST /pickle/<name>[/<var>] unpickle body into var (or merge a dict)
POST /kill/<name> kill session
POST /stop stop daemon
404 no such session, 409 session channel broken,
401 bad token.
run moves source code; /pickle moves live
objects — the fork merge-back mechanism, generalized. pysh cp
gives it scp syntax: a side is session:var, session:
(the whole picklable namespace), or a file path.
pysh cp work:df df.pkl # session -> file pysh cp df.pkl gpu:df # file -> session pysh cp work:model gpu:model # session -> session pysh cp work: backup: # clone the picklable namespace
Unpicklable values (sockets, locks, modules) are skipped and reported.
POSTing a pickle is arbitrary code loading by design — the same trust
boundary as /run.
pysh attach work is a client-side line REPL: readline history
and tab completion live in the client, every complete block runs as one cell
in the shared namespace. Ctrl-D detaches; the session stays alive
(pysh kill ends it). It is line-oriented, not a PTY.
pysh new <name> create session pysh run <name> "code" sync exec, raw output pysh run <name> @task.py post a file's contents as the cell pysh fire <name> "code" async thread, shares namespace (can't kill C code) pysh fork <name> "code" async process (POSIX), killable, pickles vars back pysh poll <name> [cell_id] check async result pysh int <name> interrupt (fire=best effort, fork=kill) pysh kill <name> terminate session pysh ls list sessions pysh status <name> JSON health pysh vars <name> JSON namespace names pysh complete <name> "text" JSON completion candidates pysh attach <name> line REPL (Ctrl-D to detach) pysh cp <src> <dst> copy pickled objects (scp syntax) pyctl start [--show-token] start daemon in foreground pyctl stop stop daemon pyctl status daemon liveness
Treat pythond like SSH into a Python runtime. Not a sandbox: code runs with the daemon user's OS permissions. Once connected, a client has full access to all sessions — the same as a login shell.
| Mode | Auth |
|---|---|
| Local POSIX | AF_UNIX socket, mode 0o600 — filesystem permissions |
| Local Windows | 127.0.0.1 + bearer token in %LOCALAPPDATA%\pythond |
| Remote | ssh's problem, on purpose |
The daemon never binds a non-loopback address. There is no network listener to harden.
Session names are lowercase only: a-z, 0-9,
underscore, and hyphen, up to 80 characters. Windows reserved device names
such as CON, NUL, COM1, and
LPT1 are rejected.
Like shell history under SSH, pythond session history and live namespaces
can expose secrets. history.py may contain executed Python
source. Do not paste API keys, passwords, tokens, or other secrets into cells
unless you are willing for them to persist in that session and its local
files.
| Platform | Transport | Notes |
|---|---|---|
| Linux / macOS / WSL | HTTP over AF_UNIX | full featured |
| Windows | HTTP over 127.0.0.1 + token | full featured except fork (no COW fork) |
None. Python standard library only.