Verify an Install Script Before Running It: What the shell.online Installer Checks, and What It Can't, After Tensorlake 0.5.144

Verify an Install Script Before Running It: What the shell.online Installer Checks, and What It Can't, After Tensorlake 0.5.144

On October 8, at 01:12:07 UTC, someone published [email protected] to npm. It was the SDK for Tensorlake's AI agent infrastructure, and this version had one new line in its manifest: "preinstall": "node lib/setup.mjs". Socket's write-up of the compromise says it flagged the package 11 minutes after it went up. By then, any machine that installed it had already run the payload. Nobody had to import the SDK first. The payload went after npm and GitHub tokens, AWS credentials, SSH keys and .env files, and it also read the configuration and MCP files for .claude, .cursor, .kiro, Windsurf and Zed. That is the machine where your coding agents run. It is also the machine where people install a terminal sharing CLI, so this post covers how to verify an install script before running it, using our own installer for shell.online as the example. The checks it makes are real. So are their limits.

Provenance said where the build came from, and that was all

The detail worth remembering is in Endor Labs' analysis of the same release. The malicious commit landed on the project's main branch, and the project's normal GitHub Actions pipeline published the result. Endor reports that the release "even carried valid build provenance." Their summary is the line to keep: "Provenance confirms where a build came from, not that the source was clean."

Checksums are the same kind of tool. A SHA-256 match tells you the bytes you got are the bytes the publisher listed. It tells you nothing about whether the publisher should have listed them. Keep that in mind when you read what an installer says about itself, ours included.

The cleanup order matters as much as the detection. Socket found a component that polls api.github.com/user with the stolen token and runs an attacker-supplied handler if that token gets revoked. Their remediation removes the gh-token-monitor persistence first and rotates credentials second. The Cloud Security Alliance research note is a good summary if you have to brief a team on it.

How to verify an install script before running it: read ours first

The documented install for macOS, Linux, the BSDs and Solaris is one line that pipes a script into sh. You don't have to run it that way. Download it, read it, then run the file you read:

curl -fsSL https://shell.online/install -o install.sh
less install.sh
sh install.sh

Today the script is 249 lines of POSIX sh. It doesn't have to be taken on trust, so here is what it does, in order:

  1. It maps uname output to a binary name such as shell-linux-arm64 or shell-darwin-arm64. On an Intel-translated shell on Apple silicon, it checks sysctl.proc_translated and picks arm64.
  2. From the same base URL it downloads that binary and a manifest, /downloads/SHA256SUMS.
  3. It rejects the manifest if the first line looks like HTML, so a captive portal or an error page can't count as a checksum. It also requires the matching entry to be exactly 64 hex characters.
  4. It hashes the download with sha256sum, or shasum -a 256 where that's what exists (the coreutils sha256sum man page covers the format). If the hashes differ, it stops with checksum_mismatch and installs nothing.
  5. It installs to /usr/local/bin if that is writable, otherwise to $XDG_BIN_HOME, otherwise to ~/.local/bin. SHELL_ONLINE_INSTALL_DIR overrides that, and it must be an absolute path.
  6. It prints Verified SHA-256: followed by the hash, then runs shell --version.

The script also phones home once, and the comment at the top says so. That one request carries an outcome word (ok, unsupported_arch, checksum_mismatch and so on) and the binary name, so that a platform that keeps failing gets noticed. To skip it:

SHELL_ONLINE_INSTALL_REPORT=0 sh install.sh

The Windows installer, install.ps1, works the same way. It fetches the same SHA256SUMS, hashes the binary with Get-FileHash, and refuses to install on a mismatch.

What that checksum does not prove

Look at step 2 again. The binary and the manifest come from the same origin. That catches a truncated download, a corrupted mirror and a proxy that swaps in HTML. It doesn't catch someone who controls shell.online/downloads/, because that person can replace both files and they will still match. This is the same lesson as Tensorlake, one level down.

There is also no signature. The v0.25.0 release ships SHA256SUMS, a release.json listing every artifact's hash, and a .sha256 file next to each binary. It has no detached signature and no GitHub artifact attestation. Running gh attestation verify against the v0.25.0 darwin-arm64 binary returns a 404 from the attestations API. We would rather say that here than have someone find it. As Tensorlake showed, GitHub's build provenance attestations would not have stopped a poisoned main branch anyway. They would add a second root of trust for the case above.

Check the hash against a second host

You can add that second root yourself today. Every release in the shell.online GitHub releases carries the same SHA256SUMS that the website serves, and GitHub is a different origin from shell.online. Compare the two before you install, or afterwards against the binary you already have:

gh release download v0.25.0 --repo TeoSlayer/shell.online -p SHA256SUMS -D gh-sums
curl -fsSL https://shell.online/downloads/SHA256SUMS -o site-SHA256SUMS
diff <(sort gh-sums/SHA256SUMS) <(sort site-SHA256SUMS) && echo match
shasum -a 256 "$(command -v shell)"

When we ran this today, the two manifests were identical. The darwin-arm64 binary hashed to da5e88b3...d1b62, which matches both manifests and the entry in release.json. An attacker would now need control of both the website and the GitHub release. That is a higher bar, though not a proof that the source was clean.

release.json is also worth reading for one more reason: it records the Go toolchain used for the published binaries. For v0.25.0 that is go1.26.8. The move to Go 1.26.9 for two net/http fixes landed on main on October 9, after that release.

Homebrew and building from source move the trust, not remove it

The Homebrew route, documented on the platforms and install page, doesn't download our binary at all. The formula in Formula/shell-online.rb points at the v0.25.0 tag archive on GitHub, pins that archive's sha256, and builds ./cmd/shell with CGO_ENABLED=0 using Homebrew's Go. You are trusting the tagged source and your own toolchain rather than our build machine. The tap can lag behind the direct installer, so check shell --version if you need a specific release.

Building from source goes one step further. The README pins the tag and the flags:

git clone --depth 1 --branch v0.25.0 https://github.com/TeoSlayer/shell.online.git
cd shell.online
go build -buildvcs=false -trimpath -ldflags="-X main.version=0.25.0" -o shell ./cmd/shell

That gets you a binary whose source you can read. It doesn't make the source correct. If the tag itself were poisoned the way Tensorlake's main branch was, every path above would install the poison faithfully. Reading the diff between tags is the only check that covers that case.

One structural difference from an npm SDK: the CLI is a single static Go binary. There is no dependency tree resolving at install time and no lifecycle hook. Apart from the shell --version call at the end, the 249 lines above are the only code that runs before you run shell yourself. npm users can get closer to that with the --ignore-scripts install option, which Endor suggests where it is practical.

If the host already ran something like tensorlake 0.5.144

A shell.online share is a URL plus an eight-character password, and together they work as a bearer credential. Anyone holding both can open the live terminal and, unless the share is --read-only, type with the permissions of the wrapped process. Terminal frames are end-to-end encrypted, so the relay can't read them, but that protects the wire. It doesn't protect a host that is already compromised. If a machine with active shares ran a credential stealer, treat those links as exposed too.

Follow Socket's order: remove the attacker's persistence first. Then rotate the browser credentials of each running session without killing the process behind it, with shell password rotate <ID>. That also revokes any MCP grants on the session. Run shell list first to find the IDs.

After that, decide whether the host itself can be trusted. The shell.online security model spells out what stays on the machine and what crosses the relay. Our introduction to shell.online walks through the share flow, if you are evaluating it for the first time. Either way, start by reading the installer and CLI source in the repository, not by piping anything anywhere.

Read it, check it, then run it

shell.online gives any terminal process a browser link and a password, with end-to-end encryption by default. The installer is short enough to read in one sitting, and every release hash is published in two places.

Try shell.online
About this article

Published by the Pilot Protocol team. Product claims are scoped to the availability labels and technical references linked in the article; deployment behavior can vary by version and environment.

How we publish · Suggest a correction · Technical references