Changelog
All notable changes are listed here. The format follows Keep a Changelog, and versions follow Semantic Versioning.
[Unreleased]
Added
just coverprints the unit tests’ coverage ofinternal/without the launcher, counted across packages, and CI fails below 70 %. The new tests took it from 43 % to 83 % (testing).The unit tests run the host side for real, on macOS and Linux, with no SSH and no systemd: session holders as child processes, the bridge in the test’s process, and a pseudo-terminal for the client (
internal/host/hosttest; see testing).setup: a guided setup for a new user (decision 0020). It asks where agents will run: a host in~/.ssh/config, a local VM with Lima, a test container with Docker, an E2B sandbox, or a server you will rent. It says which ones this computer can use now (limactlordockeronPATH, an E2B key), with the exact steps from each project’s install guide for the rest. “A server I will rent” walks through the tutorial’s steps one at a time and checks each one it can: an SSH key,ssh user@address, and a name in~/.ssh/config, whose lines it shows and adds only after a yes. It never rents anything. Then it connects or creates the host, shows a checklist of agents (first-class and supported first, best effort marked; installed ones ticked), installs the ticked ones one after another with their official installers in sessions you watch, and offers each agent’s own login in a session. It ends with a summary and the command for the first session. Ctrl+C at any step stops it and says what was done; a VM or container that Lima or Docker had only begun to make is deleted.--host,--create,--name,--agents,--yesand--no-loginanswer the questions for scripts; without a terminal, the setup waits for each installer and leaves the logins out. See getting started.host addshows the same agent checklist in a terminal when a first-class or supported agent is missing;--yesskips it.The launcher offers the guided setup: Enter on the first-run screen, or
sanywhere. A sheet asks where agents will run, makes a host with Lima, Docker or E2B (Esc stops it and cleans up), or walks through renting a server, then hands the host to the add-host wizard. The wizard gains three steps after “ready”: the agent checklist, each install or login in a session you watch, and a summary whose Enter opens the new-session form on the agent it names.A second tutorial, your first cloud host: rent a server, make a user for agents, connect it, install an agent, and get an alert on your phone when it needs you.
Repository setup scripts that you trust: the first time a session starts in a repository with an executable
.<name>/setup(or in a new worktree of it),newasks whether it may run, and so do the launcher’s quick start and form. The answer is kept on the laptop for that repository on that host, andnew --setupor--no-setupoverrides it. The script runs in the session’s terminal before the agent, after.worktreeinclude’s copy, with the variablesHOOK,SESSION_NAME,SESSION_DIR,WORKTREE_NEWandREPO(each with the product’s prefix); a failing script shows its exit code and the agent starts anyway. See docs/how-to/setup-hooks.md.host doctor --fixanddoctor --fix: the doctors offer to fix what they find, one fix at a time, each shown with what it does and the command it runs. On a host: linger off, the restore unit missing or stale, Ghostty’s terminfo missing (sent from the laptop), a brokencurrenthelper link, stale session sockets, and old helper versions nothing uses (the newest three stay). On the laptop: SSH configs generated for removed hosts, and ControlMaster sockets whose master died. Nothing uses sudo; a fix that needs root is printed instead.doctoron its own now checks the laptop:ssh, the config files (unknown keys too) and the secrets’ commands.Per-session secrets:
[secrets]inconfig.tomlnames each variable and a command that prints it on the laptop, such asop read …orsecurity find-generic-password … -w.new --secret NAME(repeatable) and a host’ssecretslist inhosts.tomlask for them. The client runs each command at the start, with a 60 s limit, and sends the values over the SSH connection; the host keeps only the names, and the session’s holder puts the values in the agent’s environment and nowhere else: no file, log,ls --json, error message or command line. After a host restart the agent starts without them, and its terminal and the launcher say so. Tests check every one of those places. See docs/how-to/secrets.md.agents install <agent> [--host <host>]: runs the agent’s official installer, kept with its source in the agent table, in a session on the host, so you see its output and can answer its questions; then it says how to log in with the agent’s own login, in a session.agents lslists the install commands.host addandhost doctorcheck each first-class and supported agent: installed, onPATHin a login shell, and logged in, read from the agent’s own status command, credentials file or key variable (“unknown” when it has none). See docs/how-to/install-agents.md.Benchmarks for each step of the attach pump, and
just bench(scripts/bench.sh), which compares them with a base commit on the same machine and fails on a slowdown over 10 %; CI runs it on pull requests.just vulncheck, also in CI: govulncheck, and deadcode with tests included, which fails on code nothing reaches. Two unused functions were removed.scripts/time-to-agent.shmeasures the time from a clean machine to an agent running on a host: 2.2 s of machine time on the test host (testing).An explanation page, how simstim compares: multiplexers, pass-through session tools, hosted agents, vendor agents on your own machine and cloud development environments, with when to choose each instead, and where simstim is behind.
host create <provider> [name]makes a host and sets it up in one step: a local Lima VM (lima), a Docker container for testing (docker, experimental) or an E2B sandbox (e2b). It creates the machine, installs the helper (printing what and where), and connects.host destroy <name>asks first, refuses while a session is working unless--force, deletes the machine, and forgets the host.hosts.tomlrecords the provider and the machine in a[host.machine]table. See Create a host.A transport seam: the bridge, the helper’s install and the hello run over SSH,
docker exec -i, or E2B’s envd process API, with no SSH and no new port for the last two. Bridges started over envd end themselves when the client’s pings stop for 30 s.Idle stop for created VMs: a systemd user timer powers the VM off after 30 minutes with no client connected and no session working (
host idle <name> <time|never>; it needs passwordless sudo, andhost doctorsays when it is missing). E2B sandboxes are kept on a lease instead: while a client is connected it is renewed every minute, for 60 minutes while a session works and 15 when none does.Wake on attach for created hosts: the client asks the provider first, and starts or resumes a stopped host before it dials.
lsand the launcher show a stopped host as stopped rather than starting it.CI makes a host in a container with
host create docker, runs the end-to-end suite against it overdocker exec, and destroys it. The suite skips the memory cap check there, since a container has no systemd.E2B’s API key comes from
E2B_API_KEY, the E2B CLI’s own login (read only), or atoken_command; it is never written, logged or sent to a host.The docs site has its own address, simstim.astrel.sh, and release builds take the address GitHub Pages reports, so each version’s docs link to the right place.
pull <host>/<session>(roadmap N7), for hosts reached over SSH: fetches the branch a session’s folder has checked out into a branch of the repository you are in, through the client’s own SSH connection, without touching either working tree.Keep your terminal’s scrollback (roadmap N2): the agent table records each agent’s inline mode (Claude Code’s classic renderer, Codex’s
--no-alt-screen),new --inlineoragents.inlineinconfig.tomlstarts agents that way, and a session started inline comes back inline after a reboot. Each session reports whether it draws full screen (full_screeninls --json), andhost doctornames those sessions and how to start them inline.win the launcher goes to the next session that needs you, on any host, and says what it waits for; pressing it again goes to the one after (roadmap N3).go install github.com/iamlakshya/simstim@latestgives a working client: with no helpers inside it, it fetches its release’s helper bundle (a new release asset,helpers_<version>.tar.gz), checks the release’s SSH signature and the bundle’s SHA-256, and caches it. Its version comes from Go’s build information.Recorded demos from tapes:
docs/demos/*.tapedrive the demo intest/ttyshot, which now records an animated SVG and an asciicast (record start,record save, and atypekey that types like a person).just screenshotsmakes them with the stills. The README opens with the launcher demo and shows a session being joined and left.User docs in the Diátaxis shape: a getting-started tutorial, and how-to guides to install, add a host, use worktrees, open ports, paste images, restore after a reboot, change keys, troubleshoot and uninstall. Every command in them is tested (
just test-docs, and in the docs checks).just public-check(also in CI on every push): the name check, no private details in the tree or the whole history (check-private.sh --history), no secrets in the history (gitleaks), licence information on every file (REUSE:REUSE.tomlandLICENSES/), the workflows (zizmor), the docs checks, and the Scorecard checks the repository controls. It passes.Community files for a public repository: SUPPORT, GOVERNANCE and MAINTAINERS; issue forms for provider requests and docs problems; Discussion forms for ideas and questions;
.gitattributesthat marks generated files. AGENTS.md is shorter and points to the contributor docs, which gain a home page, a guide to how the docs work, and a style guide.A docs site: Hugo with the Hextra theme (
site/), built fromdocs/with search and light and dark themes (just docs-site). CI builds it on every push and keeps it as an artifact; once the repository is public, GitHub Pages serves the latest release at the root, each release at/vX.Y/andmainat/dev/, and each release carries its own docs.Docs checks (
just docs-check, and CI): the generated reference is current, Markdown lint, British spelling (typos), plain English (Vale, with a small style of our own), and every internal link on the built site; links to other sites are checked onmainand weekly.scripts/setup.sh --only <tools>installs only some tools, for CI; Hugo is built withgo installat a pinned version.Reference docs generated from the code (
just docs, withcmd/docgen): every command and flag,config.tomlandhosts.tomlfrom the settings structs’ comments, the launcher’s keys from its command registry, and the agent table. CI fails when they are stale. Releases now include man pages and shell completions.A docs-impact check: CI fails a pull request that changes commands, config, keys or agents without changing the docs and the CHANGELOG, unless it is labelled “no docs needed”.
Alerts with the laptop shut (decision 0019): each session’s holder sends
needs_input,turn_done,failed,oom(and on requestexited,started,restored) to destinations you name in~/.config/<name>/alerts.tomlon the host: an ntfy topic, a webhook (JSON, Slack or Discord) or a command, which gets the event as JSON on stdin and in environment variables. Nothing is sent until you name one.needs_inputwaits 10 s and stays quiet if the session moves on or you type; each session sends one event of a kind a minute, and a host 20 a minute. The agent’s question is sent only if you turndetailon.host alerts <host>shows the settings and last deliveries, andhost alerts set|test|log|offchange, test, explain and remove them;--token-cmdtakes an ntfy token from a command on the laptop, such as a keychain read, without printing it. See docs/how-to/alerts.md.Antigravity CLI (
agy) in the agent table: Google moved Gemini CLI’s consumer users to it on 18 June 2026.A support level for each agent: Claude Code and Codex are first-class (tested end to end on every release, with status hooks and resume), opencode, Antigravity CLI and the shell are supported, and the rest are best effort. The README lists them.
A roadmap that scores every candidate feature by demand, impact and effort (docs/roadmap.md). Next and later items are GitHub issues, eleven of them good first issues, and the labels are kept as code in
.github/labels.yml(synced byscripts/sync-labels.shand the Labels workflow).One-command developer setup:
scripts/setup.sh(orjust setup) installs pinned, checksum-checked tools into.tools/, then builds and tests once. A devcontainer runs the same script, and CI checks both weekly and on every change to them.A versioning and release policy, with the steps to release and to check a release (docs/contributing/releasing.md).
The plan, architecture, UX and research notes.
Project scaffolding: licence, community files, CI, release configuration and decision records.
Host runtime: one holder process per session, with a PTY, an 8 MiB output ring with byte offsets, a manifest, and status from output signals. Each session runs as a systemd user service with a memory cap.
Host helper commands:
bridge,new,ls,rm,doctor,version.The shadow terminal (libghostty-vt in its own process): a new client gets a snapshot of the screen, scrollback and modes instead of raw history, and queries are answered while no client is attached. A crash restarts it; the agent never notices.
Wire protocol with a versioned hello, requests, attach streams and uploads.
Terminal mode tracking, reset without RIS, restore on reattach, and query removal from replayed history.
The input filter for the detach and image-paste keys, in legacy, kitty and modifyOtherKeys encodings.
The client: system OpenSSH with a generated config and a private ControlMaster, helper install with consent and SHA-256 checks, automatic helper upgrades for saved hosts, the bridge connection, and the attach pump with heartbeat, reconnect and replay by byte offset.
Image paste from the clipboard and dropped-file upload, into a private per-session folder.
The launcher: the hosts, then one host’s sessions, one list at a time, with the new-session dialog (agent, folder, name, worktree), rename, delete with a confirm, a preview on Space, the add-host wizard, a command palette, shortcuts and a first-run screen. Colours follow the terminal’s own palette. Attach and detach keep the terminal’s scrollback continuous within one run.
Folder completion in the new-session dialog: recent session folders and git repositories as hints, and Tab completing a typed path from the host.
doctor terminal: one batch of queries (DA1 last) that reports what the terminal supports, which is what agents get through the pass-through.flip: a quick switcher that opens on every host’s sessions. Host details can open an SSH shell on the host (s).Running any command from the new-session dialog (“other…”).
Key rebinding through
[keymap]inconfig.toml, andconfig keymapto print it.demo: the launcher with sample hosts and sessions, where nothing connects anywhere, and a made-up agent that fills the terminal when you attach.just screenshotsuses it to make the README’s screenshots, whichttyshotdraws as SVG from the terminal’s cells.CLI commands:
host add/ls/rm/test/doctor,new,ls,attach,kill,rename,doctor keys,version.End-to-end tests that drive the real client in a pseudo-terminal against a real host (
just e2e).Agent status hooks, as data in the agent table: Claude Code through
--settings, Codex throughnotify. They report prompts, finished turns, notifications and the agent’s own session ID (which follows/clear).send: type text into a session without attaching.A git worktree per session (
new --worktree [--base ref]): its own branch, locked while the session lives,.worktreeincludefiles copied, removed on delete only when nothing would be lost.Setup and resume hooks: an executable
.<name>/setupin the repository (or~/.config/<name>/hooks/setupon the host) runs in the session’s terminal before the agent;resumeruns before a respawn. A repository’s own hooks need your trust first (see above).portsandforward: find the ports a session listens on, and forward them to localhost through SSH.Provider scripts:
host add --provider-script <path> --wake, andhost start|stop|status. A host that does not answer is started first. Examples for GCP and AWS.Status from screen rules, kept as data (one TOML file per agent, patterns adapted from Herdr): the holder keeps a rendered copy of the screen and the rules tell working from waiting for the user. Sessions say where their state came from.
Alerts while attached: a desktop notification when another session on the host needs input or ends.
--dir ~/…on the command line maps the local home folder to the host’s.Respawn after a host restart:
restorebrings sessions back with each agent’s resume or continue command, and a boot-time user unit runs it.host restoreruns it on demand.just check-private(also injust lintand CI): fails when a tracked file holds a personal path, a private or Tailscale address, a tailnet name or an email. Maintainers can add private words of their own in the git-ignoredprivate/deny.txt.
Changed
The strict lint set is now the repository’s own (
.golangci.yml), with a reason beside each exclusion;scripts/check-size.shkeeps Go source files at 500 lines or fewer, and depguard keeps logs onlog/slog. The testing page lists every quality bar and what checks it.The log of each session on the host (
holder.log) is written with Go’slog/slog: each line has the time, a level, a short message and its details askey=valuepairs, as troubleshoot shows. When a session cannot start, the error shows the message and the error from its last line, without the time.Folders that the host helper makes for worktrees, for the files
.worktreeincludecopies, and for systemd user units are now the user’s only (0700). Unit files stay readable (0644), because systemd warns about a unit file that others cannot read.Errors from the host helper say what it was doing, and for which session or file, before the system’s own message: for example
read the manifest of session ab12cd34: …instead of the bareopen …: no such file or directory.The module path is now
github.com/iamlakshya/simstim, in lower case, as the owner’s GitHub account now is:go install github.com/iamlakshya/simstim@latest. Links, the issue forms, CODEOWNERS and the Homebrew cask follow.Host setup says when it turned on linger with
sudo -n(which never asks for a password), instead of using sudo without a word.The keys sheet fits an 80-column terminal: “next waiting session” and “copy attach command” are shorter, here and in the command palette.
host start,host stopandhost statusdrive a created host through its provider, as well as hosts with a provider script.forwardsays plainly that it needs SSH on hosts reached throughdocker execor E2B.host rmon a created host says that its machine is kept.go install github.com/iamlakshya/simstim@latestinstalls the client under its own name: its main moved to the root of the module, and the command tree tointernal/cli.CI is hardened: every action is pinned to a full commit SHA, each job asks for the least token permissions, the checkout keeps no credentials, and zizmor lints the workflows. govulncheck runs on every push, and the toolchain is Go 1.26.9, which fixes two standard-library issues it found.
Releases are signed and traceable: cosign signs
checksums.txtwithout a key (a Sigstore bundle), each archive gets an SBOM, a GitHub attestation and a SLSA level 3 provenance file, and the notes come from this CHANGELOG (scripts/release-notes.sh). A Homebrew cask is built with every release, ready for a tap.Builds are reproducible:
just repro(also in CI) builds every release binary twice from empty caches and checks they match byte for byte.CodeQL and OpenSSF Scorecard run once the repository is public;
just scorecardscores it before then.SECURITY.md has a reporting link and a disclosure timeline.
install.shchecks the release’s signature before the checksum: the release job signschecksums.txtwith an SSH key, and the script verifies it withssh-keygen, which every machine with OpenSSH has. The public key is in.github/release-signers.just test-install(also in CI, on Linux and macOS) installs a signed snapshot release and checks that changed checksums, signatures and archives are refused.The launcher fits a 60×20 terminal: below 70 cells a session list drops its folder column instead of squeezing it to two letters and spilling past the margin. Golden screens now cover 60×20 too.
Tests render the screens in every colour profile (truecolour, 256, 16,
NO_COLOR, no terminal) and check the contrast of the fixed colours on dark and light backgrounds.The repo holds nothing private: the working goal, the status page, the test-host log and the owner’s checklist moved to the git-ignored
private/folder, and examples use generic host names, users and documentation addresses.Sessions are capped with
MemoryMaxonly;MemoryHighmade a runaway process hang (decision 0008).Commits carry no sign-off or attribution lines, and contributions are covered by Apache-2.0 section 5 (decision 0010, which replaces the DCO).
The home screen is now a light launcher (decisions 0009 and 0012). It has one column and one list: the hosts, then Enter for a host’s sessions. The panes and the permanent preview are gone. The look is its own:
- a header with an animated braille logo and the name in the brand gradient, and rules instead of boxes;
- a cyan accent, and a cyan marker and tint on the selected row;
- a braille trace of each session’s recent output;
- lower-case keys;
- sheets that rise from the bottom for every dialog.
Amp’s CLI was the baseline, measured in docs/research/amp-tui.md: colours from the terminal’s palette, faint secondary text and a command palette.
The launcher moves, in the terminal’s own colours:
- on the welcome screen, a woven braille ribbon that opens out and flows, and a name that decodes out of noise;
- a boot sequence, screens that slide, and a selection that glides;
- host names and activity that decode in;
- a scan along the header rule while something is busy, and a short jack-in sweep before an attach;
- notices that slide and fade, a shimmer on work in progress, a breathing dot for a session that needs you, and a tiny Game of Life for one that works.
It asks the terminal for its colours (OSC 4, 10 and 11) to blend smoothly, and falls back to faint and bold steps. One frame tick runs only while something moves, and
ui.motion = offstills it all.The launcher is faster to use (decision 0011):
- it remembers each host’s sessions and draws them at once, before SSH answers, and opens on the session you last attached to;
attach -does the same from the shell;/searches the sessions of every host, andnstarts a session from one line (claude ~/src/app auth) with completion;- a live activity line says what the selected session is doing now, read from its screen;
- rows have numbers (1–9 open them), and the mouse selects, opens and scrolls;
- the tab title counts the sessions that need you, and a desktop notification goes out when one starts to need you while the terminal is in the background.
While attached, the client draws nothing of its own. It sets no window title, and a failed upload is now a system notification instead of a line on the bottom row. The reconnect notice is the one exception. End-to-end checks prove that the agent’s PTY is the terminal’s size, and that the screen is the session’s bytes alone.
The docs site has a landing page in the launcher’s look, with the launcher itself, live, in the first screen, and a three.js field of braille that loads only with motion on, on a wide screen. The docs overview moved to
/docs/; every other page keeps its address. Links between pages, and toCHANGELOG.md,CONTRIBUTING.md,SECURITY.mdand the other files at the root, now stay on the site, andjust docs-checkfails if one goes to GitHub instead. One motion switch, in the footer and under the launcher, stops every animation. See decision 0021.
Fixed
newwith a program that cannot run, such as a file that is not a program, said the session had started when the host was busy, or when the session had secrets. It now always fails, and says why.- On a host without a systemd user manager, a session’s holder took any systemd service it ran in, such as a CI runner’s or a sandbox’s own, for the session’s unit, so
portslisted the ports of every program in that service. A holder now takes only the unit made for its session; elsewhere it counts the agent’s own processes. - After a detach on Linux, the next attach in the same terminal could lose the keys typed into it: the old attach’s reader of the terminal was still running, and it was closed under that reader. Each attach now stops its reader before it returns.
- An attached client could miss an agent’s last output, often the reason it ended: the holder announced the end before it had read that output from the terminal. It now reads it first.
- A deleted session could come back in
ls, as ended: its holder, slow to exit, saved its manifest once more afterkillhad removed the session’s folder. Nothing saves after a holder’s last save now. - A session’s holder saved its manifest without its lock when the agent exited, while other goroutines could change it: a data race, which the new unit tests found.
- Old helper versions piled up on hosts, about 5 MB for each version installed; a test host had 33 of them. After each install the new helper now removes the versions that nothing uses: it keeps the current one, any that a session’s holder or shadow terminal runs from, any that a unit file names, and any installed in the last hour.
check-private.sh --historycould miss a match on macOS:grep -qstopped reading early, the writer died of SIGPIPE, and underpipefailthe match went unreported. Linux CI caught Dependabot’s sign-off address on its branches, which is now allowed with the other bots’ addresses.scripts/check-scripts.shnever ran on macOS, whose grep has no-P; it uses perl now, and no longer flags its own comment on Linux.newcould read a session’s record before the host’s holder wrote the agent’s start, return a row that had not started, and save it over the holder’s record. It now waits up to 5 s for the start.- A host with no sessions, or one that is offline, no longer offers attach, peek and rename in the launcher’s footer: it shows the keys that work there.
- Old helper versions piled up on hosts, about 5 MB for each version installed; a test host had 33 of them. After each install the new helper now removes the versions that nothing uses: it keeps the newest three, the current one, any that a session’s holder or shadow terminal runs from, any that a unit file names, and any installed in the last hour.
host doctor --fixuses the same rule. - On a Mac whose cache folder has a long path,
host addfailed with “ControlPath too long”: OpenSSH’s socket path went over macOS’s 104-byte limit. The shared connections’ sockets now move to a private folder in/tmpwhen the cache path is too long, and the client checks that the folder is its own. scripts/setup.shhad the same ellipsis bug asinstall.sh;just lintnow checks every script for it.install.shstopped with “unbound variable” in shells that read the…after$archas part of the name.- On a light background, the end of the brand gradient showed pink: blended colours with a channel near zero were sent as their 16-bit value. The gradient is now sent as plain 8-bit RGB.