← All docs

Onboard a new machine

Bring a fresh laptop (or a new collaborator) into the Sponic Gardens working setup β€” credentials, hooks, memory, and a paste-ready session prompt.
2026-05-03 Dev task ~15 min human + agent walks the rest Touches: Bitwarden Β· Cloudflare Β· Azure Β· Supabase Β· ~/.claude

How to use this doc

Read the "What lives where" table once to orient. Then paste the prompt at the bottom into a fresh Claude Code session in the cloned repo β€” it walks the agent through tool checks, BW bootstrap, memory seeding, and the session-recording hook. Markdown source: doc/devtasks/onboard-new-machine.md.

What lives where

ThingWhere
Azure OpenAI endpoint, deployment, key1/key2BW Azure OpenAI β€” sponic-openai-eastus2 (gpt-image-2), id 41461676-1c12-4bc0-be89-b43f00f78928. Endpoint https://sponic-openai-eastus2.openai.azure.com/, API 2025-04-01-preview.
Azure SP (subscription Contributor)BW Azure β€” claude-code-sp (Service Principal), id 40b98339-3dce-4cec-9eaf-b43f00f43e80. Sub 785e237b-…, tenant 80f5e99d-….
R2 token (sponic-images)BW Cloudflare R2 β€” sponic-images, id 0a573694-8b9d-44f1-9600-b43f011a48cf. Account 394b5de6…. Public host pub-3a79691572bc4abc93fce31910fe320b.r2.dev.
CF Pages/Workers/D1BW Cloudflare β€” Wrangler API (Sponic Gardens compute), id 842043db-7664-4d40-aaaf-b43f013a211c.
Supabase Mgmt API (SponicControl)BW Supabase mgmt - SponicControl, id c8a5e31d-8c73-4f38-adfe-b43b016b80c2. Project ref xumcmantignrocihtrdx. (psql is unreachable β€” use the Mgmt API.)
Resend (transactional email)BW Resend β€” Sponic Gardens Email, id 87184c25-8884-497c-94d2-b41600586a6e. Key in custom field API Key (Full Access).
Agent DevOps SOP (how we work with Claude)docs/devtasks/agent-devops-sop β€” the canonical workflow doc. Lanes, five skills (/brainstorm, /feature, /journal, /ship, /wrap), build-journal pattern. Read this before any non-trivial work. Recap below.
Claude skills (auto-loaded from repo).claude/skills/<name>/SKILL.md β€” brainstorm, feature, journal, ship, wrap, devtask, taskmaster. Committed; come down with git pull. Nothing to install.
Image-gen wrapperapps/control/src/lib/image-gen.ts. All image generation routes through this (Azure β†’ R2 β†’ public.images). Lint script fails CI on direct calls.
Sessions D1 (live)Worker claude-sessions.sponicgarden.workers.dev, D1 37ba42be-b8cf-4e33-bb9e-268aca325978. Auth Bearer alpaca-sessions-2026. Schema includes user/machine/title.
Service-access recipes (load-bearing)infra/runbook.md β€” full BW unlock + per-service gotchas. Checked into git; every collaborator gets it on git pull. Grep by service name to jump to the recipe.
ttran β€” cross-machine file channelinfra/bin/ttran, folder /Volumes/PortoSams2T/ttran/ on ALPUCA. Drop-box for handing files between Claude sessions on different Macs without a code push. ttran put <file> returns a 4-char ID like AA01; ttran get AA01 from any machine that can SSH to ALPUCA fetches it. Setup section below. Recipe in infra/runbook.md β†’ "ttran".

BW unlock recipe (works in worktrees)

export BW_PASSWORD=$(security find-generic-password -a "[email protected]" -s "bitwarden-cli" -w)
SESSION=$(/opt/homebrew/bin/bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null)
/opt/homebrew/bin/bw get item "<item-id>" --session "$SESSION"

The shorter export BW_SESSION=$(~/bin/bw-unlock) works in a normal shell but fails inside Claude Code worktrees β€” always use the long form.

Windows machine setup

The recipes above are macOS-shaped. On Windows, use PowerShell (not cmd.exe β€” $env: syntax, ConvertFrom-Json, here-strings, and the bash here-doc patterns the rest of this doc relies on all break in cmd.exe) and substitute the equivalents below.

Toolchain β€” winget install

winget install OpenJS.NodeJS.LTS --silent --accept-source-agreements --accept-package-agreements --scope user
winget install Python.Python.3.13  --silent --accept-source-agreements --accept-package-agreements --scope user
winget install Bitwarden.CLI       --silent --accept-source-agreements --accept-package-agreements --scope user
winget install jqlang.jq           --silent --accept-source-agreements --accept-package-agreements --scope user

Skip Microsoft.AzureCLI and npm i -g wrangler unless you'll touch Azure or Cloudflare Workers directly.

After install, open a fresh PowerShell session β€” winget writes to per-user PATH and existing shells won't see it until restart. Verify:

node --version; npm --version; python --version; bw --version; jq --version

Gotchas

BW unlock β€” Windows DPAPI equivalent

Windows has no macOS Keychain. The PowerShell equivalent for "stash a secret that only this user on this machine can decrypt" is DPAPI: ConvertFrom-SecureString (no -Key) produces a DPAPI-encrypted blob bound to the current user + machine.

One-time stash (run from PowerShell, after bw login <email> succeeds):

New-Item -ItemType Directory -Force "$env:USERPROFILE\.bw" | Out-Null
Read-Host -AsSecureString "Bitwarden master password" `
  | ConvertFrom-SecureString `
  | Set-Content -Encoding utf8 "$env:USERPROFILE\.bw\master-password.dpapi"

Daily unlock helper β€” drop this at %USERPROFILE%\.bw\bw-unlock.ps1:

# bw-unlock.ps1 β€” non-interactive BW unlock via DPAPI-stashed master password
# Usage: $env:BW_SESSION = & "$env:USERPROFILE\.bw\bw-unlock.ps1"
$ErrorActionPreference = 'Stop'
$pwFile = "$env:USERPROFILE\.bw\master-password.dpapi"
if (-not (Test-Path $pwFile)) { throw "No stashed master password at $pwFile" }
$secure = Get-Content $pwFile | ConvertTo-SecureString
$env:BW_PASSWORD = [Net.NetworkCredential]::new('', $secure).Password
try {
    bw unlock --passwordenv BW_PASSWORD --raw
} finally {
    Remove-Item Env:BW_PASSWORD -ErrorAction SilentlyContinue
}

Then in any new PowerShell session:

$env:BW_SESSION = & "$env:USERPROFILE\.bw\bw-unlock.ps1"
bw get item 41461676-1c12-4bc0-be89-b43f00f78928 --session $env:BW_SESSION `
  | ConvertFrom-Json `
  | Select-Object name, @{n='fields';e={($_.fields | ForEach-Object Name) -join ', '}}

If that resolves the Azure OpenAI item, the Windows BW chain is wired up end-to-end equivalently to the macOS recipe above.

Agent worktree sandboxes can't see your BW state

If you delegate to a sandboxed worktree agent (Claude Code, Codex), the agent's tool processes may not be able to reach %APPDATA%\Bitwarden CLI and will report bw status β†’ unauthenticated even when your interactive shell is fine. Two ways around it:

  1. Shell-only workaround. Have the agent generate the BW commands and run them yourself in PowerShell, then paste the non-secret output back (item names, field name lists, etc) for the agent to verify. Don't paste session keys into the chat β€” they're decryption keys for everything in your vault.
  2. Persistent fix β€” bridge BW into .env.local on disk. Run infra/bin/bootstrap-env-local.ps1 from your interactive PowerShell (where $env:BW_SESSION is live). It fetches every secret the apps/control .env.example references β€” Azure OpenAI, R2, CF Wrangler, Resend, Supabase service-role β€” and writes a populated apps/control/.env.local to the main checkout. The worktree-env-symlink script then makes that file visible inside every worktree, so agents can cat apps/control/.env.local and get full credentials without ever touching BW. Re-run when secrets rotate.

Agent DevOps SOP β€” how we work with Claude

The repo ships a complete workflow for Claude Code sessions: five skills, two Stop hooks, and a build-journal pattern for multi-week projects. Nothing to install — the skills (.claude/skills/) and hooks (.claude/hooks/) are committed and auto-loaded the moment you open the repo in Claude Code.

The four-command workflow

/feature [<slug>]   β†’   build   β†’   /ship   β†’   (just stop)
                       (Claude journals      (auto-flush hook
                        automatically)       + auto-push hook)

For tiny edits (typo, doc, image catalog, new devtask): skip the worktree entirely. Edit on main, just stop. The auto-push hook handles the deploy.

For the full lane policy, skill details, build-journal schema, and "Which path?" decision table, read Agent DevOps SOP. That's the canonical doc — this section is just a recap.

First day on a new machine

After the BW + session-recording setup (above), do these once to confirm the SOP machinery works on this checkout:

If apps/control/.env.local doesn't exist in your main checkout yet, create it from apps/control/.env.example + values from BW (Supabase URL/keys etc) before the symlink step succeeds. Worktrees inherit the symlink automatically once the main file exists.

Generate an image (canonical Azure recipe)

ITEM=$(/opt/homebrew/bin/bw get item 41461676-1c12-4bc0-be89-b43f00f78928 --session "$SESSION")
ENDPOINT=$(echo "$ITEM" | jq -r '.fields[]|select(.name=="endpoint").value')
KEY=$(echo "$ITEM"      | jq -r '.fields[]|select(.name=="key1").value')

curl -X POST "${ENDPOINT}openai/deployments/gpt-image-2/images/generations?api-version=2025-04-01-preview" \
  -H "api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"a tiny green sprout in a terracotta pot","size":"1024x1024","n":1,"output_format":"png"}' \
  | jq -r '.data[0].b64_json' | base64 -d > out.png

For anything that lands in the catalog, call generateImage() from the wrapper instead β€” the curl recipe is for one-off probes only.

Session recording β€” per-machine setup

Every session on every machine should land in the live D1 with user (OS login), machine (short hostname), and title (Claude Code's auto-generated aiTitle) populated, so the /en/devcontrol/sessions dashboard can attribute and label work correctly when multiple collaborators share the project.

The canonical hook lives in this repo at apps/control/cloudflare/claude-sessions/hooks/save-session.sh. Each machine installs a copy at ~/.claude/hooks/save-session.sh with the production URL and token baked in.

Why each user does this once

The hook file is per-machine β€” it lives outside the repo because it embeds an auth token and runs against your local ~/.claude/projects/ JSONLs. Pulling the repo doesn't update it. When the canonical hook changes (like the 2026-05-04 update that added user/machine/aiTitle capture and replaced the time-based throttle with content-based dedup), every machine needs to re-run the install below.

Step-by-step install (or refresh)

Run this from a checkout of the sponic repo. Works on macOS β€” on Linux replace sed -i '' with sed -i.

# 1. Copy the canonical hook into place.
cp apps/control/cloudflare/claude-sessions/hooks/save-session.sh \
   ~/.claude/hooks/save-session.sh

# 2. Bake in the production URL + token.
sed -i '' \
  -e 's|API_URL="https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/sessions"|API_URL="https://claude-sessions.sponicgarden.workers.dev/sessions"|' \
  -e 's|API_TOKEN="YOUR_AUTH_TOKEN"|API_TOKEN="alpaca-sessions-2026"|' \
  ~/.claude/hooks/save-session.sh

# 3. Make it executable.
chmod +x ~/.claude/hooks/save-session.sh

# 4. Confirm the values landed.
grep -E "^API_(URL|TOKEN)=" ~/.claude/hooks/save-session.sh
# Expected:
#   API_URL="https://claude-sessions.sponicgarden.workers.dev/sessions"
#   API_TOKEN="alpaca-sessions-2026"

Step 5 β€” Register the hook (one-time, only if ~/.claude/settings.json doesn't already have it)

If you've never installed this hook before, also add a Stop entry to ~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/save-session.sh",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

If a Stop array already exists, append to its hooks list instead of replacing it. Use Stop, not SessionEnd β€” SessionEnd doesn't fire for worktree/subagent sessions and would silently lose them.

Step 6 β€” Restart any active Claude Code sessions

Already-running sessions don't reload settings.json or hook scripts. Quit and reopen Claude Code so the next Stop event picks up the new hook.

The harness may block writes to ~/.claude/hooks/

Some Claude Code configurations sandbox ~/.claude/hooks/. If cp fails with a permission error, run the cp + sed + chmod from a regular terminal outside Claude Code, or temporarily add ~/.claude/hooks to additionalDirectories in ~/.claude/settings.json so an agent can do it for you.

Claude Code β€” Windows variant

The macOS steps above assume bash resolves to the right interpreter via PATH, that .sh files execute via shebang, and that ~/.claude/settings.json user-level Stop hooks fire. None of those assumptions hold on Windows.

The combination that works on Windows (Claude Code 2.1.146, empirically verified 2026-05-21):

Step 1 β€” install the hook into %USERPROFILE%\.claude\hooks\save-session.sh

Same as macOS but with three sed substitutions instead of two β€” replace /usr/bin/python3 with python (winget Python's binary name). Run from Git Bash, anywhere inside the repo:

mkdir -p "$USERPROFILE/.claude/hooks"
cp apps/control/cloudflare/claude-sessions/hooks/save-session.sh \
   "$USERPROFILE/.claude/hooks/save-session.sh"
sed -i \
  -e 's|API_URL="https://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/sessions"|API_URL="https://claude-sessions.sponicgarden.workers.dev/sessions"|' \
  -e 's|API_TOKEN="YOUR_AUTH_TOKEN"|API_TOKEN="alpaca-sessions-2026"|' \
  -e 's|/usr/bin/python3|python|g' \
  "$USERPROFILE/.claude/hooks/save-session.sh"
chmod +x "$USERPROFILE/.claude/hooks/save-session.sh"

Step 2 β€” register the Stop hook in <repo>/.claude/settings.local.json, NOT user-level

Claude Code on Windows silently drops user-level Stop hooks when the project has its own Stop array; only project-scope and project-local-scope hooks fire. settings.local.json is gitignored, so this stays per-machine β€” every Windows collaborator adds their own.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "name": "Save Claude session to DevControl",
            "type": "command",
            "command": "$HOME/.claude/hooks/save-session.sh",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

The command must be the bash-style form $HOME/.claude/hooks/save-session.sh β€” matching the existing committed Stop hooks (wrap-on-stop.sh, auto-push-on-stop.sh). Claude Code on Windows auto-dispatches the command through bash, so bash.exe -c "..." wrappers, .cmd shims, and Windows absolute paths all fail silently. Forward slashes only.

Step 3 β€” set PYTHONUTF8=1 at User scope

So every Python process on the machine (including the hook's inline Python) defaults to UTF-8 for open(), stdin/stdout, and locale-derived encodings β€” preventing the cp1252 UnicodeDecodeError on JSONL transcripts that contain non-ASCII (em-dash, emoji, accented chars). Already covered in "Windows machine setup" above:

[Environment]::SetEnvironmentVariable('PYTHONUTF8', '1', 'User')

Takes effect for newly-spawned processes β€” including Claude Code itself after a restart, and the bash β†’ python chain the hook invokes.

Step 4 β€” restart Claude Code completely

Same as macOS: running sessions don't reload settings.json or hook scripts. Quit every window and reopen.

Canonical script version cutoff (2026-05-21)

Two Windows-specific bugs in the canonical hook were fixed in commit c94a96bd: a cp1252 UnicodeDecodeError reading UTF-8 JSONL transcripts, and WinError 206 "filename or extension is too long" on the curl subprocess when transcripts are large. If you sed-installed before that commit, re-run Step 1 to inherit the fixes. Macs are unaffected.

Backfill of pre-existing local JSONLs

Claude Code writes every session transcript to ~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl regardless of whether the Stop hook ran β€” so any sessions you had before installing the hook are still on disk and can be replayed into D1. The hook's content-based dedup (lockfile per session ID) makes this idempotent. Run from Git Bash:

HOOK="$USERPROFILE/.claude/hooks/save-session.sh"
python <<'PYEOF'
import json, os, subprocess, time, urllib.request
from pathlib import Path

req = urllib.request.Request(
    "https://claude-sessions.sponicgarden.workers.dev/sessions?user=" + os.environ.get("USERNAME","") + "&limit=200",
    headers={"Authorization": "Bearer alpaca-sessions-2026"},
)
d1_ids = {r["id"] for r in json.loads(urllib.request.urlopen(req).read())["sessions"]}

projects = Path.home() / ".claude" / "projects"
hook = os.environ["HOOK"]
posted = skipped = 0
for jsonl in sorted(projects.rglob("*.jsonl")):
    sid = jsonl.stem
    if sid in d1_ids:
        skipped += 1; continue
    cwd = ""
    with open(jsonl, encoding="utf-8") as f:
        for line in f:
            try:
                d = json.loads(line)
                if d.get("cwd"):
                    cwd = d["cwd"]; break
            except: pass
    payload = json.dumps({"session_id": sid, "cwd": cwd or str(Path.home())})
    print(f"  POST {sid} ({jsonl.stat().st_size//1024} KB)")
    r = subprocess.run(
        ["C:/Program Files/Git/bin/bash.exe", hook],
        input=payload, text=True, capture_output=True, timeout=60,
    )
    if r.returncode == 0:
        posted += 1
    time.sleep(0.3)
print(f"\nposted={posted} skipped={skipped}")
PYEOF

Verify it landed

End a turn in Claude Code (the Stop hook fires after every assistant response), then:

curl -s "https://claude-sessions.sponicgarden.workers.dev/sessions?limit=1" \
  -H "Authorization: Bearer alpaca-sessions-2026" \
| python3 -c "import json,sys; s=json.load(sys.stdin)['sessions'][0]; \
  print('user=', s.get('user'), ' machine=', s.get('machine'), \
        ' title=', s.get('title'), ' project=', s.get('project'))"

Expected: user=<your-OS-login> machine=<your-short-hostname> title=<Claude's auto-title or None for very short sessions> project=sponic. If user/machine come back as None, the hook didn't fire β€” confirm the Stop entry is registered and the file is executable.

Force a re-save (debug)

The hook dedups by the UUID of the last JSONL entry, stored at ~/.claude/hooks/.session-locks/<session-id>. To force the next Stop to re-upload the current session:

rm ~/.claude/hooks/.session-locks/<session-id>

List session IDs (filenames) under ~/.claude/projects/<encoded-cwd>/.

Deploying the worker (maintainer task β€” only when src/index.js changes)

Most collaborators never need this β€” installing the local hook is enough. You only deploy the worker when its code changes (e.g. accepting new POST fields, adding query params). Pushing to main does not auto-deploy the worker; it's a manual wrangler deploy against the sponicgarden compute account.

Two gotchas worth knowing first

# 1. Unlock Bitwarden if you don't have a session yet.
export BW_SESSION=$(/opt/homebrew/bin/bw unlock --raw)

# 2. Pull the Wrangler API token. BW item 842043db is "Cloudflare β€”
#    Wrangler API (Sponic Gardens compute)" β€” has Workers/D1/Pages write.
export CLOUDFLARE_API_TOKEN=$(
  /opt/homebrew/bin/bw get item 842043db-7664-4d40-aaaf-b43f013a211c \
    --session "$BW_SESSION" | jq -r '.login.password'
)

# 3. Pin the account ID (sponicgarden compute).
export CLOUDFLARE_ACCOUNT_ID=394b5de665bbfdea54cdd57be9094762

# 4. Sanity check β€” should print "[email protected]'s Account".
cd ~/sponic/apps/control/cloudflare/claude-sessions
npx wrangler whoami

# 5. Deploy. Absolute paths matter β€” see gotcha #1 above.
npx wrangler deploy "$(pwd)/src/index.js" -c "$(pwd)/wrangler.jsonc"

A successful deploy ends with:

Deployed claude-sessions triggers (...)
  https://claude-sessions.sponicgarden.workers.dev
Current Version ID: ...

Verify the deploy

Round-trip a probe row to confirm any newly-bound fields land:

curl -s -X POST "https://claude-sessions.sponicgarden.workers.dev/sessions" \
  -H "Authorization: Bearer alpaca-sessions-2026" \
  -H "Content-Type: application/json" \
  -d '{"id":"deploy-probe-'"$(date +%s)"'","project":"sponic","user":"'"$(whoami)"'","machine":"'"$(hostname -s)"'","summary":"post-deploy probe"}' \
  | python3 -m json.tool

curl -s "https://claude-sessions.sponicgarden.workers.dev/sessions?user=$(whoami)&limit=1" \
  -H "Authorization: Bearer alpaca-sessions-2026" | python3 -m json.tool

If the second call's row has user and machine populated, the deploy is live.

Schema migrations (rare)

For a fresh DB or a new column, run the appropriate migration with --remote:

cd ~/sponic/apps/control/cloudflare/claude-sessions
npx wrangler d1 execute claude-sessions \
  --file=migrations/001-add-user-machine.sql --remote

The live D1 (37ba42be-…) already has user, machine, title β€” that specific migration is a no-op against it. Future migrations should be additive and applied here before the matching worker code ships.

ttran β€” quick file channel between machines

ttran is a tiny SSH-backed drop-box living on ALPUCA. Any machine that can SSH to ALPUCA can put a file and get back a 4-char ID (AA01, AA02, …); any Claude session on any other machine can cat/get it back. Use it to hand prompts, scratch results, and pasted context between sessions without bloating the repo.

For setup and full usage, see the dedicated reference: /docs/reference/TTRAN. It's the single source of truth β€” covers the three-step setup (SSH access, PATH symlink, global ~/.claude/CLAUDE.md block), smoke test, day-to-day usage, the worked two-Claude-session handoff, and failure modes. Tailscale is Sponic's chosen way to reach ALPUCA; the wrapper itself is host-agnostic via TTRAN_HOST.

After setup, sanity-check the symlink and the global CLAUDE.md block:

which ttran                              # should print a path on $PATH
ttran help | head -1                     # should start "ttran β€” Textfile Transfer..."
grep -c "## ttran" ~/.claude/CLAUDE.md   # should print 1+

Operational recipe (BW unlock, drive paths, internals): infra/runbook.md β†’ "ttran".

Session prompt for a new machine

Paste everything between the dividers into a fresh Claude Code session in this repo. It bootstraps the same context the primary machine has.

I'm a new collaborator (or this is a new machine) on the Sponic Gardens monorepo. Help me inherit the working setup. Read these in order, then summarize what I need to do manually before any agent work can run: 1. CLAUDE.md (image-generation rules, branching, where the research vault lives). 2. docs/devtasks/agent-devops-sop.md β€” the canonical workflow for Claude Code sessions (lanes, five skills, build journal). Skip to "What to actually do" first. 3. systemarchitecture.md. 4. config/project.config.ts (BW item IDs, env-var names, account IDs). 5. doc/devtasks/onboard-new-machine.md (the markdown source of this page β€” "What lives where" is the index). Then verify the local toolchain (bw, jq, curl, python3, node β‰₯20, gh, optionally wrangler/az). Tell me which are missing and the brew/npm commands to install. Walk me through the BW bootstrap a human has to do once: bw login <email>; security add-generic-password to stash the master password in Keychain; get added to the devops-sponic collection; then test that the unlock recipe resolves bw get item 41461676-…. Service recipes live in infra/runbook.md β€” you already have it from git pull. No separate memory file to copy or seed. Grep by service name to jump to the recipe (e.g. grep -nA 30 "## Cloudflare R2" infra/runbook.md). When you discover a faster access pattern, update the runbook and commit. Sanity check the wrapper rule: ask me "make me an image of a tiny green sprout" and verify you would route through apps/control/src/lib/image-gen.ts. Don't actually run unless I confirm. Walk me through session recording: confirm ~/.claude/hooks/save-session.sh exists and posts to claude-sessions.sponicgarden.workers.dev. Diff it against the canonical apps/control/cloudflare/claude-sessions/hooks/save-session.sh in this repo β€” if they differ, walk me through the install steps in this doc (cp + sed to bake the URL/token + chmod) and remind me to restart any running Claude Code sessions afterwards. The harness may block writes to ~/.claude/hooks/, so I may need to run cp/sed myself in a regular terminal. Then run the verification curl and report whether the latest row has user/machine/title populated.

Notes for the maintainer when sharing this

Doc owner: Rahul. Last updated 2026-05-03. Markdown source of truth: doc/devtasks/onboard-new-machine.md.