diff --git a/public/install.sh b/public/install.sh
new file mode 100644
index 0000000..d506646
--- /dev/null
+++ b/public/install.sh
@@ -0,0 +1,205 @@
+#!/usr/bin/env sh
+#
+# install.sh — one-shot installer for the `instant` CLI.
+#
+# Usage:
+# curl -sSfL https://instanode.dev/install.sh | sh
+#
+# What it does:
+# 1. Detect the host OS (darwin | linux | windows) and arch (amd64 | arm64).
+# 2. Resolve the latest release tag from the GitHub API (or honor an
+# explicit INSTANT_VERSION env var, e.g. v0.2.0).
+# 3. Download the matching tar.gz archive from the release page.
+# 4. Verify its SHA-256 against the release's checksums.txt.
+# 5. Drop `instant` into INSTANT_INSTALL_DIR (default /usr/local/bin).
+#
+# Why POSIX sh (not bash): the curl-pipe-sh path runs in whatever /bin/sh
+# the user has — that's POSIX dash on Debian/Ubuntu, bash on macOS,
+# busybox sh on Alpine. Sticking to POSIX keeps the install path
+# friction-free everywhere. No arrays, no [[ ]], no `local`.
+#
+# Why not `go install`: a Go toolchain is a > 200 MB dependency for what
+# should be a 30-second install. `go install` is still documented in the
+# README as a fallback for users who already have Go.
+#
+# CLI-MCP-13R2 — closes the BugBash QA round 2 strategic gap: the CLI had
+# no release path at all.
+
+set -eu
+
+REPO="InstaNode-dev/cli"
+BINARY_NAME="instant"
+DEFAULT_INSTALL_DIR="/usr/local/bin"
+INSTALL_DIR="${INSTANT_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
+VERSION="${INSTANT_VERSION:-}"
+
+# ── helpers ─────────────────────────────────────────────────────────────────
+
+# Coloured logging — gracefully degrades on terminals without ANSI support.
+if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
+ BOLD="$(tput bold)"
+ DIM="$(tput dim)"
+ RED="$(tput setaf 1)"
+ GREEN="$(tput setaf 2)"
+ YELLOW="$(tput setaf 3)"
+ RESET="$(tput sgr0)"
+else
+ BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; RESET=""
+fi
+
+info() { printf "%s==>%s %s\n" "$GREEN$BOLD" "$RESET" "$1"; }
+warn() { printf "%s==>%s %s\n" "$YELLOW$BOLD" "$RESET" "$1" >&2; }
+fail() { printf "%serror:%s %s\n" "$RED$BOLD" "$RESET" "$1" >&2; exit 1; }
+
+# detect_os normalises uname's output into goreleaser's archive naming
+# (darwin | linux | windows). MINGW / MSYS / CYGWIN all collapse to
+# `windows`; the windows path emits an explicit "use the zip from the
+# release page" message because curl-pipe-sh under windows is not
+# something we want to surprise users with.
+detect_os() {
+ uname_out=$(uname -s 2>/dev/null || echo unknown)
+ case "$uname_out" in
+ Darwin) printf 'darwin' ;;
+ Linux) printf 'linux' ;;
+ MINGW*|MSYS*|CYGWIN*|Windows_NT) printf 'windows' ;;
+ *) fail "unsupported OS: $uname_out (expected Darwin, Linux, or Windows)" ;;
+ esac
+}
+
+# detect_arch normalises uname -m into goreleaser's arch names. The
+# common Apple Silicon (arm64), Intel (amd64 / x86_64), and Linux/arm64
+# variants are covered; 32-bit and esoteric ISAs are explicitly
+# rejected (the platform isn't shipped for them).
+detect_arch() {
+ arch_out=$(uname -m 2>/dev/null || echo unknown)
+ case "$arch_out" in
+ x86_64|amd64) printf 'amd64' ;;
+ arm64|aarch64) printf 'arm64' ;;
+ *) fail "unsupported architecture: $arch_out (expected amd64 or arm64)" ;;
+ esac
+}
+
+# need_cmd checks that a required CLI exists on PATH, with a clear error
+# pointing at the missing dependency.
+need_cmd() {
+ command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1. Please install it and re-run."
+}
+
+# resolve_version queries GitHub's release API for the latest tag when
+# INSTANT_VERSION is unset. The endpoint returns 200 with `tag_name` in
+# JSON; we grep it out with sed (no jq dependency).
+resolve_version() {
+ if [ -n "$VERSION" ]; then
+ printf '%s' "$VERSION"
+ return
+ fi
+ info "Resolving latest release for $REPO..." >&2
+ api_url="https://api.github.com/repos/$REPO/releases/latest"
+ latest=$(curl -fsSL "$api_url" 2>/dev/null \
+ | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
+ | head -n1) || true
+ if [ -z "$latest" ]; then
+ fail "could not resolve latest release for $REPO. Set INSTANT_VERSION=vX.Y.Z and retry."
+ fi
+ printf '%s' "$latest"
+}
+
+# verify_checksum downloads checksums.txt for the release, matches the
+# archive name, and re-computes the SHA-256 locally. shasum (BSD/macOS)
+# and sha256sum (GNU/Linux) are both supported transparently.
+verify_checksum() {
+ archive="$1"
+ checksum_url="$2"
+ archive_base=$(basename "$archive")
+ info "Verifying checksum..."
+ if ! curl -fsSL "$checksum_url" -o "$archive.checksums"; then
+ fail "could not download checksums.txt from $checksum_url"
+ fi
+ expected=$(awk -v n="$archive_base" '$2 == n {print $1}' "$archive.checksums")
+ if [ -z "$expected" ]; then
+ fail "no checksum entry for $archive_base in checksums.txt"
+ fi
+ if command -v sha256sum >/dev/null 2>&1; then
+ actual=$(sha256sum "$archive" | awk '{print $1}')
+ elif command -v shasum >/dev/null 2>&1; then
+ actual=$(shasum -a 256 "$archive" | awk '{print $1}')
+ else
+ warn "neither sha256sum nor shasum found; skipping checksum verification"
+ return
+ fi
+ if [ "$expected" != "$actual" ]; then
+ fail "checksum mismatch: expected $expected, got $actual"
+ fi
+ info "Checksum OK ($expected)"
+}
+
+# install_binary copies the extracted binary into INSTALL_DIR, using
+# sudo if the target dir isn't writable by the current user. Permission
+# requests are explicit so a curl-pipe-sh user understands the prompt.
+install_binary() {
+ src="$1"
+ dest="$INSTALL_DIR/$BINARY_NAME"
+ if [ -w "$INSTALL_DIR" ]; then
+ install -m 0755 "$src" "$dest"
+ else
+ info "Installing to $dest requires sudo..."
+ sudo install -m 0755 "$src" "$dest"
+ fi
+}
+
+# ── main ────────────────────────────────────────────────────────────────────
+
+need_cmd curl
+need_cmd tar
+need_cmd uname
+
+os=$(detect_os)
+if [ "$os" = "windows" ]; then
+ fail "Windows is not supported by this script. Download the .zip from https://github.com/$REPO/releases and add instant.exe to your PATH."
+fi
+arch=$(detect_arch)
+version=$(resolve_version)
+
+# Strip the leading "v" — goreleaser archives use the bare semver in
+# their filename (e.g. instant_0.2.0_darwin_arm64.tar.gz).
+version_no_v="${version#v}"
+
+archive_name="${BINARY_NAME}_${version_no_v}_${os}_${arch}.tar.gz"
+release_base="https://github.com/$REPO/releases/download/$version"
+archive_url="$release_base/$archive_name"
+checksum_url="$release_base/checksums.txt"
+
+info "Detected: $os/$arch"
+info "Installing $BINARY_NAME $version from $archive_url"
+
+tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t instant-install)
+trap 'rm -rf "$tmpdir"' EXIT INT TERM
+
+archive_path="$tmpdir/$archive_name"
+if ! curl -fsSL "$archive_url" -o "$archive_path"; then
+ fail "could not download $archive_url. Check that the release exists at https://github.com/$REPO/releases/tag/$version"
+fi
+
+verify_checksum "$archive_path" "$checksum_url"
+
+info "Extracting archive..."
+tar -xzf "$archive_path" -C "$tmpdir"
+
+if [ ! -f "$tmpdir/$BINARY_NAME" ]; then
+ fail "extracted archive does not contain $BINARY_NAME"
+fi
+
+install_binary "$tmpdir/$BINARY_NAME"
+
+info "Installed $BINARY_NAME $version to $INSTALL_DIR/$BINARY_NAME"
+printf "%sRun%s '%s --version' to verify.\n" "$DIM" "$RESET" "$BINARY_NAME"
+
+# Sanity-check that INSTALL_DIR is on PATH; warn if not (silent install
+# pipelines are no fun to debug).
+case ":$PATH:" in
+ *":$INSTALL_DIR:"*) ;;
+ *)
+ warn "$INSTALL_DIR is not on your PATH. Add it to your shell profile, e.g.:"
+ printf "\n export PATH=\"%s:\$PATH\"\n\n" "$INSTALL_DIR" >&2
+ ;;
+esac
diff --git a/scripts/prerender.mjs b/scripts/prerender.mjs
index 1ec4a61..fdd91cd 100644
--- a/scripts/prerender.mjs
+++ b/scripts/prerender.mjs
@@ -453,7 +453,17 @@ async function main() {
// already covers /app on its own (Step 4.5). Other /app/* deep links
// remain on the 404-status fallback — they are not external CTA
// destinations and aren't worth pre-generating.
- const authShellRoutes = ['/login', '/login/callback', '/claim', '/cli-auth', '/app/checkout', '/app/billing']
+ // DOG-42 (2026-05-29): extend to every /app/* page reachable from
+ // authenticated nav so deep links / refresh / share / bookmark return
+ // HTTP 200 instead of 404 (which the catch-all 404.html hydrates as
+ // the right page anyway, but the 404 status confuses monitoring +
+ // reader-mode tools + uptime checks).
+ const authShellRoutes = [
+ '/login', '/login/callback', '/claim', '/cli-auth',
+ '/app', '/app/checkout', '/app/billing',
+ '/app/dashboard', '/app/resources', '/app/deployments',
+ '/app/team', '/app/settings', '/app/audit', '/app/vault',
+ ]
for (const route of authShellRoutes) {
const p = resolve(DIST, route.replace(/^\//, ''), 'index.html')
await mkdir(dirname(p), { recursive: true })
diff --git a/src/pages/MarketingPage.tsx b/src/pages/MarketingPage.tsx
index 05673ff..ad9a459 100644
--- a/src/pages/MarketingPage.tsx
+++ b/src/pages/MarketingPage.tsx
@@ -666,7 +666,7 @@ export function MarketingPage() {
{'\n '}"mcpServers": {'{'}
{'\n '}"instanode": {'{'}
{'\n '}"command": "npx",
- {'\n '}"args": ["-y", "@instanode/mcp"]
+ {'\n '}"args": ["-y", "instanode-mcp"]
{'\n '}{'}'}
{'\n '}{'}'}
{'\n'}{'}'}