Blog

Clusy has a terminal now

A real shell in your project's sandbox, one keystroke from the notebook. Same files and same environment as your cells, so what you install in the shell, the next cell imports.

Product5 min read

Terminal was the thing people asked us for most. By a distance.

It's live today. Hit Ctrl + ` and a dock comes up under the notebook with a real shell in your project's sandbox. pip install, git, ls, htop. vim, if you're that way inclined.

Yes, we shipped an IDE without a terminal first. People used it anyway, and then they told us what was missing, which is more or less how this works.

churn.ipynb❯_TerminalIn [2]from rich import printprint(summary)A CELLchurn $pip install richSuccessfully installed rich$A PROMPTONE SANDBOXsame user, same files, same interpreter/home/user/home/user/.venv
Two ways in, one machine. What the shell installs, the next cell imports.

The same machine, not a copy

Your shell runs as the same Unix user the agent runs as, in the same /home/user, against the same /home/user/.venv. The rest of this post leans on that, so here is what it means in practice.

$ pip install xgboost
Successfully installed xgboost-3.1.4

import xgboost now works in the next cell. No restart, no sync step, nothing to click. The package went into the interpreter your cells were already using, because there is only the one.

It works the other way round too. Whatever your notebook wrote turns up in ls. Whatever you curl down is sitting there for the agent's next run_cell. git status is the repo the agent has been committing to.

One take, sped up. Run a cell, open the dock, start a shell, install a package, then use it from the next cell against the DataFrame the kernel already had.

The execution slot is the one thing we kept apart. Your shell is not the Jupyter kernel, it is its own process, so a twenty-minute training cell and a slow pip install can run at the same time and neither of them stops the agent working.

OS packages go through sudo clusy-apt install <pkg>, which is the only thing sudo will touch. More on that further down.

The agent reads your scrollback

We spent longer on this than on the shell itself.

Every command gets journaled against the project: the text you typed, where you were, the exit code, how long it took, and a clipped tail of whatever came back. A compacted version of the recent ones travels with the agent's context, labelled as yours rather than its own.

TERMINAL JOURNAL[-3m]$ pip install xgboostexit 0 · 12.3s[-1m]$ ls data/exit 0 · 0.1s[-12s]$ head -3 train.csvexit 0 · 0.2sDIGESTClusyYou installed xgboost a fewminutes ago, so I’ll import itrather than install it again,and train on train.csv.terminal_history(limit=20)… when it needs the full log.
Every command is journaled per project. The agent reads a compacted digest of it, not your whole scrollback.

Install something, say "now train on it", and it already knows the package is there. head a CSV, ask about a column you spotted, and it knows which file you mean. When the digest is not enough it calls terminal_history for the full log instead of dragging your entire scrollback around behind it.

Two details worth putting on the record.

The digest is capped at 1,200 characters and renders below our prompt cache boundary, so terminal activity cannot invalidate the cached system prompt. On a turn where you have not touched the shell, it costs nothing at all.

The journal also has two sources that do not depend on each other. The shell emits markers around each command. Separately, the bridge keeps a small line-editor model of the bytes it forwards. Run exec zsh and lose the hooks, or type inside a nested REPL, and the reconstructed version still lands, flagged as reconstructed. When both turn up for the same line the shell's wins, and you do not get a duplicate.

What persists, honestly

Terminals hold state, so here is what survives what.

Reload the page and your shells are waiting. They sit parked server-side for a grace window, the client re-attaches and replays the buffered tail, and you come back to the same directory looking at the same scrollback.

Files under /home/user persist with the workspace, same as anything else you or the agent writes.

Packages you pip install get picked up by the environment freeze and come back with the project.

Packages you install with clusy-apt do not. Re-provision the sandbox and they are gone, and you will install them again. The terminal says so the first time you use it in a session, rather than letting you find out three days later.

Pause the sandbox and the shell dies, because the process it was is no longer running. The tab tells you, and offers you a new one. An honest dead terminal beats one that looks alive and has quietly lost its process underneath.

The parts you can't reach

A shell in a sandbox is a security surface. We drew the boundary with kernel primitives rather than a list of things to watch out for.

Every terminal launches into its own private mount namespace. Before your shell starts, empty filesystems go over the top of our runtime scripts, the checkpoint machinery, the transfer helper and the token it holds, and the Jupyter connection files. They are not hidden from ls. They are not there. /proc carries hidepid, so other processes' command lines are not visible either.

No ambient capabilities, no inherited privilege. E2B's base image hands uid 1000 passwordless sudo; we take that away at provision time, lock the account passwords and strip setuid bits. What is left is clusy-apt, which accepts package names matching a strict pattern and refuses everything else.

Commands get scanned before they run, and a short list of families gets refused outright: cryptomining, proxy and tunnel setup, sandbox escapes, network attack tooling. A refusal shows up as one dim line in your scrollback, sitting where that command's output would have gone.

Credentials are never on that list. Reading your own .env, exporting a key, piping printenv somewhere useful, that is ordinary work in your own sandbox and the gate has no business standing in front of it. No raw command text leaves the sandbox bridge either way, whatever the family.

About the dock

One 32-pixel row holds the lot: your tabs, a new-terminal button, an overflow menu, hide. Drag the top edge to resize it. Ctrl + ` toggles, Ctrl + Shift + ` opens another shell, Esc hands focus back to the notebook. It picks up whichever of the six themes you are running.

The prompt is just the working directory. A sandbox has one user and one host, so user@clusy on every line is noise nobody needs. On a branch you get the branch's name where its directory would be, so fork-e6ea1f $ rather than a UUID.


Open a notebook and hit Ctrl + `.

If something you want to do in there does not work yet, tell us. That list is how the terminal got built in the first place.