#!/bin/sh # meathook enrollment — device code flow (spec §6.4). # # Adds this machine to a meathook board without copying a secret between # machines. The token is minted HERE, from /dev/urandom, and never leaves: # the server is sent only its sha256 hash, and the reply that says "approved" # carries nothing secret. So there is no value to paste into a terminal, no # value in a shell history, and none in an agent's transcript. # # 1. this script mints a token and asks the server for a short code # 2. you type that code into https:///enroll, signed in as operator # 3. you approve; the server registers the hash as an ingest token # 4. this script writes ~/.meathook/config.json and stops # # It does NOT edit your Claude Code settings — it prints that command for you # to run. Dependencies: curl, sha256 (shasum or sha256sum), od. # # Usage: meathook-enroll.sh [--url URL] [--machine LABEL] [--force] set -eu URL="${MEATHOOK_URL:-https://meathook.ai}" MACHINE="" FORCE=0 while [ $# -gt 0 ]; do case "$1" in --url) URL="$2"; shift 2 ;; --machine) MACHINE="$2"; shift 2 ;; --force) FORCE=1; shift ;; -h|--help) sed -n '2,20p' "$0"; exit 0 ;; *) echo "unknown argument: $1" >&2; exit 2 ;; esac done URL=${URL%/} [ -n "$MACHINE" ] || MACHINE=$(hostname -s) case "$URL" in https://*|http://localhost*|http://127.0.0.1*) ;; *) echo "refusing: $URL is not https — the ingest token would travel in clear text" >&2 exit 2 ;; esac CONFIG="$HOME/.meathook/config.json" if [ -f "$CONFIG" ] && [ "$FORCE" -eq 0 ]; then echo "$CONFIG already exists. Re-run with --force to replace it." >&2 exit 2 fi sha() { if command -v shasum >/dev/null 2>&1; then shasum -a 256; else sha256sum; fi; } jget() { printf '%s' "$1" | grep -o '"'"$2"'"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 | sed 's/.*:[[:space:]]*"\(.*\)"$/\1/' } # The token exists only in this shell and, at the end, in a 0600 config file. TOKEN="mh_ing_$(od -An -tx1 -N20 /dev/urandom | tr -d ' \n')" HASH=$(printf '%s' "$TOKEN" | sha | cut -d' ' -f1) PREFIX=$(printf '%s' "$TOKEN" | cut -c1-13) START=$(curl -sS --max-time 10 -X POST "$URL/v1/enroll/device" \ -H 'Content-Type: application/json' \ -d "{\"token_hash\":\"$HASH\",\"token_prefix\":\"$PREFIX\",\"machine\":\"$MACHINE\"}") USER_CODE=$(jget "$START" user_code) DEVICE_CODE=$(jget "$START" device_code) VERIFY=$(jget "$START" verification_uri) if [ -z "$USER_CODE" ] || [ -z "$DEVICE_CODE" ]; then echo "enrollment request failed: $START" >&2 exit 1 fi echo echo " Go to: $VERIFY" echo " Enter: $USER_CODE" echo " As: $MACHINE" echo echo "Waiting for approval (ten minutes, Ctrl-C to give up)..." # The server tells us how often to ask and how long the code lives; don't # out-guess it, and don't hammer a rate-limited public endpoint. INTERVAL=$(printf '%s' "$START" | grep -o '"interval"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$') EXPIRES=$(printf '%s' "$START" | grep -o '"expires_in"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$') [ -n "$INTERVAL" ] || INTERVAL=3 [ -n "$EXPIRES" ] || EXPIRES=600 WAITED=0 while [ "$WAITED" -lt "$EXPIRES" ]; do sleep "$INTERVAL" WAITED=$((WAITED + INTERVAL)) POLL=$(curl -sS --max-time 10 -X POST "$URL/v1/enroll/poll" \ -H 'Content-Type: application/json' -d "{\"device_code\":\"$DEVICE_CODE\"}") case "$(jget "$POLL" status)" in pending) ;; approved) break ;; denied) echo "Denied by the operator. Nothing was installed." >&2; exit 1 ;; expired) echo "The code expired. Run this again." >&2; exit 1 ;; *) echo "Unexpected reply: $POLL" >&2; exit 1 ;; esac done if [ "$WAITED" -ge "$EXPIRES" ]; then echo "Timed out waiting for approval. Run this again." >&2 exit 1 fi # The operator may have corrected the label; the token record is authoritative # for it either way (§6.1), so record what the server actually decided. APPROVED_MACHINE=$(jget "$POLL" machine) [ -n "$APPROVED_MACHINE" ] || APPROVED_MACHINE="$MACHINE" mkdir -p "$HOME/.meathook" UMASK_WAS=$(umask); umask 077 cat >"$CONFIG" < /tmp/mh-merged.json mv /tmp/mh-merged.json ~/.claude/settings.json else mkdir -p ~/.claude && cp /tmp/mh-hooks.json ~/.claude/settings.json fi Hooks load at session start, so start a new Claude Code session to appear. Second, schedule the reaper. Hooks cannot see a closed terminal — no hook fires when the window goes away — so without this, sessions you close stay on the board until the deadman timers catch them, and the ones sitting in idle or blocked never do: sh ~/.meathook/meathook-reap.sh --install It installs a job that runs every minute, and sends nothing at all if it cannot ask the harness what is running. See for yourself first: sh ~/.meathook/meathook-reap.sh --dry-run EOF