#!/usr/bin/env bash # Claude setup — installs Claude Code and signs this computer in with the # setup-token you were given. Nothing runs in the background afterwards and no # usage is reported anywhere. set -u say() { printf '%s\n' "$*"; } say "" say " Claude setup" say " ============" say "" # ---------------------------------------------------------------- 1. Claude Code NEED_NEW_SHELL=0 if command -v claude >/dev/null 2>&1; then say " [1/2] Claude Code is already installed" else say " [1/2] Installing Claude Code (this takes a minute)..." if ! curl -fsSL https://claude.ai/install.sh | bash >/tmp/claude-install.$$ 2>&1; then say "" say " [X] Could not install Claude Code. Last lines:" tail -5 /tmp/claude-install.$$ 2>/dev/null | sed 's/^/ /' rm -f /tmp/claude-install.$$ exit 1 fi rm -f /tmp/claude-install.$$ # the installer drops the binary here but only adds it to PATH for new shells for d in "$HOME/.local/bin" "$HOME/.claude/bin"; do [ -x "$d/claude" ] && PATH="$d:$PATH" done export PATH NEED_NEW_SHELL=1 say " installed" fi # ---------------------------------------------------------------- 2. the token # (written the long way on purpose: a dollar-brace default here would be read # as a JavaScript template expression before this script is ever served) TOKEN="" if [ $# -ge 1 ]; then TOKEN="$1"; fi if [ -z "$TOKEN" ]; then if [ ! -r /dev/tty ]; then say "" say " [X] No terminal to ask for the token. Run it like this instead:" say " curl -fsSL URL_HERE | bash -s -- sk-ant-oat01-..." exit 1 fi say "" say " [2/2] Paste the setup token you were given, then press Enter." say " (it stays hidden while you type)" printf " Setup token: " IFS= read -rs TOKEN < /dev/tty printf "\n" fi # strip stray spaces / quotes from a copy-paste TOKEN="$(printf '%s' "$TOKEN" | tr -d '[:space:]' | tr -d "\"'")" case "$TOKEN" in sk-ant-*) ;; "") say ""; say " [X] No token entered."; exit 1 ;; *) say "" say " [X] That does not look like a setup token." say " It should start with sk-ant-oat01-" exit 1 ;; esac # last 8 characters — the same 8 the admin panel shows, so the person can # check at a glance that the right token landed TAIL="$(printf '%s' "$TOKEN" | tail -c 8)" say " token ending ...$TAIL" PY="$(command -v python3 || command -v python || true)" if [ -z "$PY" ]; then say "" say " [X] Python 3 is required and was not found." say " macOS : xcode-select --install" say " Ubuntu: sudo apt install -y python3" exit 1 fi TOKEN="$TOKEN" "$PY" - <<'PYEOF' import json, os, shutil, sys from pathlib import Path HOME = Path.home() TOKEN = os.environ["TOKEN"] ok, notes = [], [] def load(p): """Parse JSON, tolerating // and /* */ comments and trailing commas.""" try: text = p.read_text(encoding="utf-8") except Exception: return {} if text[:1] == "": text = text[1:] out, i, n, in_str, esc = [], 0, len(text), False, False while i < n: c = text[i] if in_str: out.append(c) if esc: esc = False elif c == "\\": esc = True elif c == '"': in_str = False i += 1; continue if c == '"': in_str = True; out.append(c); i += 1; continue if c == "/" and i + 1 < n and text[i + 1] == "/": j = text.find("\n", i); i = n if j < 0 else j; continue if c == "/" and i + 1 < n and text[i + 1] == "*": j = text.find("*/", i + 2); i = n if j < 0 else j + 2; continue out.append(c); i += 1 s = "".join(out) res, i, n, in_str, esc = [], 0, len(s), False, False while i < n: c = s[i] if in_str: res.append(c) if esc: esc = False elif c == "\\": esc = True elif c == '"': in_str = False i += 1; continue if c == '"': in_str = True; res.append(c); i += 1; continue if c == ",": k = i + 1 while k < n and s[k] in " \t\r\n": k += 1 if k < n and s[k] in "}]": i += 1; continue res.append(c); i += 1 try: d = json.loads("".join(res)) return d if isinstance(d, dict) else {} except Exception: return {} def save(p, data): """Write JSON, keeping one backup of whatever was there first.""" p.parent.mkdir(parents=True, exist_ok=True) if p.exists(): bak = p.with_name(p.name + ".bak-before-setup") if not bak.exists(): try: shutil.copy2(p, bak) except Exception: pass tmp = p.with_name(p.name + ".tmp-setup") tmp.write_text(json.dumps(data, indent=2), encoding="utf-8") os.replace(tmp, p) try: os.chmod(p, 0o600) except Exception: pass # the credential. settings.json is the one place the terminal CLI, the VS Code # extension and the desktop app all read. sf = HOME / ".claude" / "settings.json" data = load(sf) env = data.get("env") if not isinstance(env, dict): env = {} env["CLAUDE_CODE_OAUTH_TOKEN"] = TOKEN data["env"] = env data.setdefault("hasCompletedOnboarding", True) save(sf, data) ok.append("signed in") # an older browser login would outrank the token, so set it aside (kept) cf = HOME / ".claude" / ".credentials.json" if cf.exists(): bak = cf.with_name(".credentials.json.bak-personal") try: if bak.exists(): bak.unlink() cf.rename(bak) notes.append("a previous Claude login was saved at\n " + str(bak)) except Exception: pass # skip the first-run login menu in the terminal cj = HOME / ".claude.json" d = load(cj) d["hasCompletedOnboarding"] = True save(cj, d) ok.append("terminal ready (no login screen)") # stop VS Code flashing its login tab on every start — best effort, and a # settings file we cannot read is left completely alone vs = [] base = HOME / "Library" / "Application Support" if sys.platform == "darwin" else Path( os.environ.get("XDG_CONFIG_HOME") or (HOME / ".config")) for name in ("Code", "Code - Insiders", "Code - OSS", "VSCodium", "Cursor", "Windsurf"): d2 = base / name / "User" if not d2.is_dir(): continue p = d2 / "settings.json" try: cur = load(p) if p.exists() else {} if cur.get("claudeCode.disableLoginPrompt") is True and cur.get("claudeCode.hideOnboarding") is True: continue cur["claudeCode.disableLoginPrompt"] = True cur["claudeCode.hideOnboarding"] = True save(p, cur) vs.append(name) except Exception: continue if vs: ok.append("VS Code login pop-up disabled (" + ", ".join(vs) + ")") for line in ok: print(" [OK] " + line) for line in notes: print(" [i] " + line) PYEOF rc=$? if [ $rc -ne 0 ]; then say "" say " [X] Setup did not finish." exit $rc fi say "" say " All done." say "" if [ "$NEED_NEW_SHELL" = "1" ]; then say " Open a NEW terminal (this one does not know about claude yet), then type:" else say " Close Claude / VS Code if they are open, reopen them, then type:" fi say " claude" say ""