#!/bin/sh # shipd join — terminal-native onboarding. No email, no browser, no dashboard. # # curl -fsSL https://shipd.aikaara.com/join | sh # # What it does (read it, it's short): # 1. finds your SSH key (~/.ssh/id_ed25519 by default; SHIPD_KEY=path to pick another) # 2. asks shipd for a nonce, signs it with `ssh-keygen -Y sign` (proof you hold the key) # 3. shipd checks the key is on github.com/.keys → your GitHub login is your identity # 4. prints your MCP token once, and installs it into Claude Code if `claude` is on PATH # Nothing leaves your machine except: your GitHub login, your PUBLIC key, and the signature. set -eu trap 'rm -f "$HOME/.shipd.pub.tmp"' EXIT API="${SHIPD_API:-https://api.aikaara.com/api/v1/mcp}" KEY="${SHIPD_KEY:-}" GH="${SHIPD_GITHUB:-${1:-}}" say() { printf '%s\n' "$*" >&2; } die() { say "shipd: $*"; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "needs $1"; } need ssh-keygen; need curl json() { python3 -c 'import sys,json; d=json.load(sys.stdin); print(d.get(sys.argv[1],""))' "$1" 2>/dev/null || true; } command -v python3 >/dev/null 2>&1 || die "needs python3 (for json)" # 1. github login (needed first — we pick the local key that is actually on the account) if [ -z "$GH" ]; then GH="$(gh api user -q .login 2>/dev/null || true)" [ -z "$GH" ] && GH="$(git config --get github.user 2>/dev/null || true)" fi if [ -z "$GH" ]; then printf 'github login: ' >&2; read -r GH )" GHKEYS="$(curl -fsS "https://github.com/$GH.keys" 2>/dev/null | cut -d' ' -f2)" || die "github.com/$GH.keys not reachable (typo in login?)" [ -n "$GHKEYS" ] || die "github.com/$GH has no SSH keys. add one at github.com/settings/keys, re-run." # 2. key: SHIPD_KEY if given; else the first local key (~/.ssh/*.pub, then the agent) that GitHub knows PUB=""; SIGNKEY="" pick() { # $1 = candidate pub line → sets PUB if GitHub has it k="$(printf '%s' "$1" | cut -d' ' -f2)"; [ -n "$k" ] || return 1 printf '%s\n' "$GHKEYS" | grep -qxF "$k" && PUB="$(printf '%s' "$1" | cut -d' ' -f1,2)" } if [ -n "$KEY" ]; then [ -f "$KEY" ] || die "SHIPD_KEY=$KEY not found" if [ -f "$KEY.pub" ]; then PUB="$(cut -d' ' -f1,2 "$KEY.pub")"; else PUB="$(ssh-keygen -y -f "$KEY" /dev/null | cut -d' ' -f1,2)"; fi [ -n "$PUB" ] || die "could not read a public key for $KEY" printf '%s\n' "$GHKEYS" | grep -qxF "$(printf '%s' "$PUB" | cut -d' ' -f2)" || die "$KEY is not on github.com/$GH.keys" else for f in "$HOME"/.ssh/*.pub; do [ -f "$f" ] || continue if pick "$(cat "$f")"; then KEY="${f%.pub}"; break; fi done if [ -z "$PUB" ] && command -v ssh-add >/dev/null 2>&1; then OLDIFS=$IFS; IFS=' '; for line in $(ssh-add -L 2>/dev/null); do if pick "$line"; then KEY=""; break; fi; done; IFS=$OLDIFS fi fi if [ -z "$PUB" ]; then say "none of your local keys are on github.com/$GH.keys:" for f in "$HOME"/.ssh/*.pub; do [ -f "$f" ] && say " $(ssh-keygen -lf "$f" | awk '{print $2}') $f"; done die "add one at github.com/settings/keys (pbcopy < ~/.ssh/id_ed25519.pub), or pick one: SHIPD_KEY=~/.ssh/ curl … | sh" fi FP="$(printf '%s' "$PUB" | ssh-keygen -lf - 2>/dev/null | awk '{print $2}')" # signing: through the agent when it holds the key (-f ⇒ agent), else the private key with a tty prompt printf '%s\n' "$PUB" > "$HOME/.shipd.pub.tmp" if command -v ssh-add >/dev/null 2>&1 && ssh-add -L 2>/dev/null | grep -qF "$(printf '%s' "$PUB" | cut -d' ' -f2)"; then SIGNKEY="$HOME/.shipd.pub.tmp" elif [ -n "$KEY" ] && [ -f "$KEY" ]; then SIGNKEY="$KEY" else die "key $FP is on GitHub but not loaded in ssh-agent and no private file found — ssh-add it" fi say "→ github $GH" say "→ key $FP (${KEY:-ssh-agent}) ✔ on github" # 3. challenge → sign → claim body=$(python3 -c 'import json,sys; print(json.dumps({"github":sys.argv[1],"pubkey":sys.argv[2]}))' "$GH" "$PUB") ch=$(curl -sS -X POST "$API/join/challenge" -H 'Content-Type: application/json' -d "$body") NONCE=$(printf '%s' "$ch" | json nonce); [ -n "$NONCE" ] || die "$(printf '%s' "$ch" | json error)" SIG=$(printf '%s' "$NONCE" | ssh-keygen -Y sign -n shipd-join -f "$SIGNKEY" 2>/dev/null) || { [ "$SIGNKEY" != "$KEY" ] && [ -n "$KEY" ] && SIG=$(printf '%s' "$NONCE" | ssh-keygen -Y sign -n shipd-join -f "$KEY" 2>/dev/tty); } || die "signing failed — ssh-add ${KEY:-your key} (OpenSSH ≥ 8.0 needed)" claim=$(python3 -c 'import json,sys; print(json.dumps({"github":sys.argv[1],"pubkey":sys.argv[2],"signature":sys.argv[3]}))' "$GH" "$PUB" "$SIG") res=$(curl -sS -X POST "$API/join/claim" -H 'Content-Type: application/json' -d "$claim") TOKEN=$(printf '%s' "$res" | json token); [ -n "$TOKEN" ] || die "$(printf '%s' "$res" | json error)" ALPHA=$(printf '%s' "$res" | json alpha) # 4. output + install say "" say "✔ joined as @$GH" case "$ALPHA" in [Tt]rue) say "★ alpha seat active — container deploys + postgres unlocked · 200/day" ;; *) say "· deploys: locked until a seat is granted — say hi in chat (below)" ;; esac say "" printf '%s\n' "$TOKEN" say "" mkdir -p "$HOME/.shipd" && umask 077 && printf '%s\n' "$TOKEN" > "$HOME/.shipd/token" && say "saved to ~/.shipd/token (0600)" if command -v claude >/dev/null 2>&1; then claude mcp add --transport http shipd https://mcp.aikaara.com/sse --header "Authorization: Bearer $TOKEN" >/dev/null 2>&1 \ && say "added to claude code: claude mcp list" || say "add manually: claude mcp add --transport http shipd https://mcp.aikaara.com/sse --header \"Authorization: Bearer \"" else say "mcp config: {\"mcpServers\":{\"shipd\":{\"url\":\"https://mcp.aikaara.com/sse\",\"headers\":{\"Authorization\":\"Bearer \"}}}}" fi say "" say "next: say “deploy this with shipd” to your agent" say "chat: curl -fsSL https://shipd.aikaara.com/chat | sh (talk to the team, terminal to terminal)" say "alerts: curl -fsSL https://shipd.aikaara.com/alerts | sh (get told when a bug you report is fixed)"