Skip to content

Deploying on a cluster

sinteractive is one binary with zellij compiled in. The batch job execs that binary on the compute node (sbatch --wrap "exec /path/to/sinteractive __job …"), so the only deployment rule is: the binary must live on a filesystem the compute nodes can see. A user's ~/.local/bin on a shared home, a group /projects directory, a cluster-wide /opt — any of them works, and there is nothing to install on the nodes. Only when /usr/local is node-local and you want it there does make nodes come in.

What the binary needs

  • glibc no newer than the oldest node's. The binary links glibc dynamically, so a build on a newer distribution refuses to start on an older one with GLIBC_2.xx not found. Build on the oldest system the binary must run on — the release builds run in a rockylinux:8 container (glibc 2.28, what Alpine's Rocky 8 nodes ship) for exactly this reason.
  • libcurl.so.4 at runtime (the zellij web client links it). Present on every RHEL-family node; ldd sinteractive shows what else it wants.
  • libnvidia-ml.so.1, optionally: the monitor loads NVML lazily and reports no GPUs without it, so a login node without a driver is fine.
  • Slurm client tools on PATH: squeue, sbatch, scontrol, sacct, sinfo; srun for attach.
  • ssh to the nodes only for attach --ssh, peek, send, monitor --live and doctor --nodes. attach itself goes through srun --overlap, and the login-node reporting commands read the shared cache.

Building

git clone https://github.com/rnabioco/sinteractive
cd sinteractive
make build      # cargo build --release -p sint --features web_server_capability

rust-toolchain.toml pins stable Rust and adds the wasm32-wasip1 target: the status plugin (crates/sint-zellij) is built for it by sint's build.rs and embedded, so a plain cargo build needs nothing beyond the toolchain file. The rest is a C/C++ toolchain with cmake and perl, and the libcurl and OpenSSL headers — on Rocky:

dnf install -y gcc gcc-c++ make cmake perl git pkgconfig libcurl-devel openssl-devel

The first build takes a while (zellij is large); SINT_SKIP_BUNDLE=1 skips the plugin build for quick local iterations (the binary then warns that the status plugin is not embedded). FEATURES= drops the web client and its dependencies. Compile on a compute node, not the login node, on clusters that police it.

The man page and the shell completions are generated by the binary (sinteractive gen man, sinteractive gen completions bash|zsh|fish); make man and make completions refresh the committed copies under man/ and completions/ from the built binary, and the install targets run both.

Installing

make install                  # as a user: ~/.local/bin and ~/.local/share
sudo make install-system      # /usr/local/bin and /usr/local/share

Both put the binary, man page and completions in place, and the Claude Code share tree (<prefix>/share/sinteractive/{skills,claude}) beside the binary, which is what sinteractive claude install reads from when run from an installed copy rather than a checkout. make install refuses with a hint when target/release/sinteractive has not been built; the Makefile does not fall back to anything else.

The binary is never written over in place. Every running session is this binary, executing on its compute node as the zellij server, and so is every launch or attach sitting on a login node. On an NFS home (Alpine's Isilon) unlinking or renaming over a file that another node is executing takes the inode away underneath it: the next page that process faults in comes back ESTALE and it dies with SIGBUS — the server first ("Lost connection to the Zellij server"), then the client ("Bus error (core dumped)"). So scripts/install-bin.sh puts each build at <bindir>/.sinteractive-<sha> (a no-op for a build already there) and swaps <bindir>/sinteractive, a relative symlink, into place with a rename. current_exe() resolves through the symlink, so a session keeps spawning its helpers from the build it started on instead of mixing zellij client and server versions across an upgrade. Old builds are pruned only when nothing can still be running them: everything installed since the earliest submit time in the queue is kept (the user's jobs, or every job for a root install), plus the two builds before that; with no scheduler or no jobs, the current build and the two before it. make nodes is different — /usr/local there is node-local, the running copy is on the same kernel as the rename, and renaming over it is safe — so it keeps its mv.

Then check it:

sinteractive doctor
#   ✓ binary   /home/me/.local/bin/sinteractive 1.3.0 (zellij 0.45.1)
#   ✓ plugin   status plugin embedded
#   ✓ bundle   extracted to /home/me/.cache/sinteractive/bin/1a14af25b66b
#   ✓ cache    /home/me/.cache/sinteractive writable on nfs (2.0 GB)
#   ✓ slurm    squeue sbatch scontrol sacct sinfo
#   ✓ cluster  alpine
#   ✓ ssh      /usr/bin/ssh
#   ✓ shell    /bin/bash
#   ✓ nvml     libnvidia-ml.so.1 loadable
#   ✓ home     /home/me on nfs (2.0 GB)

fail means a session cannot work until it is fixed (exit 1); warn is something to know about — NVML missing on a login node is normal, a $HOME on a small local filesystem is a reason to set SINTERACTIVE_CACHE. doctor --nodes additionally sweeps every node sinfo knows about over ssh and reports reachability, the binary's version there and whether the extracted bundle is visible from each — the check that catches a binary or cache directory on a filesystem the nodes do not mount. The node sweep never affects the exit code; a node being down is Slurm's business. --json gives both as one object.

The cache directory

SINTERACTIVE_CACHE (default $XDG_CACHE_HOME/sinteractive, else ~/.cache/sinteractive) holds everything a session writes for the login node to read, plus zellij's own working files. It must be on a filesystem the compute nodes share with the login node, and it needs a little room:

<cache>/
  JOBID.json            time-budget state file (frozen schema, see Scripting)
  JOBID.notices         TSV kind<TAB>text; absent when there is nothing to say
  JOBID.metrics.json    latest host snapshot from the in-session sampler
  JOBID.events.ndjson   the session's event log, one JSON object per line
  JOBID.poke            touch to make the session re-check its deadline now
  quota.json            cached quota probe, per user
  bin/<sha12>/          the extracted zellij bundle for this build:
                        sint-zellij.wasm, config.kdl, layouts/, .complete
  xdg/zellij/           zellij's cache (plugin artifacts, permission grants)

The bundle is extracted once per build (the id hashes the zellij version, the plugin and the config), write-to-temp-then-rename, so concurrent first runs on different nodes cannot see a half-written tree; old bundles can be deleted freely. Per-session files are removed when the session ends.

Node-local state lives under SINTERACTIVE_RUNTIME_DIR (default /tmp): sint-JOBID/ holds the zellij socket, the readiness marker and the path of the config the server was started with. It never needs to be shared.

Alpine

/home is 2 GB, so export SINTERACTIVE_CACHE=/projects/$USER/.cache/sinteractive in ~/.bashrc. /scratch/alpine would also do, but it is purged.

Installing on every node

Only for a system-wide install into a node-local /usr/local:

sudo make install-system      # the head/login node
sudo make nodes               # every node sinfo knows about

make nodes installs the same set of files as install-system — binary, man page, bash completion and the share tree — on each node. The share tree matters there and not only on the head node, because claude install resolves it relative to the running binary, and someone who runs it from inside a session is running the node's copy.

If the checkout lives on a cluster-wide mount and pdsh is available, each node installs straight from the shared path; otherwise a tar is piped to each node in turn. NODES (default sinfo -hN -o '%N') and SSH_USER (default root) are overridable: sudo make nodes NODES="compute00 compute01".

The pdsh call asks for the ssh rcmd module by name (-R ssh). pdsh's own default is rsh, which on a cluster with nothing listening on port 514 answers connect: Connection refused for every node at once — and a PDSH_RCMD_TYPE=ssh exported in root's shell does not rescue sudo make nodes, because sudo resets the environment. Override with make nodes PDSH_RCMD=<module>; pdsh -V lists what is compiled in.

The binary is copied to a temporary name and renamed into place rather than written over: on a node with a running session, the binary is that session's zellij server, and overwriting a running executable fails with "text file busy". Running sessions keep the old inode until they end, so an upgrade never disturbs them; sinteractive doctor --nodes shows which version each node has.

Upgrading from 0.x

The 0.x bash script needed tmux ≥ 3.7 on every compute node and, for a system-wide install, the script itself on each; both are gone. To upgrade:

  1. Install the binary as above. It can replace the script at the same path.
  2. Unset SINTERACTIVE_TMUX in shell profiles; it is ignored.
  3. Run sinteractive claude install once per user. It removes the 0.x hook scripts from ~/.claude/hooks/, rewrites their settings.json entries to sinteractive claude hook …, and registers the statusline and MCP server.
  4. Old top-level flags (--status, --attach, …) keep working with a warning for one release; update scripts to the subcommands.

The state-file contract (<cache>/JOBID.json) is unchanged, so tooling that polls it keeps working. tmux itself can stay or go; nothing reads it.