diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b940ecbcb5..3b879bc548 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -44,6 +44,9 @@ jobs:
- name: ๐ Pre-commit hooks
uses: pre-commit/action@v3.0.1
+ - name: ๐งช install.sh unit tests
+ run: sh dev/install/install_test.sh
+
feature-check:
# Guards the library/CLI cleave: the `cli` feature gates clap, skim,
# crossterm, termimad, env_logger, humantime. If anything reachable from
diff --git a/README.md b/README.md
index 4041ac70e6..2dc845be35 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ Privacy & security
sudo pacman -S worktrunk && wt config shell install
```
+
+Script installer (experimental)
+
+Downloads a static binary โ no package manager required.
+
+**macOS & Linux:**
+
+```bash
+curl -fsSL https://worktrunk.dev/install.sh | sh
+```
+
+**Windows:**
+
+```bash
+powershell -c "irm https://worktrunk.dev/install.ps1 | iex"
+```
+
+
+
## Quick start
Create a worktree for a new feature:
diff --git a/dev/install/install_test.sh b/dev/install/install_test.sh
new file mode 100755
index 0000000000..7162792f82
--- /dev/null
+++ b/dev/install/install_test.sh
@@ -0,0 +1,165 @@
+#!/bin/sh
+set -eu
+
+# Unit tests for docs/static/install.sh.
+#
+# These are fast behavioral tests that run install.sh with a mocked curl on
+# PATH and verify exit codes + output. They cover the error/edge paths only.
+# The happy path (curl | sh yielding a working `wt`) is covered by the
+# container hand-test: dev/install/test-containers.sh
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh"
+
+PASS=0
+FAIL=0
+
+pass() {
+ PASS=$((PASS + 1))
+ echo " PASS: $1"
+}
+
+fail() {
+ FAIL=$((FAIL + 1))
+ echo " FAIL: $1"
+ if [ -n "${2:-}" ]; then
+ printf ' %s\n' "$2"
+ fi
+}
+
+MOCK_DIR="$(mktemp -d)"
+CARGO_DIR="$(mktemp -d)"
+trap 'rm -rf "$MOCK_DIR" "$CARGO_DIR"' EXIT
+
+# Write an executable shell script to $MOCK_DIR/$1 with the given body.
+mock_bin() {
+ name="$1"
+ body="$2"
+ path="$MOCK_DIR/$name"
+ printf '#!/bin/sh\n%s\n' "$body" > "$path"
+ chmod +x "$path"
+}
+
+# Mock curl to find the `-o ` arg and write a minimal installer there
+# that exits with the given code. Simulates a successful download of an
+# installer whose *execution* then succeeds ($1=0) or fails ($1!=0).
+mock_curl_writes_installer() {
+ mock_bin curl "while [ \$# -gt 0 ]; do
+ if [ \"\$1\" = \"-o\" ]; then
+ shift
+ printf '%s\n' '#!/bin/sh' 'exit $1' > \"\$1\"
+ exit 0
+ fi
+ shift
+done
+exit 1"
+}
+
+# Run install.sh with $MOCK_DIR first on PATH and a curated environment.
+# Captures exit code in $rc and combined output in $output. Pass a value for
+# $OS as the first argument (default empty).
+run_install() {
+ os_val="${1:-}"
+ set +e
+ output="$(PATH="$MOCK_DIR:/usr/bin:/bin" \
+ OS="$os_val" \
+ HOME="$CARGO_DIR/home" \
+ CARGO_HOME="$CARGO_DIR/cargo" \
+ sh "$INSTALL_SH" 2>&1)"
+ rc=$?
+ set -e
+}
+
+echo "=== install.sh test suite ==="
+echo ""
+
+# ---------------------------------------------------------------------------
+echo "--- Platform gate ---"
+
+# Windows detection: sets OS=Windows_NT, expects exit 1 with guidance.
+run_install Windows_NT
+if [ "$rc" -eq 1 ] && echo "$output" | grep -q "Windows detected" \
+ && echo "$output" | grep -q "install.ps1"; then
+ pass "exits with PowerShell guidance when OS=Windows_NT"
+else
+ fail "exits with PowerShell guidance when OS=Windows_NT" "rc=$rc output: $output"
+fi
+
+echo ""
+
+# ---------------------------------------------------------------------------
+echo "--- Download / installer failures ---"
+
+# curl fails to download: expect non-zero exit.
+mock_bin curl 'exit 22'
+run_install
+if [ "$rc" -ne 0 ]; then
+ pass "exits non-zero when curl fails"
+else
+ fail "exits non-zero when curl fails" "rc=$rc output: $output"
+fi
+
+# curl succeeds but writes a failing installer: expect non-zero exit.
+mock_curl_writes_installer 5
+run_install
+if [ "$rc" -ne 0 ]; then
+ pass "exits non-zero when upstream installer fails"
+else
+ fail "exits non-zero when upstream installer fails" "rc=$rc output: $output"
+fi
+
+echo ""
+
+# ---------------------------------------------------------------------------
+echo "--- Post-install path resolution ---"
+
+# curl writes a no-op installer; CARGO_HOME is empty and wt is absent from
+# PATH โ script should warn and exit 0 (installer succeeded but wt missing).
+mock_curl_writes_installer 0
+mkdir -p "$CARGO_DIR/cargo"
+run_install
+if [ "$rc" -eq 0 ] && echo "$output" | grep -q "'wt' not found in PATH"; then
+ pass "warns and exits 0 when wt is missing after install"
+else
+ fail "warns and exits 0 when wt is missing after install" "rc=$rc output: $output"
+fi
+
+# As above, but wt exists in CARGO_HOME/bin with an env file. The script
+# should source env, find wt, and reach the post-wt-found stage. Whether the
+# final `wt config shell install` actually runs depends on /dev/tty being
+# openable โ that's a property of the test environment, so accept either the
+# sentinel (TTY) or the non-interactive fallback message (no TTY).
+sentinel="$CARGO_DIR/wt.args"
+mkdir -p "$CARGO_DIR/cargo/bin"
+cat > "$CARGO_DIR/cargo/bin/wt" < "$sentinel"
+WT
+chmod +x "$CARGO_DIR/cargo/bin/wt"
+cat > "$CARGO_DIR/cargo/env" </dev/null) output: $output"
+fi
+
+echo ""
+
+# ---------------------------------------------------------------------------
+echo "=== Results ==="
+echo " $PASS passed, $FAIL failed"
+echo ""
+
+if [ "$FAIL" -gt 0 ]; then
+ exit 1
+fi
diff --git a/dev/install/test-containers.sh b/dev/install/test-containers.sh
new file mode 100755
index 0000000000..bcb513f18e
--- /dev/null
+++ b/dev/install/test-containers.sh
@@ -0,0 +1,101 @@
+#!/bin/sh
+set -eu
+
+# Hand-test: run docs/static/install.sh inside clean Docker containers and
+# verify it produces a working `wt`. Not wired into CI because it hits the
+# real network (GitHub releases) and needs Docker.
+#
+# Usage:
+# sh dev/install/test-containers.sh # test all images
+# sh dev/install/test-containers.sh ubuntu # test one image
+#
+# The script under test is the one checked into this repo (not fetched from
+# worktrunk.dev) โ we're testing the current source, not what's published.
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+INSTALL_SH="$SCRIPT_DIR/../../docs/static/install.sh"
+
+if ! command -v docker >/dev/null 2>&1; then
+ echo "docker is required for the container test. Install Docker Desktop or"
+ echo "equivalent, then re-run."
+ exit 1
+fi
+
+# Each entry: image | setup. The upstream cargo-dist installer downloads a
+# tar.xz release and extracts it, so `xz` must be on PATH alongside curl.
+IMAGES="
+ubuntu:24.04|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils
+debian:12|apt-get update -qq && apt-get install -y -qq curl ca-certificates xz-utils
+fedora:41|dnf install -y -q curl xz
+alpine:3.20|apk add --no-cache curl ca-certificates xz
+archlinux:latest|pacman -Sy --noconfirm curl ca-certificates xz
+"
+
+FILTER="${1:-}"
+PASS=0
+FAIL=0
+FAILED=""
+
+run_one() {
+ image="$1"
+ setup="$2"
+
+ echo ""
+ echo "=== $image ==="
+
+ # Copy install.sh into the container, run setup + install, then verify
+ # `wt --version` prints a worktrunk version. Use `sh -c` as entrypoint so
+ # the script runs regardless of the image's default command. We capture
+ # into a temp file rather than piping โ a pipe would mask docker's exit
+ # code behind sed's.
+ log="$(mktemp)"
+ set +e
+ docker run --rm \
+ -v "$INSTALL_SH:/tmp/install.sh:ro" \
+ "$image" \
+ sh -c "set -e; $setup >/dev/null; sh /tmp/install.sh; . \${CARGO_HOME:-\$HOME/.cargo}/env; wt --version" \
+ >"$log" 2>&1
+ rc=$?
+ set -e
+ sed 's/^/ /' "$log"
+ rm -f "$log"
+
+ if [ "$rc" -eq 0 ]; then
+ echo " PASS: $image"
+ PASS=$((PASS + 1))
+ else
+ echo " FAIL: $image (exit $rc)"
+ FAIL=$((FAIL + 1))
+ FAILED="$FAILED $image"
+ fi
+}
+
+echo "Testing install.sh in containers..."
+
+# Shell-splitting on newlines in POSIX sh: set IFS to newline, iterate.
+old_ifs="$IFS"
+IFS='
+'
+for entry in $IMAGES; do
+ IFS='|'
+ # shellcheck disable=SC2086
+ set -- $entry
+ IFS="$old_ifs"
+ image="$1"
+ setup="$2"
+ if [ -n "$FILTER" ] && ! echo "$image" | grep -q "$FILTER"; then
+ continue
+ fi
+ run_one "$image" "$setup"
+ IFS='
+'
+done
+IFS="$old_ifs"
+
+echo ""
+echo "=== Results ==="
+echo " $PASS passed, $FAIL failed"
+if [ "$FAIL" -gt 0 ]; then
+ echo " Failed:$FAILED"
+ exit 1
+fi
diff --git a/docs/content/worktrunk.md b/docs/content/worktrunk.md
index 81f3dcd209..6c4a69bf69 100644
--- a/docs/content/worktrunk.md
+++ b/docs/content/worktrunk.md
@@ -131,6 +131,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ Privacy & security
{{ terminal(cmd="sudo pacman -S worktrunk && wt config shell install") }}
+
+Script installer (experimental)
+
+Downloads a static binary โ no package manager required.
+
+**macOS & Linux:**
+
+```bash
+curl -fsSL https://worktrunk.dev/install.sh | sh
+```
+
+**Windows:**
+
+```bash
+powershell -c "irm https://worktrunk.dev/install.ps1 | iex"
+```
+
+
+
## Quick start
Create a worktree for a new feature:
diff --git a/docs/static/install.ps1 b/docs/static/install.ps1
new file mode 100644
index 0000000000..19acae2237
--- /dev/null
+++ b/docs/static/install.ps1
@@ -0,0 +1,42 @@
+# Worktrunk Installer (Windows)
+# https://worktrunk.dev/install.ps1
+
+$ErrorActionPreference = 'Stop'
+
+if ($IsWindows -eq $false -and $PSVersionTable.PSVersion.Major -ge 6) {
+ Write-Host "Non-Windows environment detected. Please use the shell installer instead:" -ForegroundColor Yellow
+ Write-Host " curl -fsSL https://worktrunk.dev/install.sh | sh"
+ exit 1
+}
+
+Write-Host "Installing worktrunk..."
+irm https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.ps1 | iex
+
+# Update PATH to pick up the newly installed binary.
+# Respect CARGO_HOME if set, otherwise use the default location.
+$cargoBin = if ($env:CARGO_HOME) { "$env:CARGO_HOME\bin" } else { "$HOME\.cargo\bin" }
+if ($env:Path -notlike "*$cargoBin*") {
+ $env:Path += ";$cargoBin"
+}
+
+# Check whether `wt` on PATH is actually worktrunk (Windows Terminal uses
+# the same alias). Wrap in try/catch โ with ErrorActionPreference='Stop',
+# a failing `wt --version` would throw instead of falling through.
+$wtIsWorktrunk = $false
+if (Get-Command wt -ErrorAction SilentlyContinue) {
+ try {
+ $wtIsWorktrunk = [bool](wt --version 2>&1 | Select-String 'worktrunk')
+ } catch {
+ $wtIsWorktrunk = $false
+ }
+}
+
+if ($wtIsWorktrunk) {
+ wt config shell install
+} elseif (Get-Command git-wt -ErrorAction SilentlyContinue) {
+ git-wt config shell install
+} else {
+ Write-Host ""
+ Write-Host "Warning: worktrunk installed but neither 'wt' nor 'git-wt' found in PATH." -ForegroundColor Yellow
+ Write-Host "Restart your shell and run 'wt config shell install' (or 'git-wt config shell install') manually."
+}
diff --git a/docs/static/install.sh b/docs/static/install.sh
new file mode 100644
index 0000000000..01b8d210c2
--- /dev/null
+++ b/docs/static/install.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+set -eu
+
+# Worktrunk Installer (Unix)
+# https://worktrunk.dev/install.sh
+
+if [ "${OS:-}" = "Windows_NT" ]; then
+ echo "Windows detected. Please use the PowerShell installer instead:"
+ echo " powershell -c \"irm https://worktrunk.dev/install.ps1 | iex\""
+ exit 1
+fi
+
+echo "Installing worktrunk..."
+
+# Download to a temp file instead of piping curl to sh. This avoids two issues:
+# 1. Pipe swallows curl failures (pipefail is not POSIX)
+# 2. Piping consumes stdin, blocking interactive prompts in the installer
+installer="$(mktemp)"
+trap 'rm -f "$installer"' EXIT
+curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/latest/download/worktrunk-installer.sh -o "$installer"
+sh "$installer"
+
+# Source the cargo env to pick up PATH changes from the installer.
+# This handles custom CARGO_HOME and avoids hardcoding ~/.cargo/bin.
+# POSIX sh exits the whole script when `.` can't read the file, even with
+# `|| true`, so guard with an explicit existence check.
+cargo_env="${CARGO_HOME:-$HOME/.cargo}/env"
+if [ -r "$cargo_env" ]; then
+ # shellcheck disable=SC1090
+ . "$cargo_env"
+fi
+
+if ! command -v wt >/dev/null 2>&1; then
+ echo ""
+ echo "Warning: worktrunk installed but 'wt' not found in PATH."
+ echo "Restart your shell and run 'wt config shell install' manually."
+ exit 0
+fi
+
+# Configure shell integration. We use < /dev/tty so the interactive prompt
+# works even when this script was piped into sh (e.g. curl ... | sh). On
+# non-interactive contexts /dev/tty exists but isn't openable; probe before
+# redirecting. Use `true` (a regular builtin) rather than `:` โ POSIX exits
+# the shell on a redirect failure against a special builtin.
+if { true < /dev/tty; } 2>/dev/null; then
+ wt config shell install < /dev/tty
+else
+ echo ""
+ echo "Non-interactive environment detected."
+ echo "Run 'wt config shell install' after restarting your shell."
+fi
diff --git a/skills/worktrunk/reference/worktrunk.md b/skills/worktrunk/reference/worktrunk.md
index b0d4d0077d..1f40c16715 100644
--- a/skills/worktrunk/reference/worktrunk.md
+++ b/skills/worktrunk/reference/worktrunk.md
@@ -117,6 +117,25 @@ Alternatively, disable Windows Terminal's alias (Settings โ Privacy & security
sudo pacman -S worktrunk && wt config shell install
```
+
+Script installer (experimental)
+
+Downloads a static binary โ no package manager required.
+
+**macOS & Linux:**
+
+```bash
+curl -fsSL https://worktrunk.dev/install.sh | sh
+```
+
+**Windows:**
+
+```bash
+powershell -c "irm https://worktrunk.dev/install.ps1 | iex"
+```
+
+
+
## Quick start
Create a worktree for a new feature: