serve /install as well as /install.sh — the path the deployed caller requests

hanzo.ai/install.sh execs `curl -fsSL https://hanzo.sh/install | sh`. This site
serves install.sh and not install, so that fetch 404s; -f writes nothing and
exits 22; and sh reading an empty script exits 0. The documented one-liner —
curl -fsSL hanzo.ai/install.sh | sh — reports success and installs nothing.

The caller is already corrected in hanzo.ai, but that deploy has not shipped, so
the break is live right now. Serving the path it asks for repairs it from this
side immediately, with no dependency on the other pipeline.

Extensionless is this site's existing convention rather than a new one: cli, dev,
agents, bot and full are all served that way and hanzo.sh/cli answers 200 today.

The bytes are install.sh's, verified identical to what production serves, and the
shebang is #!/bin/sh — which matters, because the caller pipes this into sh and
not bash. The two files must stay identical: install.sh is the name the docs and
release notes use, install is the name the deployed caller asks for.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-03 07:33:23 -07:00
co-authored by Hanzo Dev
parent 94931c7d03
commit 8a5d333da1
Executable
+328
View File
@@ -0,0 +1,328 @@
#!/bin/sh
# hanzo installer
#
# curl -fsSL hanzo.sh | sh
# curl -fsSL hanzo.sh | bash
#
# copyright (c) 2024-2026 hanzo ai inc.
# https://hanzo.ai
#
# Downloads one prebuilt native binary per tool, verifies its sha256, and puts it
# on PATH. That is the whole design. Nothing is built here, no package manager is
# involved, and there is no runtime to install first.
#
# POSIX sh on purpose. The site published `| bash` and llms.txt published `| sh`,
# and the script was bash-only — so every reader who copied the `sh` form (every
# agent reading llms.txt) got `set: Illegal option -o pipefail` from dash and
# nothing installed. Rather than pick a winner and leave the other broken, this
# runs under both: no arrays, no [[ ]], no <<<, no echo -e, no pipefail. The
# polyglot the site serves already declares #!/bin/sh, so this also makes that
# shebang honest.
#
# It does NOT re-implement downloading. hanzoai/cli/install.sh is the one
# implementation of "fetch a Hanzo binary" — platform detection, asset naming,
# checksum verification and the second-name symlink all live there, once. This
# fetches it and drives it once per tool. Two copies of platform detection would
# be the same class of bug as two copies of a route table.
#
# Every tool below is a published, public, checksummed native binary. A tool that
# is not one is NOT installed by some other mechanism to pad the list — it is
# named, with the reason. An installer that quietly reaches for a package manager
# to look complete is the defect this file exists to remove.
set -eu
HANZO_DIR="${HANZO_INSTALL_DIR:-$HOME/.local/bin}"
HANZO_QUIET="${HANZO_QUIET:-0}"
CLI_REPO="${HANZO_CLI_REPO:-hanzoai/cli}"
# The complete set of Hanzo tools that ship as a public, checksummed, native
# binary today, as <name>:<repo>:<binary>:<second-name>.
#
# The binary is also the asset prefix and the name inside the tarball — the
# convention hanzoai/cli/install.sh relies on. Adding a tool is one line, the day
# its repo is public and publishes <binary>-<os>-<arch>.tar.gz.sha256.
#
# `dev` takes no second name on purpose. cli/install.sh defaults the alias to
# `hanzo-node`, which the CLI already owns — giving it to `dev` too would point
# the delegate name at a different program.
TOOLS='hanzo:hanzoai/cli:hanzo:hanzo-node
mcp:hanzoai/mcp:hanzo-mcp:mcp
dev:hanzoai/dev:dev:'
# The installable names, read off TOOLS so no message can name a different set
# than the loop installs.
tool_names() {
printf '%s\n' "$TOOLS" | while IFS= read -r n_row; do
printf ' %s' "${n_row%%:*}"
done
}
# Named, not silently skipped, and never substituted with something else.
unavailable_rows() {
cat <<'ROWS'
node|source is not public yet
desktop|`hanzo desktop` is in the CLI; the standalone app is not public yet
bot|`hanzo bot` is in the CLI; the standalone node is not native yet
ROWS
}
if [ -t 1 ] && [ "${TERM:-}" != dumb ]; then
R=$(printf '\033[0;31m'); G=$(printf '\033[0;32m'); Y=$(printf '\033[0;33m')
C=$(printf '\033[0;36m'); N=$(printf '\033[0m'); BD=$(printf '\033[1m')
DM=$(printf '\033[2m')
else
R=''; G=''; Y=''; C=''; N=''; BD=''; DM=''
fi
INSTALLED_N=0
FAILED_N=0
FAILED_LIST=''
log() { [ "$HANZO_QUIET" = 1 ] || printf ' %s\n' "$1"; }
ok() { printf ' %s✓%s %s\n' "$G" "$N" "$1"; }
warn() { printf ' %s!%s %s\n' "$Y" "$N" "$1"; }
fail() { printf ' %s✗%s %s\n' "$R" "$N" "$1"; }
die() { fail "$1"; exit 1; }
banner() {
[ "$HANZO_QUIET" = 1 ] && return 0
printf '%s' "$DM"
cat <<'ART'
__
/ /_ ____ _____ ____ ____
/ __ \/ __ `/ __ \/_ / / __ \
/ / / / /_/ / / / / / /_/ /_/ /
/_/ /_/\__,_/_/ /_/ /___/\____/
ART
printf '%s\n' "$N"
}
usage() {
cat <<'EOF'
hanzo installer
usage: curl -fsSL hanzo.sh | sh
curl -fsSL hanzo.sh | sh -s -- [options] [tool...]
Downloads one prebuilt native binary per tool and verifies its checksum.
With no tool named, installs every tool that ships as a public native binary.
tools:
hanzo the Hanzo CLI (also installed as hanzo-node, the same build)
mcp the MCP server (also installed as hanzo-mcp, the same build)
dev Hanzo Dev, the coding agent `hanzo code` runs by default
options:
-d, --dir PATH install directory (default: ~/.local/bin)
--version TAG pin the CLI release (default: latest)
-q, --quiet less output
-h, --help show this help
shortcuts:
curl -fsSL hanzo.sh | sh # every installable tool
curl -fsSL hanzo.sh/cli | sh # the CLI only
curl -fsSL hanzo.sh/mcp | sh # the MCP server only
Re-run any time to upgrade; installing is just downloading the current build.
EOF
exit 0
}
WANT=''
parse_args() {
while [ $# -gt 0 ]; do
case $1 in
--dir|-d) HANZO_DIR="$2"; shift 2 ;;
--version) HANZO_VERSION="$2"; export HANZO_VERSION; shift 2 ;;
--quiet|-q) HANZO_QUIET=1; shift ;;
--help|-h) usage ;;
# Retired flags. Installing IS upgrading — there is one path and it
# always fetches the current build — so these mean nothing now.
# Accept and ignore rather than failing an install someone scripted.
--upgrade|-u|--force|-f) shift ;;
--bundle|-b) WANT="$WANT $2"; shift 2 ;;
-*) warn "ignoring unknown option: $1"; shift ;;
*) WANT="$WANT $1"; shift ;;
esac
done
}
# wanted <name> <binary> — did the caller ask for this tool?
# `default`, `full` and `all` named the old bundles. They meant "everything",
# which is what naming nothing means, so they map to that rather than failing an
# install someone already scripted.
wanted() {
for w in $WANT; do
case $w in
"$1"|"$2"|default|full|all) return 0 ;;
esac
done
return 1
}
# hanzoai/cli/install.sh, fetched once and reused for every tool.
#
# Prefer the API with an explicit raw Accept: it serves the CURRENT file, while
# raw.githubusercontent sits behind a CDN that hands back a copy from some
# minutes ago. Serving a stale script is the exact bug this rewrite removes, so
# do not risk it — but fall back to raw when the API is unreachable or has
# rate-limited an anonymous caller, because a rate limit must not stop an install.
ONE_INSTALLER=''
fetch_one_installer() {
api="https://api.github.com/repos/${CLI_REPO}/contents/install.sh?ref=main"
token="${HANZO_INSTALL_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
# Never interpolate a token into an unquoted argument list; branch instead.
if [ -n "$token" ]; then
curl -fsSL -H "Accept: application/vnd.github.raw" \
-H "Authorization: Bearer $token" "$api" -o "$1" 2>/dev/null && return 0
else
curl -fsSL -H "Accept: application/vnd.github.raw" "$api" -o "$1" 2>/dev/null && return 0
fi
curl -fsSL "https://raw.githubusercontent.com/${CLI_REPO}/main/install.sh" -o "$1" 2>/dev/null
}
# install <repo> <binary> <second-name>
install_tool() {
i_repo="$1"; i_bin="$2"; i_alias="$3"
# HANZO_VERSION names a CLI release. Passing it to anything else would pin a
# tag that repo has never published, so it reaches exactly one tool.
i_pin=''
if [ "$i_repo" = "$CLI_REPO" ]; then i_pin="${HANZO_VERSION:-}"; fi
if i_out=$(HANZO_INSTALL_PREFIX="$HANZO_DIR" \
HANZO_INSTALL_REPO="$i_repo" \
HANZO_INSTALL_BIN="$i_bin" \
HANZO_INSTALL_ALIAS="$i_alias" \
HANZO_VERSION="$i_pin" \
sh "$ONE_INSTALLER" 2>&1); then
# Surface the installer's own warnings (PATH shadowing, PATH missing).
# They are the difference between an install that worked and one the user
# will never actually run — so carry the CONTINUATION lines too. The
# shadow warning's second line is the half that says what to do about it,
# and a filter that keeps only the alarm and drops the remedy is worse
# than not warning at all.
printf '%s\n' "$i_out" \
| grep -E 'WARNING|Remove it, or put|not on PATH|export PATH' \
| sed 's/^/ /' || true
else
printf '%s\n' "$i_out" | sed 's/^/ /'
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_bin"
fail "$i_bin"; return 1
fi
# Trust the artifact, not the log line: ask the binary that is now on disk.
if [ ! -x "$HANZO_DIR/$i_bin" ]; then
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_bin"
fail "$i_bin (nothing at $HANZO_DIR/$i_bin)"; return 1
fi
# The first token that looks like a version, not the last token on the line:
# `--version` output is not one shape across tools (`hanzo 1.9.18` ends on
# the version, `hanzo dev 0.6.91 (sha) 2026-04-01` ends on a date), and
# printing a build date where a version belongs is its own small lie.
i_ver=$("$HANZO_DIR/$i_bin" --version 2>/dev/null | head -1 \
| tr ' ' '\n' | grep -m1 -E '^v?[0-9]+\.[0-9]+' | sed 's/^v//')
[ -n "${i_ver:-}" ] || i_ver='?'
if [ -n "$i_alias" ]; then
# Both names or it is not a finished install. A missing second name falls
# through to whatever older copy is already on the box, silently.
if [ ! -e "$HANZO_DIR/$i_alias" ]; then
FAILED_N=$((FAILED_N + 1)); FAILED_LIST="$FAILED_LIST $i_alias"
fail "$i_alias was not installed alongside $i_bin"; return 1
fi
ok "$i_bin $i_ver ${DM}(+ $i_alias, same build)${N}"
else
ok "$i_bin $i_ver"
fi
INSTALLED_N=$((INSTALLED_N + 1))
}
show_unavailable() {
[ "$HANZO_QUIET" = 1 ] && return 0
printf '\n %snot included:%s\n' "$BD" "$N"
unavailable_rows | while IFS='|' read -r u_name u_why; do
printf ' %s○ %-8s %s%s\n' "$DM" "$u_name" "$u_why" "$N"
done
}
finish() {
printf '\n'
if [ "$INSTALLED_N" -eq 0 ]; then die "nothing was installed"; fi
# Never say "ready" over a failure. A partial install that reads as success
# is how someone spends an afternoon on a tool that was never there.
if [ "$FAILED_N" -gt 0 ]; then
printf ' %s%sincomplete%s — did not install:%s\n' "$R" "$BD" "$N" "$N"
for f in $FAILED_LIST; do printf ' %s✗%s %s\n' "$R" "$N" "$f"; done
printf '\n'
return 0
fi
printf ' %sready%s %s→ %s%s\n\n' "$G" "$N" "$DM" "$HANZO_DIR" "$N"
printf ' quick start:\n'
printf ' %shanzo auth login%s # authenticate\n' "$C" "$N"
printf ' %shanzo code%s # start a coding session\n' "$C" "$N"
printf ' %shanzo --help%s # every command\n' "$C" "$N"
if [ -x "$HANZO_DIR/hanzo-mcp" ]; then
printf ' %shanzo-mcp --help%s # the MCP server\n' "$C" "$N"
fi
printf '\n docs: https://docs.hanzo.ai\n\n'
}
main() {
parse_args "$@"
banner
command -v curl >/dev/null 2>&1 || die "need curl on PATH"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
ONE_INSTALLER="$tmp/install.sh"
fetch_one_installer "$ONE_INSTALLER" \
|| die "could not fetch the installer from ${CLI_REPO}"
[ -s "$ONE_INSTALLER" ] || die "the installer fetched from ${CLI_REPO} is empty"
mkdir -p "$HANZO_DIR"
selected=0
for row in $TOOLS; do
t_name=${row%%:*}; rest=${row#*:}
t_repo=${rest%%:*}; rest=${rest#*:}
t_bin=${rest%%:*}; t_alias=${rest#*:}
if [ -n "$WANT" ] && ! wanted "$t_name" "$t_bin"; then continue; fi
selected=1
install_tool "$t_repo" "$t_bin" "$t_alias" || true
done
if [ "$selected" -eq 0 ]; then
# Asked for something real that we publish no binary for. Answer with the
# reason from the one place that holds it, rather than a "no such tool"
# that reads like a typo. Non-zero: they asked for an install and did not
# get one.
for w in $WANT; do
why=$(unavailable_rows | while IFS='|' read -r u_name u_why; do
if [ "$u_name" = "$w" ]; then printf '%s' "$u_why"; fi
done)
if [ -n "$why" ]; then
fail "$w — $why"
printf ' %sinstallable today:%s%s\n' "$DM" "$(tool_names)" "$N"
exit 1
fi
done
die "no such tool:$WANT — installable today:$(tool_names)"
fi
if [ -z "$WANT" ]; then show_unavailable; fi
finish
# A failed download or a missing second name is a failed install, and the
# exit code has to say so — a half-install discovered days later is worse
# than one that stopped here.
if [ "$FAILED_N" -gt 0 ]; then exit 1; fi
exit 0
}
main "$@"