- Python 89%
- Shell 11%
pyrepl #
macOS-support fork of https://tangled.org/poggers.website/pyrepl.
Named, persistent Python sessions for Claude Code (and for you, from the shell).
A session is a Python process that keeps its namespace — imports, loaded data,
functions, open connections — between tool calls and across conversations,
until it is killed. Claude uses it the way you use a Jupyter kernel: load the
200 MB log once, then ask twenty questions of it, instead of re-parsing it in
twenty python3 -c calls.
$ pyrepl exec chirp 'import numpy as np; x = np.load("/var/tmp/cap.npy")'
[Created session 'chirp' (python 3.12.15, cwd /home/me/work).]
$ pyrepl exec chirp 'x.shape'
Out[2]: (480000,)
$ pyrepl sessions
chirp pid 81234 python 3.12.15 up 2m idle 40s 2 cells 210 MB cwd /home/me/work
names: np, x
Let Claude install it #
Paste this into Claude Code:
Install pyrepl from https://tangled.org/alice.strange.domains/pyrepl — clone it and follow its AGENTS.md.
It checks prerequisites, clones to ~/tools/pyrepl, registers the MCP server,
runs the tests, and asks before touching your ~/.claude/CLAUDE.md.
Install by hand #
Requirements: Linux or macOS, an MCP client (or use the CLI directly), and Python ≥ 3.8 for the client and daemon
(python3 on PATH; no third-party packages). Sessions
themselves can run on any Python ≥ 3.6, e.g. a project venv.
git clone https://tangled.org/alice.strange.domains/pyrepl ~/tools/pyrepl
claude mcp add -s user pyrepl -- ~/tools/pyrepl/pyrepl mcp
Restart Claude Code; /mcp should list pyrepl with five tools. Optionally put
~/tools/pyrepl on your PATH to use the CLI.
To have Claude reach for it by default, add something like this to your
~/.claude/CLAUDE.md:
Use the pyrepl MCP for running Python instead of
python3 -cor throwaway scripts. Callsessionsfirst and reuse a matching session.
Remove it with pyrepl stop and claude mcp remove -s user pyrepl.
How it fits together #
Claude Code ──stdio MCP──▶ pyrepl mcp ──unix socket──▶ pyrepld ──pipes──▶ worker.py (one per session)
pyreplis the client.pyrepl mcpis the MCP server Claude Code launches; the other subcommands are a CLI for you.pyrepld.pyis the daemon that owns the sessions. The client starts it on first use as a transientsystemd --userunit namedpyrepld, withMemoryHigh=2G/MemoryMax=3Gcovering all sessions together (UNIT_PROPSinpyrepl). Without a user systemd manager it falls back to a plain detached process, with no memory limit. It survives logout only if lingering is enabled (loginctl enable-linger), and never survives a reboot.worker.pyis one session: a process holding one namespace, started by the firstexecunder a new name, in the caller's cwd and environment.
MCP tools: exec, wait, interrupt, kill, sessions.
CLI:
pyrepl exec NAME 'code' [-t SECS] [--python PATH] # or: echo code | pyrepl exec NAME -
pyrepl wait NAME [-t SECS]
pyrepl interrupt NAME
pyrepl kill NAME
pyrepl sessions [NAME]
pyrepl start | stop
Behaviour #
- Runs are synchronous up to a timeout.
execwaitstimeoutseconds (default 30, max 600). If the code is still going, the call returns, the code keeps running, andwaitcollects the output and result later. A session runs one cell at a time; use another session for parallel work. - REPL semantics. Output from
print(), subprocesses and C code is captured; if the last statement is an expression itsreprcomes back asOut[n]. Sessions run as a real__main__, so their functions pickle.input()gets EOF, andsys.exit()is caught; the session stays up. interruptraisesKeyboardInterruptin the running cell (it works even insidetime.sleep). If the cell is stuck in C code that never checks for signals,killthe session.- No persistence, no replay. A namespace lives exactly as long as its
process. Every cell is appended to
~/.local/state/pyrepl/<host>/<session>.pyfor reference; nothing ever re-runs it. Write results that matter to files. - Losses are reported. When a session dies (kill, crash, OOM, daemon stop) a
note is written, and the next
execunder that name opens with what was lost and where its code is.wait/interrupton a dead session say how it died. - Every reply mentions other sessions with runs still going or results nobody has read yet.
- Sessions are per host and per user. With an NFS home, the MCP config
follows you to every machine, but each machine runs its own daemon; state is
kept under
<host>/so machines don't overwrite each other's records. - Output caps: a reply carries at most 30 000 chars of output (head and
tail kept), a result
reprat most 10 000 chars, and up to 2 M chars of unread output are buffered per session.
Security #
A session runs arbitrary code as you, so access is limited to you:
- The socket lives in
$XDG_RUNTIME_DIR/pyrepl/(mode 0700). If that variable is unset,/tmp/pyrepl-<uid>/pyrepl/, which the daemon refuses to use unless it is a real directory owned by you. - The daemon checks each connecting peer's uid (
SO_PEERCREDon Linux, libcgetpeereidon macOS), and the client checks the daemon's, since everyexecsends the caller's environment. - Transcripts can contain secrets that were in your code; their directory is created 0700.
There is no network listener.
macOS #
The CLI and MCP server run with python3 from PATH, or an explicitly selected
interpreter: python3 /path/to/pyrepl/pyrepl mcp. The daemon uses the client's
interpreter; new sessions default to that interpreter unless PYREPL_PYTHON
or --python selects another one. Python 3.6 compatibility applies to the
worker, not the client/daemon.
On macOS the daemon starts as a detached process, not a systemd service or
launchd job. There is no aggregate memory cap in this mode. pyrepl stop
stops the daemon and discards all its sessions. Memory reporting uses /bin/ps.
Both ends retain kernel-backed peer UID authentication; a credential lookup
failure is an error, never a permission to trust the socket path.
Troubleshooting #
- Daemon log:
$XDG_RUNTIME_DIR/pyrepl/pyrepld.log(also in the journal, if you can read it:journalctl --user -u pyrepld). pyrepl sessionsshows each session's memory. A session killed by SIGKILL while the daemon runs under systemd most likely hit the sharedMemoryMax; kill idle sessions or raise the limit inUNIT_PROPS, thenpyrepl stop(it restarts on the next call).- After updating the code, run
pyrepl stopso the next call starts the new daemon. That discards every live session.
Tests #
./test_pyrepl.sh
python3 -m unittest -v test_platform_support
Runs the CLI against a throwaway daemon (its own XDG_RUNTIME_DIR and
XDG_STATE_HOME), so it doesn't touch your real sessions.
The shell suite accepts PYREPL_TEST_PYTHON=/path/to/python3. On macOS its
descriptor-leak check uses /usr/sbin/lsof; realtime-signal coverage is
explicitly skipped because those signals are unavailable. Platform unit tests
also cover Linux selection and startup via mocks; they do not replace running
the full suite on Linux.
History #
Adapted from house-repl, a single-namespace REPL server on
TCP 127.0.0.1:9399. The main changes: one process per named session instead of
one shared namespace; a uid-checked unix socket instead of an unauthenticated
TCP port; every run starts synchronously with a timeout instead of guessing from
the code text which runs are slow; captured output; real interrupts.