Skip to content
Changelog

Changelog

All notable changes are listed here. The format follows Keep a Changelog, and versions follow Semantic Versioning.

[Unreleased]

Added

  • just cover prints the unit tests’ coverage of internal/ 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 (limactl or docker on PATH, 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, --yes and --no-login answer the questions for scripts; without a terminal, the setup waits for each installer and leaves the logins out. See getting started.

  • host add shows the same agent checklist in a terminal when a first-class or supported agent is missing; --yes skips it.

  • The launcher offers the guided setup: Enter on the first-run screen, or s anywhere. 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), new asks 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, and new --setup or --no-setup overrides it. The script runs in the session’s terminal before the agent, after .worktreeinclude’s copy, with the variables HOOK, SESSION_NAME, SESSION_DIR, WORKTREE_NEW and REPO (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 --fix and doctor --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 broken current helper 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. doctor on its own now checks the laptop: ssh, the config files (unknown keys too) and the secrets’ commands.

  • Per-session secrets: [secrets] in config.toml names each variable and a command that prints it on the laptop, such as op read … or security find-generic-password … -w. new --secret NAME (repeatable) and a host’s secrets list in hosts.toml ask 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 ls lists the install commands. host add and host doctor check each first-class and supported agent: installed, on PATH in 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.sh measures 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.toml records 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, and host doctor says 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. ls and 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 over docker 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 a token_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 --inline or agents.inline in config.toml starts agents that way, and a session started inline comes back inline after a reboot. Each session reports whether it draws full screen (full_screen in ls --json), and host doctor names those sessions and how to start them inline.

  • w in 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@latest gives 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/*.tape drive the demo in test/ttyshot, which now records an animated SVG and an asciicast (record start, record save, and a type key that types like a person). just screenshots makes 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.toml and LICENSES/), 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; .gitattributes that 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 from docs/ 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/ and main at /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 on main and weekly.

  • scripts/setup.sh --only <tools> installs only some tools, for CI; Hugo is built with go install at a pinned version.

  • Reference docs generated from the code (just docs, with cmd/docgen): every command and flag, config.toml and hosts.toml from 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 request exited, started, restored) to destinations you name in ~/.config/<name>/alerts.toml on 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_input waits 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 turn detail on. host alerts <host> shows the settings and last deliveries, and host alerts set|test|log|off change, test, explain and remove them; --token-cmd takes 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 by scripts/sync-labels.sh and the Labels workflow).

  • One-command developer setup: scripts/setup.sh (or just 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] in config.toml, and config keymap to 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 screenshots uses it to make the README’s screenshots, which ttyshot draws 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 through notify. 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, .worktreeinclude files copied, removed on delete only when nothing would be lost.

  • Setup and resume hooks: an executable .<name>/setup in the repository (or ~/.config/<name>/hooks/setup on the host) runs in the session’s terminal before the agent; resume runs before a respawn. A repository’s own hooks need your trust first (see above).

  • ports and forward: find the ports a session listens on, and forward them to localhost through SSH.

  • Provider scripts: host add --provider-script <path> --wake, and host 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: restore brings sessions back with each agent’s resume or continue command, and a boot-time user unit runs it. host restore runs it on demand.

  • just check-private (also in just lint and 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-ignored private/deny.txt.

Changed

  • The strict lint set is now the repository’s own (.golangci.yml), with a reason beside each exclusion; scripts/check-size.sh keeps Go source files at 500 lines or fewer, and depguard keeps logs on log/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’s log/slog: each line has the time, a level, a short message and its details as key=value pairs, 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 .worktreeinclude copies, 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 bare open …: 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 stop and host status drive a created host through its provider, as well as hosts with a provider script. forward says plainly that it needs SSH on hosts reached through docker exec or E2B. host rm on a created host says that its machine is kept.

  • go install github.com/iamlakshya/simstim@latest installs the client under its own name: its main moved to the root of the module, and the command tree to internal/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.txt without 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 scorecard scores it before then.

  • SECURITY.md has a reporting link and a disclosure timeline.

  • install.sh checks the release’s signature before the checksum: the release job signs checksums.txt with an SSH key, and the script verifies it with ssh-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 MemoryMax only; MemoryHigh made 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 = off stills 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, and n starts 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 to CHANGELOG.md, CONTRIBUTING.md, SECURITY.md and the other files at the root, now stay on the site, and just docs-check fails if one goes to GitHub instead. One motion switch, in the footer and under the launcher, stops every animation. See decision 0021.

Fixed

  • new with 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 ports listed 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 after kill had 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 --history could miss a match on macOS: grep -q stopped reading early, the writer died of SIGPIPE, and under pipefail the 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.sh never ran on macOS, whose grep has no -P; it uses perl now, and no longer flags its own comment on Linux.
  • new could 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 --fix uses the same rule.
  • On a Mac whose cache folder has a long path, host add failed 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 /tmp when the cache path is too long, and the client checks that the folder is its own.
  • scripts/setup.sh had the same ellipsis bug as install.sh; just lint now checks every script for it.
  • install.sh stopped with “unbound variable” in shells that read the … after $arch as 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.

View the source of this page Suggest a change