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.
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.
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.
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.
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.