kore
Controls tmux sessions named kore-*, starting and stopping them, capturing pane contents, injecting keystrokes, and streaming terminal interaction across server and worker nodes.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@korelist my active Claude sessions on all machines"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
kore
Manage kore sessions — interactive Claude Code CLI running inside tmux across one or
more machines — and watch/drive them from a web dashboard, or expose them to an agent
as MCP tools. One command to start, no environment setup, stdlib only.
A "kore session" is a tmux session named kore-<name> on a machine running claude
interactively. The engine starts/stops those sessions, captures their panes, and types
into them over tmux (send-keys / capture-pane / control-mode attach).
Server and client
kore has an explicit two-role model — there is no transport to choose and nothing is hardcoded:
kore serverstarts the central node: the dashboard, the engine, and the agent registry. The server is also a worker, so on a single machinekore serveris all you need — the engine runs that host's tmux commands locally.kore client --server <ip:port> --token <tok>joins another machine as a worker. You don't type that line by hand: the server prints it (k3s / tailscale style), and you paste it on the new machine. The client runs a small stdlib agent that registers itself into the roster and answers the server over HTTP.
The engine routes per node: a command for the server's own host runs locally; a command for a client runs over HTTP against that client's agent. No Ray, no shared filesystem, no cluster manager.
browser ─HTTP─▶ server.py ─▶ koreops.py ─▶ kore-bridge.py ─┬─self──▶ subprocess on the server host ─▶ tmux kore-<name>
xterm.js (WB token) (write door) (engine) │
terminal └─client▶ kore-agent (on a client) ─HTTP─▶ tmux kore-<name>
agent registry ◀── each client's agent POSTs /api/agents/register every ~15sEnv naming: every setting is a
KORE_*variable (KORE_AGENT_TOKEN,KORE_NODE_IP, …). This is the only spelling the code reads.
Related MCP server: claude-mux.mcp
Quickstart (one machine, zero config)
Install with one line, then run kore. No config files, no token to copy — all runtime
state lands under ~/.kore/:
curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh
kore # start + print a linkkore is stdlib-only, so the installer needs nothing but Python ≥ 3.8 and curl/wget —
no pip, no uv, no virtualenv. It downloads the latest release into
~/.local/share/kore, puts a kore launcher in ~/.local/bin, and adds that dir to your
PATH if needed (open a new shell afterwards). Installer options go after sh -s --:
curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh -s -- --version v0.3.1
curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh -s -- --no-modify-path
curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh -s -- --uninstallkore (same as kore server) mints the tokens it needs, starts the dashboard in the
background, waits for it to come up, and prints a ready-to-click link with the access
token already in it — one click, no login — plus the join command for adding more
machines:
kore server is up — pid 12345, prefix=kore-
👉 open the dashboard (token included — one click, no login):
http://<server-ip>:8198/?token=<token>
http://127.0.0.1:8198/?token=<token>
add another machine — run this on it (needs network both ways):
kore client --server <server-ip>:8198 --token <agent-token>
(kore not installed there yet? curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh)
logs: ~/.kore/server-<host>.log
stop: kore downkore status reprints the link; kore down stops it; kore upgrade updates in place.
Re-running kore is idempotent; if you pass config that disagrees with a board that's
already up it warns and tells you to kore server --restart (which tears the board
down and brings it back with the new config) instead of silently ignoring the flags.
Other ways to run it — all stdlib only:
uv tool install git+https://github.com/Tiiiktak/kore@v0.3.1 # via uv instead of install.sh
git clone https://github.com/Tiiiktak/kore && cd kore
./kore doctor # preflight: tmux/claude on PATH, tokens, node-ip…
./kore # same launcher, no installFrom the dashboard you can list sessions, create/kill a kore session, and open a live xterm.js terminal into any pane (keystrokes, resize, scroll). Sessions are grouped by the machine they run on (the server's own host first, then each client, each with its own color). Every session and machine gets a small ASCII icon — fruits and animals, picked automatically from its name — and you can give it a nickname or pick another icon by clicking that icon (saved on the server, so every device sees the same names). The session tabs sit along the top or, on wide screens, in a left sidebar — toggle with the button at the top right.
Add another machine
The dashboard shows exactly what to run: click + 添加机器 (next to + 新建) for the
install line and the join line, each with a copy button, plus the state of every machine
that has joined. The same join line is printed when you start kore. On the new machine:
curl -fsSL https://raw.githubusercontent.com/Tiiiktak/kore/main/install.sh | sh
kore client --server <server-ip>:8198 --token <agent-token>There is no default for --server or --token; both come from the server's join
line. kore client runs in the background (like the server) and only returns once the
server has accepted the machine, so problems are reported right away instead of as a node
that never shows up:
server not reachable from this machine → says so (check the address and port);
wrong token → refused before anything is started or stopped;
joined, but the server cannot connect back to this machine's agent → joins anyway and tells you which port to open (the node is listed, but its sessions cannot be driven yet).
kore status shows the client's state, kore down takes the machine out again, and
kore client --foreground … runs it attached to the terminal (systemd / debugging). The new
node appears in the roster at once, and you can start and drive sessions on it exactly like
the server's own node.
Network reachability is mutual — open both directions:
client → server on the dashboard port (default
8198): the client POSTs its heartbeat there.server → client on the agent port (default
8199,KORE_AGENT_PORTto change; if 8199 is taken the agent picks a free port and reports it): the engine POSTs/execand streams/tapthere.
The kore command
command | what it does |
| start the dashboard in the background, print a tokened link and the |
| join THIS machine as a worker node, in the background (paste the server's join line); no defaults. |
| bring tmux sessions that already run on this machine under kore: with no name it lists the ones without the prefix; |
| stop whatever kore runs on this machine (the server and/or a client) |
| what runs on this machine: the server (with its link) and/or a client (with its registration state) |
| update kore in place to the latest release — |
| print the installed version |
| run the MCP-over-stdio server (stdout = protocol JSON only) |
| the write-door / engine CLI ( |
| preflight the environment; PASS/WARN/FAIL table |
The cross-command "cluster identity" vars are top-level flags — put them before the
subcommand so server/client/mcp can't disagree: --agent-token, --agents-file,
--prefix, --node-ip. Example: kore --agent-token "$TOK" server.
Any tmux session whose name carries the prefix (kore-* by default) shows up on the board
automatically, on whichever node it lives. A session you started yourself under another
name can be added with kore adopt <name> on that machine, or from the dashboard
(+ 已有 session): kore renames it into the prefix; nothing inside it is restarted.
What's in the box
kore/ repo root
├── install.sh one-line installer (`curl -fsSL … | sh`)
├── kore clone-and-run launcher (stdlib, no install)
├── pyproject.toml packaging (hatchling; console-script `kore`)
├── tests/ test_transport.py (self-local + http-client routing)
└── src/kore/
├── cli.py the CLI dispatch + env defaults + doctor
├── kore-bridge.py engine; also an MCP-over-stdio server
├── koreops.py the single write door; reads the policy below
├── kore-agent.py per-node HTTP agent run on a client: /exec /tap /ping
├── dispatch-allowlist.json the kore write policy (nodes / verbs / protected / limits)
└── workbench/
├── server.py the HTTP dashboard + SSE terminal server
├── state.py holds one process-wide bridge instance
├── index.html the dashboard UI (xterm.js terminal, session list)
├── service.sh bare start/stop/restart/status (for systemd ops)
└── vendor/xterm/ pinned xterm.js (served locally, no CDN)Prerequisites
The server host: Python 3.8+,
tmux, and theclaudeCLI onPATH(or pass an explicitclaude_pathon start). Stdlib only — no third-party runtime dependencies.Each client machine: the same — Python 3.8+,
tmux,claude. It runs the agent (stdlib only). The server must be able to reach the client's agent port over the network, and the client must be able to reach the server's dashboard port.tmux 3.0+ is recommended, not required. Older tmux works with degraded features (below 2.9 the dashboard cannot fit the window to the browser).
kore doctorreports your tmux version and prints concrete upgrade commands for your system (package manager, Homebrew, conda-forge, or building from source), andkorewarns at startup when tmux is old.
Window size, browser vs your own terminal. A tmux window has one size shared by every
client. kore follows whoever is using it: while the dashboard has focus it fits the window
to the browser; the moment you switch away (to your own tmux attach, another window or
tab) the window is handed back to tmux and your terminal gets its own size back. This works
on every tmux version (it emulates tmux 3.1's window-size latest).
Configure
Everything is behind a KORE_* env var with a sane default — nothing is hardcoded. All
runtime state defaults under ~/.kore/ (override with KORE_STATE_HOME). Every variable
below is a KORE_* name — that is the only spelling the code reads.
env | default | meaning |
| (minted) | server↔client agent secret, and the client-join token. |
|
| the client-agent registry (node_ip → url) the server writes |
|
| seconds before a silent client is pruned from the roster |
|
| client agent bind port (falls back to a free port when 8199 is taken); |
|
| client agent bind address |
| (set by | the server's web base URL the client POSTs to; |
| (autodetect) | override a node's advertised IP (use the reachable one) |
|
| dashboard bind address / port |
|
| WB (browser↔dashboard) shared-token file |
Other engine env (all optional): KORE_BRIDGE_PREFIX (tmux name prefix, default kore-),
KORE_REGISTRY (the separate name→node session registry, default
~/.kore/sessions.json), KORE_MAX_OUTPUT_CHARS, WB_PORT_ALSO, WB_MAX_STREAMS,
WB_STREAM_*.
Scope the write policy in src/kore/dispatch-allowlist.json → kore:
nodes:["*"]= any routable node (the server's own host, or any client with a fresh agent), or list specific worker IPs.deny_nodes,protected_sessions(read-only names),verbs,cwd_roots,allowed_keys,max_per_minute, … — see the comments in the file. Edits take effect with no restart.
HTTP endpoints (all need the WB token except /api/health)
GET /api/state · /api/kore/roster · /api/kore/read · /api/kore/stream (SSE line-diff) ·
/api/kore/bytes (SSE byte relay) · /api/kore/status · /api/kore/jobs · /api/kore/job ·
/api/agents (the client registry, TTL-pruned) · /api/join (the add-a-machine panel).
GET /api/kore/unmanaged?node_ip= lists a node's tmux sessions without the prefix.
POST /api/kore/{start,stop,send,keys,input,resize,adopt} drives a session (adopt renames
an existing session into the prefix).
POST /api/labels sets a nickname / icon for a node or session (dashboard-only, no node is touched).
POST /api/agents/register is the client registry-push door — fenced by KORE_AGENT_TOKEN,
a different token domain from the dashboard's WB token.
Reads accept Authorization: Bearer <tok> or ?token=<tok>. Writes accept the
Bearer header only, and must echo confirm: "<verb>:<node_ip>:<session>".
Run as an MCP server (for an agent)
kore mcp is a standalone MCP-over-stdio server (stdout carries protocol JSON only;
logs go to stderr). Example MCP client config:
{
"mcpServers": {
"kore": {
"command": "uvx",
"args": ["--from", "git+https://github.com/Tiiiktak/kore@v0.3.1", "kore", "mcp"],
"env": { "KORE_AGENT_TOKEN": "<shared-secret>" }
}
}
}Tools exposed: kore_cluster, kore_list, kore_start, kore_send, kore_read, kore_keys,
kore_status, kore_stop, node_run, node_read.
Policy scope:
dispatch-allowlist.jsonis enforced only on the web path (server.py→koreops.py). The MCP tools call the engine directly and do not consult the allowlist — anyone who can speak to the MCP server's stdin can drive any session on any node the engine can route to. Only expose it to a trusted agent.
Security notes
The dashboard port carries writes (start/kill claude sessions, type into panes). One shared WB token is the whole fence — keep
WB_HOST=0.0.0.0only with that token in force, or setWB_HOST=127.0.0.1and tunnel.The WB token may appear in URLs (EventSource /
<script src>can't send headers); it is redacted from the server's own access log but treat it as sensitive.Two distinct token domains — do not conflate them. The WB token (browser→server, == the share-link token) is NOT the agent token.
KORE_AGENT_TOKEN(server→client, the client-join secret, and thePOST /api/agents/registerfence) is a separate secret; a browser token can never register a client or reach a node, and an agent token can never drive a pane.The engine will
POST /exec(arbitrary command execution) to any client with a fresh registry entry whose agent honorsKORE_AGENT_TOKEN. It is leak-sensitive: anyone holding the agent token who can reach a client's agent port has RCE on that machine. Set a strongKORE_AGENT_TOKEN, keepagents.jsonand the token off world-readable paths, and prefer explicitnodesover["*"]. An empty token disables client joins entirely, and a client agent then refuses to bind anything but127.0.0.1./tapis read-only by construction (tmux -C attach -r).
Contributing & releases
See CONTRIBUTING.md. CI runs py_compile, the routing tests,
bash -n service.sh, JSON validation, and a packaging smoke that installs the wheel and
proves every data file shipped. Cutting a release: push a vX.Y.Z tag (matching
src/kore/__init__.py) and the release workflow builds the wheel + sdist and attaches
them to a GitHub Release. Users then run uvx --from git+…@vX.Y.Z kore ….
License
MIT — see LICENSE. Bundles xterm.js under its own MIT license
(src/kore/workbench/vendor/xterm/LICENSE-*.txt).
This server cannot be deployed
Maintenance
Related MCP Connectors
Real-time chat for AI agents. Claude Code, Cursor, Cline and Codex join channels over MCP.
Work on your own computers from Claude or ChatGPT: run commands, edit and search files.
Remote MCP server exposing your Node Command fleet (files, shell, processes, browser) to AI hosts.
Real-time chat hub for AI agents — Claude Code, Cursor, Cline, Codex over MCP or REST.
Related MCP Servers
- AlicenseAqualityCmaintenanceAn MCP server that gives orchestrator agents fine-grained control over interactive Claude Code sessions running inside tmux, enabling mid-session steering, interruption, and token-efficient result extraction.15MIT
- AlicenseNot gradedqualityDmaintenanceGives Claude Code terminal control and multi-agent coordination through tmux sessions.4MIT
- AlicenseAqualityDmaintenanceMCP server for orchestrating multiple Claude Code instances via tmux, enabling spawning, reading, sending, listing, and killing sessions.528 npm2MIT
- AlicenseAqualityAmaintenancePersistent tmux-backed MCP server that lets Claude Code control Codex CLI's interactive TUI, including slash commands and $imagegen. Run local shells, SSH, Docker, WSL, REPLs, and supported coding agents through durable PTY sessions.195,268 npm6MIT