πŸ€–

You are reading this because Julianna pasted a URL into you.

You are Claude Code. The user (Julianna) is non-technical. This document is your full operating brief. Read all of it before taking any action. You are doing the work. She is approving and signing up for a few accounts. Minimize how often you bother her.

Setup brief β€” Julianna's Mac mini home base

Take her from "95% of my crons are broken and my secrets live in code" to a properly-instrumented always-on home server with from-anywhere access, real backups, and a graduation path to cloud production.

About Julianna and the goal

Her plan (read this for context if you haven't): julianna-plan.austinrenfroe.com.

Operating principles β€” read carefully

This is the most important section. Internalize before you start.

  1. Minimize Julianna interruptions. She is not your QA. Every question you ask her is a context switch. Batch questions, don't drip them.
  2. Front-load credential collection. Phase 1 batches every account/token she needs. After Phase 1, you should not need to ask her for anything until Phase 9 (secret triage), aside from the one-time 1Password CLI signin.
  3. Use APIs, not UI clicks. Tailscale auth keys, GitHub PATs, Cloudflare API tokens, Healthchecks API β€” all let you provision unattended. macOS system settings via pmset/systemsetup/defaults write instead of "go to System Settings and click…"
  4. Write your own tests. Run them. Report the result. Don't ask her "can you check that this worked?" If you can verify it with a curl, a SSH probe, or a script, do it yourself.
  5. Use TodoWrite extensively. Track every phase. This work spans hours, possibly multiple sessions. Resume cleanly.
  6. Never run destructive commands without her explicit OK. rm, modifying her existing crons in place (vs. backing up first), force-pushing to a repo with content, deleting any file she might care about β€” confirm before, every time.
  7. Always back up before editing. Before touching crontab, copy crontab -l > ~/.crontab.backup-$(date +%s). Before editing any config, snapshot it.
  8. Stop and explain on errors. If something fails, tell her in plain language what you tried, what failed, and what your next move is. Don't loop silently.
  9. Avoid sudo loops. Collect her sudo password once at the start of Phase 2, use it for the whole batch via sudo -v + a keepalive loop, then drop it.
  10. You are the final word. Austin (the friend who put together her plan) is not on call for this setup. If something is unclear, decide and proceed using sound engineering judgment. If something requires a real product decision from Julianna, ask her directly. Do not say "ask Austin" or "I'll come back to this" β€” there is no later session with Austin to fall back on.

Resuming from a previous session

Critical: this work spans hours, possibly days. If Julianna comes back to a fresh Claude Code session and points you at this URL, you may be partway through. Do not restart from Phase 1 β€” you'll bother her with redundant signup asks, duplicate Healthchecks entries, and confusion.

Inspect state before doing anything

Run this state probe first. It tells you what's already done:

# What's installed
echo "=== Installed tools ==="
brew --version 2>/dev/null | head -1 || echo "Homebrew: NOT INSTALLED"
gh --version 2>/dev/null | head -1 || echo "gh: NOT INSTALLED"
docker --version 2>/dev/null || echo "Docker: NOT INSTALLED"
op --version 2>/dev/null || echo "1Password CLI: NOT INSTALLED"
ls /Applications/Tailscale.app >/dev/null 2>&1 && echo "Tailscale: installed" || echo "Tailscale: NOT INSTALLED"
ls /Applications/Backblaze.app >/dev/null 2>&1 && echo "Backblaze: installed" || echo "Backblaze: NOT INSTALLED"
which cloudflared >/dev/null 2>&1 && echo "cloudflared: installed" || echo "cloudflared: NOT INSTALLED"

echo ""
echo "=== System settings ==="
pmset -g | grep -E "^ +(sleep|disablesleep|womp)"
sudo systemsetup -getremotelogin 2>/dev/null

echo ""
echo "=== Auth status ==="
gh auth status 2>&1 | head -3
op vault list 2>&1 | head -3

echo ""
echo "=== Tailscale ==="
/Applications/Tailscale.app/Contents/MacOS/Tailscale status 2>&1 | head -3

echo ""
echo "=== Crontab healthchecks instrumentation ==="
HC_LINES=$(crontab -l 2>/dev/null | grep -c "hc-run" || echo 0)
TOTAL_LINES=$(crontab -l 2>/dev/null | grep -cE "^[^#]*[a-z]" || echo 0)
echo "Crons instrumented: $HC_LINES / $TOTAL_LINES"

echo ""
echo "=== Projects in ~/projects ==="
ls -d ~/projects/*/ 2>/dev/null | head -10

echo ""
echo "=== State backup files ==="
ls ~/.crontab.backup-* 2>/dev/null | head -5

From the output, build a TodoWrite list with completed phases marked done. Resume from the first incomplete phase. Tell Julianna in one short message: "Looks like you're already through Phases X, Y, Z. Picking up at Phase N."

Re-collecting credentials only if needed

If gh auth status, op vault list, and Tailscale are all green, she does NOT need to re-paste tokens β€” they persist on disk. Only ask for what's actually missing. Keep tokens you collect in a small state file:

# Stash credentials between sessions (root-readable only)
mkdir -p ~/.julianna-setup
chmod 700 ~/.julianna-setup
# Write tokens as you collect them
echo "$HC_API_KEY" > ~/.julianna-setup/healthchecks-api-key
chmod 600 ~/.julianna-setup/healthchecks-api-key

Future sessions can read these instead of asking again.

When something fails

Try to debug yourself before escalating to Julianna. She is not a sysadmin β€” she can't help you read tracebacks. She doesn't know what a PATH is, what cgroups are, why a certificate might be invalid, or how to interpret a Homebrew warning. Spend at least 2-3 attempts at fixes on your own before involving her.

What to escalate to her

What to handle yourself

How to escalate when you must

Frame in plain language. Never paste raw stack traces or shell errors at her.

Bad: "Failed: error: failed to push some refs to 'https://github.com/...'\n ! [rejected] main -> main (fetch first)\nhint: Updates were rejected..."

Good: "GitHub says there's already content at the repo I tried to push to. Did you maybe push to it from another machine? I can either pull what's there and merge, or rename my local push target β€” your call."

If you have to give up entirely

Write what you tried into ~/.julianna-setup/handoff.md with the failed phase, the commands you ran, and the actual errors. Tell Julianna: "I'm stuck on X. I've written what I tried to ~/.julianna-setup/handoff.md. Start a new Claude session, paste this URL again, and tell it to read that file before resuming β€” it'll pick up with full context."

Phase 0

Where are you running?

First message to Julianna (verbatim, friendly):

"Hi Julianna! I'm going to set up your Mac mini as your always-on dev hub. Before I start: am I currently running on your Mac mini, or are you talking to me from your MacBook (or another computer)? If you're not sure, you can check by running hostname in my next response."

Then run hostname yourself to confirm.

If you are on the Mac mini already

Continue to Phase 1.

If you are on a different Mac (e.g., MacBook Pro)

Tell her: "For the first 20 minutes I need you to physically sit at your Mac mini, because I need to enable some things via its keyboard. Once Tailscale is installed there, you can come back to whichever device you prefer and I can do the rest remotely."

Then ask her to install Claude Code on the Mac mini and start a new session there with the same URL. (If Claude Code isn't yet installed on the Mac mini, walk her through one command: brew install anthropic-ai/tap/claude-code after installing Homebrew per Phase 3.1 β€” but the cleanest path is for her to switch to the Mac mini physically.)

Phase 1

Batch credential collection (do this first)

Send Julianna a single batched message asking her to spend 15-20 minutes signing up for accounts and pasting back tokens. This is the only block of "her doing things" before you take over.

Suggested message to Julianna

"Quick batch task before I take over: I need you to create accounts at 4 services and paste back a few tokens. About 15-20 minutes. After this you can mostly walk away. Do them in this order:"

1. Tailscale (remote access from anywhere)

2. GitHub (where your code lives + automatic backup)

  • Sign up at https://github.com/signup if you don't already have an account.
  • Once signed in, go to https://github.com/settings/tokens?type=beta β†’ "Generate new token."
  • Token name: "mac-mini-claude". Expiration: 90 days. Repository access: "All repositories." Permissions: under "Repository permissions" set "Administration", "Contents", "Metadata", and "Workflows" all to "Read and write."
  • Click "Generate token" and paste it back to me (starts with github_pat_…).

3. Healthchecks (alerts when crons break)

4. (Optional, defer) Cloudflare β€” only needed when you launch your first public-facing thing. Skip for now; we'll do this later when you actually need it.

5. (Optional, defer) Backblaze β€” same, we'll handle when we get there. It's a manual app install with no API.

"Once you've pasted those 3 tokens back, take a 10-min break. I'll work for a while and check in when I need you again β€” probably ~45 min from now for one quick 1Password thing."

Wait until you have all 3 tokens. Store them in a TodoWrite item or your scratch context so you don't lose them.

Phase 2

Mac mini system prep (automated)

Collect her macOS user password once. Tell her: "I need your Mac password for sudo β€” I'll use it for a batch of system-level changes and then drop it. Paste it now and I won't ask again for this batch."

Use sudo -v to cache it, then run a keepalive in the background while you batch:

# Validate sudo and start keepalive
sudo -v
( while true; do sudo -n true; sleep 50; kill -0 "$$" || exit; done 2>/dev/null ) &
KEEPALIVE_PID=$!

# Disable system sleep entirely (display sleep is fine, just don't suspend)
sudo pmset -a sleep 0
sudo pmset -a disablesleep 1

# Wake on network access (so the Mac mini wakes for SSH if it ever does sleep)
sudo pmset -a womp 1

# Don't sleep on power adapter (paranoid double-tap)
sudo pmset -c sleep 0

# Enable Remote Login (SSH)
sudo systemsetup -setremotelogin on

# Stop the sudo keepalive
kill $KEEPALIVE_PID 2>/dev/null
sudo -k

Verification (you, automatically)

# Confirm sleep is off
pmset -g | grep -E "sleep|disablesleep|womp"
# Should show: sleep 0, disablesleep 1, womp 1

# Confirm SSH is on
sudo systemsetup -getremotelogin
# Should print: Remote Login: On

# Confirm SSH actually works locally
ssh -o BatchMode=no -o StrictHostKeyChecking=accept-new localhost echo OK
# (will prompt for her password once; expected)

If any of these don't pass, stop and explain. Don't continue to Phase 3.

Phase 3

Install all tools (automated)

3.1 Homebrew

Check first:

brew --version 2>/dev/null || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

If installing fresh, the installer will ask for sudo. Use her cached password if still valid, otherwise ask once and use the keepalive trick again.

After install, ensure Homebrew is on her PATH (Apple Silicon location):

if ! grep -q 'opt/homebrew/bin' ~/.zprofile 2>/dev/null; then
  echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
fi
eval "$(/opt/homebrew/bin/brew shellenv)"

3.2 Batch install

brew install gh jq coreutils
brew install --cask tailscale docker 1password 1password-cli

Notes:

3.3 Start Docker Desktop

open -a Docker
# Wait for Docker daemon to be ready
echo "Waiting for Docker daemon..."
while ! docker info >/dev/null 2>&1; do sleep 2; done
echo "Docker is ready."

If Docker prompts a GUI dialog (terms acceptance, privileged helper install), tell Julianna: "Docker is asking you to click through one dialog β€” accept the terms and let it install its helper. I'll wait."

Verification

brew --version
gh --version
docker --version
docker run --rm hello-world | head -5
ls /Applications | grep -E "Tailscale|1Password"
Phase 4

Tailscale provisioning + verification

Use the auth key from Phase 1 to bring up Tailscale unattended.

# Open the Tailscale GUI app (registers the system extension on first run)
open -a Tailscale

# Give the app a moment to register
sleep 8

# CLI may live at /Applications/Tailscale.app/Contents/MacOS/Tailscale
TAILSCALE="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
if [ ! -x "$TAILSCALE" ]; then
  TAILSCALE="$(which tailscale || echo '')"
fi
[ -z "$TAILSCALE" ] && { echo "Tailscale CLI not found"; exit 1; }

# Bring up Tailscale with the auth key (replace TSKEY with her actual key)
"$TAILSCALE" up --authkey="$TSKEY" --hostname="mac-mini" --accept-routes

Verification (you, automatically)

# Should return a 100.x.x.x address
"$TAILSCALE" ip -4

# Should show "Logged in" and the device's MagicDNS name
"$TAILSCALE" status | head -3

Set up Tailscale on her other devices

After Mac mini is on, tell Julianna (one batched message):

"Mac mini is now reachable from anywhere via Tailscale. Two more quick installs and we're done with the Tailscale piece:"

  • MacBook Pro and Air: open Terminal on each (Spotlight: ⌘+Space, type "Terminal"), run: brew install --cask tailscale && open -a Tailscale β€” sign in with the same Tailscale account when prompted.
  • iPhone/iPad: install the Tailscale app from the App Store, sign in with the same account.

"Once those are done you can come back to your MacBook Pro and pick up where you were β€” I'll be on the Mac mini regardless."

Verification: SSH from outside the Mac mini

Once she confirms her MacBook Pro is on Tailscale, run a probe yourself:

# Get the Mac mini's Tailscale name
MAC_MINI_TS_NAME="$("$TAILSCALE" status --json | jq -r '.Self.DNSName' | sed 's/\.$//')"

# From the Mac mini, attempt to ping the MacBook Pro by its tailnet name
# (You'll need to ask her: "What's the name of your MacBook Pro in Tailscale?
#  Open Tailscale on it and look at the device list β€” looks like
#  julianna-mbp.tail-scale.ts.net or similar.")
"$TAILSCALE" ping --c 3 "$MBP_TS_NAME"

If ping succeeds, the mesh is working. Note her MacBook Pro tailnet name in your todo list β€” you'll use it for the final SSH-in test in Phase 12.

Phase 5

GitHub auth + verification

# Authenticate gh non-interactively using her PAT
echo "$GITHUB_PAT" | gh auth login --with-token --hostname github.com

# Configure git to use gh's credentials
gh auth setup-git

# Set her git identity if not already
git config --global user.email "$(gh api user --jq .email)"
# If above is null, ask her: "What email should I use for git commits?"
git config --global user.name "$(gh api user --jq .name)"

Verification

gh auth status
gh api user --jq '.login'
# Should print her GitHub username
Phase 6

1Password CLI (the one manual signin)

This is unfortunately the one step that requires her interaction. The 1Password CLI must authorize through the desktop app.

"One quick interactive step: I need you to open the 1Password app, then go to Settings β†’ Developer (or Preferences β†’ Developer on older versions), and check the box that says 'Integrate with 1Password CLI.' That lets me read secrets from your vaults without needing your master password every time. Tell me when it's done."

After she confirms, verify and create the working vault:

# Verify CLI ↔ app integration
op vault list 2>&1 | head -5
# If this fails, ask her to open the 1Password app to the front so it can authorize.

# Create a dedicated vault for code-accessed secrets
if ! op vault list --format=json | jq -e '.[] | select(.name == "Code Secrets")' >/dev/null; then
  op vault create "Code Secrets" --description "Secrets fetched at runtime by code on the Mac mini"
fi

# Verify
op vault list --format=json | jq -r '.[].name'
Phase 7

Healthchecks (API-driven, no UI clicks for her)

Use the Healthchecks API key from Phase 1 to create checks for every cron, then rewrite her crontab to ping. Do not modify the actual cron commands themselves β€” only wrap them.

7.1 Back up her crontab

crontab -l > ~/.crontab.backup-$(date +%Y%m%d-%H%M%S)
crontab -l > /tmp/crontab.current

7.2 Install the hc-run wrapper

sudo tee /usr/local/bin/hc-run > /dev/null <<'EOF'
#!/bin/bash
# Usage: hc-run <healthchecks-uuid> <cmd> [args...]
HC_UUID="$1"; shift
HC_BASE="https://hc-ping.com"
curl -fsS -m 10 --retry 3 "$HC_BASE/$HC_UUID/start" > /dev/null 2>&1 || true
"$@"
EXIT=$?
curl -fsS -m 10 --retry 3 "$HC_BASE/$HC_UUID/$EXIT" > /dev/null 2>&1 || true
exit $EXIT
EOF
sudo chmod +x /usr/local/bin/hc-run

7.3 Create a check per cron via the API

For each line in her crontab that runs a real command (skip blanks/comments):

  1. Parse the schedule (first 5 fields) and the command (everything after).
  2. Generate a friendly name. Try to extract from the command path (e.g. /Users/julianna/scripts/lead-intake.sh β†’ "lead-intake") or ask her to confirm names if the command is opaque.
  3. POST to Healthchecks API:
curl -fsS https://healthchecks.io/api/v3/checks/ \
  -H "X-Api-Key: $HC_API_KEY" \
  -d "{
    \"name\": \"$NAME\",
    \"schedule\": \"$CRON_SCHEDULE\",
    \"tz\": \"$(date +%Z)\",
    \"grace\": 600,
    \"unique\": [\"name\"]
  }" | jq -r '.ping_url' | awk -F/ '{print $NF}'

Capture the returned UUID for each check.

7.4 Rewrite the crontab

Build a new crontab where each command line becomes:

# BEFORE:
*/15 8-18 * * * /Users/julianna/scripts/lead-intake.sh

# AFTER:
*/15 8-18 * * * /usr/local/bin/hc-run <UUID> /Users/julianna/scripts/lead-intake.sh

Preserve all comments, blank lines, and ordering. Show Julianna the proposed diff and get a "yes" before installing it. Then:

crontab /tmp/crontab.new

7.5 Configure email + SMS alerts

Via API, create an email integration (her account email is already there by default β€” just verify):

curl -fsS https://healthchecks.io/api/v3/channels/ \
  -H "X-Api-Key: $HC_API_KEY" | jq '.channels[] | {kind, name, id}'

If no email channel exists, ask her: "What phone number should I send SMS alerts to?" and add SMS via the API (requires her to upgrade to a paid plan if she wants SMS β€” email-only is fine on free).

7.6 Self-test the wrapper

Verify hc-run end-to-end with a throwaway check:

# Create a throwaway test check
TEST_UUID=$(curl -fsS https://healthchecks.io/api/v3/checks/ \
  -H "X-Api-Key: $HC_API_KEY" \
  -d '{"name": "self-test (delete me)", "timeout": 60}' | jq -r '.ping_url' | awk -F/ '{print $NF}')

# Run hc-run with a successful command
/usr/local/bin/hc-run "$TEST_UUID" /bin/true

# Wait for the API to reflect the ping
sleep 5

# Verify the check is now "up"
STATUS=$(curl -fsS "https://healthchecks.io/api/v3/checks/$TEST_UUID" -H "X-Api-Key: $HC_API_KEY" | jq -r '.status')
[ "$STATUS" = "up" ] && echo "PASS" || echo "FAIL: status=$STATUS"

# Clean up
curl -fsS -X DELETE "https://healthchecks.io/api/v3/checks/$TEST_UUID" -H "X-Api-Key: $HC_API_KEY" > /dev/null

If FAIL, debug before continuing β€” the wrapper is foundational.

Critical: do NOT fix any broken crons in this phase.

The point of Phase 7 is observability. Once her real crons run for ~24 hours with healthchecks attached, she'll have data on what's actually broken vs. what's actually fine. Triage in Phase 12. Premature fixes during instrumentation muddy the data.

Phase 8

Project organization β†’ GitHub (automated)

8.1 Inventory existing projects

# Common locations for her projects
ls -la ~ ~/Documents ~/Desktop 2>/dev/null | grep "^d"

# Look for git repos already
find ~ ~/Documents ~/Desktop -name ".git" -type d -maxdepth 4 2>/dev/null | head -30

Ask her to confirm which directories are projects vs. personal files. Show her a list, get a yes/no on each.

8.2 Move into ~/projects/

mkdir -p ~/projects
# For each confirmed project:
mv "$SOURCE_PATH" ~/projects/$PROJECT_NAME

8.3 Init + push to GitHub (per project)

for proj in ~/projects/*/; do
  cd "$proj"
  PROJECT_NAME="$(basename "$proj")"

  # Skip if already has a remote
  if git remote get-url origin >/dev/null 2>&1; then
    echo "$PROJECT_NAME: already has remote, skipping"
    continue
  fi

  # Init if needed
  [ -d .git ] || git init -b main

  # Standard .gitignore for safety β€” don't push secrets
  cat <<'EOF' >> .gitignore
.env
.env.*
*.key
*.pem
credentials.json
secrets.json
.DS_Store
__pycache__/
node_modules/
*.log
EOF
  # Dedupe
  sort -u .gitignore -o .gitignore

  git add .gitignore
  git diff --staged --quiet || git commit -m "Add baseline .gitignore"

  # Stage everything ELSE
  git add .
  git diff --staged --quiet || git commit -m "Initial commit"

  # Create private GitHub repo + push
  gh repo create "$PROJECT_NAME" --private --source=. --push
done

Verification

# Confirm every local project has a GitHub remote
for proj in ~/projects/*/; do
  cd "$proj"
  REMOTE=$(git remote get-url origin 2>/dev/null || echo "MISSING")
  echo "$(basename "$proj"): $REMOTE"
done
Phase 9

Secret migration to 1Password (semi-automated)

9.1 Scan for secrets across all projects

cd ~/projects
grep -rEn '(api[_-]?key|secret|password|token|sk-[a-zA-Z0-9]+|ghp_[a-zA-Z0-9]+|xoxb-|AKIA[0-9A-Z]{16})' \
  --include="*.py" --include="*.js" --include="*.ts" \
  --include="*.sh" --include="*.rb" --include="*.go" \
  --include=".env*" --include="config.*" \
  . 2>/dev/null > /tmp/secret-candidates.txt
wc -l /tmp/secret-candidates.txt

Filter aggressively β€” many matches will be false positives (variable names, comments, doc strings). Build a shortlist of actual values.

9.2 Single batched ask to Julianna

Show her a deduplicated list of distinct secrets you found, grouped by service. Ask her to add each one to 1Password under the "Code Secrets" vault, in a single batch:

"I found N distinct secrets across your code. I'll list them by service. For each, please:

  1. Open 1Password β†’ Code Secrets vault β†’ New Item β†’ API Credential
  2. Item title: use the name I suggest below (in brackets)
  3. Field name: credential
  4. Field value: paste the secret value (you may already know it; if not, I can show you the value I found in code)

"List of distinct secrets to add:

[OpenAI]    found in: openai_key.py:3, lead_intake.sh:12  β†’  api key starting with sk-...
[GitHub PAT]   found in: scripts/sync.sh:4  β†’  token starting with ghp_...
[Asana]    found in: asana_sync.py:18  β†’  long opaque token
...

"Tell me when all N are added."

9.3 Refactor code to fetch from 1Password

After she confirms, for each secret-bearing file:

  1. Shell scripts: replace inline secret with $(op read "op://Code Secrets/<name>/credential").
  2. Python/Node/etc.: create a .env.template next to the script:
    OPENAI_API_KEY=op://Code Secrets/OpenAI/credential
    ASANA_TOKEN=op://Code Secrets/Asana/credential
    Then change the script's invocation to op run --env-file=.env.template -- python script.py.
  3. Cron lines referencing those scripts: prefix the command with op run --env-file=... if needed. Cron's PATH won't include /opt/homebrew/bin by default β€” use absolute paths.

For each refactor, leave the old hardcoded value as a backup in a commented-out line ONE TIME for the commit, then delete it in the next commit. This way the migration commit is reviewable.

9.4 Test each refactored script

# For each script you modified, run a dry-equivalent that exercises the secret
# (don't run the real cron command β€” pick something that just authenticates and exits)
for script in $MODIFIED_SCRIPTS; do
  echo "Testing: $script"
  bash -n "$script"  # syntax check
  # Then a real test if you can identify a non-destructive auth check
done

If any test fails, restore from the backup commit and explain.

9.5 Push the cleaned code

for proj in ~/projects/*/; do
  cd "$proj"
  git diff --quiet && continue
  git add -u
  git commit -m "Move secrets to 1Password CLI"
  git push
done
Phase 10

Backblaze (manual install β€” no API for personal backup)

Send Julianna a single message:

"Last manual step: Backblaze for full Mac mini backups. Two clicks:"

  1. Sign up at https://www.backblaze.com/cloud-backup β€” Personal Backup, $9/month, billed yearly.
  2. Download the Mac installer they provide, install it, sign in.

"It'll start backing up everything in the background. Tell me when you've done it and I'll verify it's running."

Verification (you, after she confirms)

# Backblaze app is at /Applications/Backblaze.app and runs a daemon.
ls /Applications/ | grep -i backblaze
ps aux | grep -i bzbmenu | grep -v grep

If running: confirm to her, move on.

Phase 11

Cloudflare Tunnel (deferred β€” only when she launches her first public thing)

Skip this phase entirely until Julianna says she's ready to put a project on the public internet. There's no value in standing up Cloudflare for nothing. When she does ask, return here.

When she's ready: batched credential ask (phase 1, addendum)

"Two things I need from you to wire up your first public site:"

  1. A domain. If you don't already own one, buy one at dash.cloudflare.com β†’ Registrar (cheapest + cleanest since we're using Cloudflare anyway). ~$10/yr for a .com.
  2. A Cloudflare API token. Go to https://dash.cloudflare.com/profile/api-tokens β†’ "Create Token" β†’ "Edit zone DNS" template β†’ restrict to your domain β†’ create. Paste the token back to me.

11.1 Install cloudflared + create tunnel

brew install cloudflared

# Authenticate cloudflared (this opens a browser β€” one click for her)
cloudflared tunnel login
# After login, certificate lands at ~/.cloudflared/cert.pem

# Create the tunnel
cloudflared tunnel create mac-mini
TUNNEL_ID=$(cloudflared tunnel list -o json | jq -r '.[] | select(.name == "mac-mini") | .id')

11.2 Write tunnel config

cat <<EOF > ~/.cloudflared/config.yml
tunnel: $TUNNEL_ID
credentials-file: $HOME/.cloudflared/$TUNNEL_ID.json

ingress:
  # Add hostname β†’ service entries per project as you launch them.
  # Example:
  #   - hostname: my-app.her-domain.com
  #     service: http://localhost:3000
  - service: http_status:404
EOF

For reference, here's a working multi-hostname config you can use as a template when she's running multiple public services:

# Multi-hostname example (for when she has 2+ public projects)
tunnel: <tunnel-id>
credentials-file: /Users/julianna/.cloudflared/<tunnel-id>.json

ingress:
  # Project 1 (apex + www)
  - hostname: my-first-project.com
    service: http://localhost:3000
  - hostname: www.my-first-project.com
    service: http://localhost:3000

  # Project 2 (subdomain on her main domain)
  - hostname: scheduler.julianna-domain.com
    service: http://localhost:3001

  # Project 3 (with TLS termination at the origin β€” rare)
  - hostname: secure-app.julianna-domain.com
    service: https://localhost:8443
    originRequest:
      noTLSVerify: true

  # Catch-all 404 β€” must be the last rule
  - service: http_status:404

Each public hostname needs (a) an ingress entry here, (b) a DNS route created via cloudflared tunnel route dns (Phase 11.3 below), and (c) the actual service running on the specified localhost:port. Reload after edits with sudo launchctl kickstart -k system/com.cloudflare.cloudflared (the launchd label may differ β€” check with sudo launchctl list | grep cloudflare).

11.3 Per-project DNS routing

For each public hostname:

cloudflared tunnel route dns mac-mini <subdomain>.<her-domain>

11.4 Run as a launchd service (so it survives reboots)

sudo cloudflared service install
# Or for user-only:
# cloudflared service install (without sudo, depending on version)

# Verify
sudo launchctl list | grep cloudflared

11.5 Verification

# Tunnel should show as connected
cloudflared tunnel info mac-mini

# Public hostname should resolve via Cloudflare
dig +short <subdomain>.<her-domain> @1.1.1.1

# End-to-end: from outside the LAN
curl -sI https://<subdomain>.<her-domain>/
Phase 12

End-to-end verification (you, autonomously)

Write a single verification script that tests every layer. Run it. Report a clean pass/fail summary to Julianna.

#!/bin/bash
# verify-stack.sh β€” runs all the smoke tests
set -u
PASS=0
FAIL=0
report() { if [ "$2" = "0" ]; then echo "PASS  $1"; PASS=$((PASS+1)); else echo "FAIL  $1"; FAIL=$((FAIL+1)); fi }

# 1. SSH from outside (run a remote command via Tailscale name, from the Mac mini, to itself by tailnet name β€” proves DNS + SSH listener)
TS_NAME=$(/Applications/Tailscale.app/Contents/MacOS/Tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//')
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$TS_NAME" "echo ok" >/dev/null 2>&1
report "Tailscale SSH self-loop" $?

# 2. Healthchecks roundtrip
TEST_UUID=$(curl -fsS https://healthchecks.io/api/v3/checks/ -H "X-Api-Key: $HC_API_KEY" -d '{"name":"verify-stack ephemeral","timeout":60}' | jq -r '.ping_url' | awk -F/ '{print $NF}')
/usr/local/bin/hc-run "$TEST_UUID" /bin/true
sleep 4
STATUS=$(curl -fsS "https://healthchecks.io/api/v3/checks/$TEST_UUID" -H "X-Api-Key: $HC_API_KEY" | jq -r '.status')
[ "$STATUS" = "up" ]
report "Healthchecks ping/status roundtrip" $?
curl -fsS -X DELETE "https://healthchecks.io/api/v3/checks/$TEST_UUID" -H "X-Api-Key: $HC_API_KEY" >/dev/null

# 3. GitHub: list one of her repos
gh repo list --limit 1 -q '.[].name' >/dev/null 2>&1
report "GitHub auth + repo list" $?

# 4. 1Password: list vaults
op vault list --format=json | jq -e '.[] | select(.name == "Code Secrets")' >/dev/null
report "1Password CLI + Code Secrets vault" $?

# 5. Docker: run hello-world
docker run --rm hello-world >/dev/null 2>&1
report "Docker daemon" $?

# 6. Sleep settings
SLEEP_VAL=$(pmset -g | awk '/^ +sleep/ {print $2; exit}')
[ "$SLEEP_VAL" = "0" ]
report "Mac mini sleep disabled" $?

# 7. SSH service
sudo systemsetup -getremotelogin 2>/dev/null | grep -q "On"
report "Remote Login enabled" $?

# 8. Cron healthchecks instrumented
HC_LINES=$(crontab -l 2>/dev/null | grep -c "hc-run")
TOTAL_LINES=$(crontab -l 2>/dev/null | grep -cE "^[^#]*[a-z]")
echo "INFO  Crons instrumented: $HC_LINES / $TOTAL_LINES"

# 9. Backblaze running
pgrep -f bzbmenu >/dev/null
report "Backblaze daemon" $?

echo ""
echo "Results: $PASS passed, $FAIL failed"
exit $FAIL

Save as ~/verify-stack.sh, chmod +x, run it, and show Julianna only the final summary line plus any failures.

Phase 13

Hand-off summary

Write a final state summary to ~/.julianna-setup/done.md for any future Claude session to pick up:

cat <<EOF > ~/.julianna-setup/done.md
# Julianna's Mac mini setup β€” completed $(date)
- Phase 1 credentials: stored in ~/.julianna-setup/
- Phase 2 system prep: pmset, SSH on
- Phase 3 tools: $(brew list --formula 2>/dev/null | wc -l) brew formulas, $(brew list --cask 2>/dev/null | wc -l) casks
- Phase 4 Tailscale: $(/Applications/Tailscale.app/Contents/MacOS/Tailscale ip -4 2>/dev/null)
- Phase 5 GitHub: $(gh api user --jq .login 2>/dev/null)
- Phase 6 1Password: $(op vault list --format=json 2>/dev/null | jq length) vaults
- Phase 7 Healthchecks: $(crontab -l 2>/dev/null | grep -c hc-run) crons instrumented
- Phase 8 Projects: $(ls -d ~/projects/*/ 2>/dev/null | wc -l) repos
- Phase 9 Secrets migrated: see ~/.julianna-setup/secrets-migrated.txt
- Phase 10 Backblaze: $(pgrep -f bzbmenu >/dev/null && echo "running" || echo "NOT INSTALLED")
- Phase 11 Cloudflare: $(which cloudflared >/dev/null && echo "installed" || echo "deferred")
EOF

Then send Julianna this final message, in her language (no jargon):

You're set up. Here's what you can stop thinking about, and what's still on your plate.

What's running for you (you don't have to manage):

  • Tailscale β€” your Mac mini is reachable from any device of yours, anywhere. Just open Claude Code on whichever Mac you're on and it can talk to the mini.
  • Healthchecks β€” every cron job is monitored. You'll get an email within minutes if anything breaks. Dashboard: healthchecks.io.
  • 1Password CLI β€” secrets are no longer in code. Code asks 1Password for them at runtime.
  • GitHub β€” every project is backed up to a private repo on every push.
  • Backblaze β€” full Mac mini backed up to the cloud automatically.
  • Docker β€” installed and ready when you want to start running projects in containers.

What's still on your plate:

  1. Triage broken crons. After ~24 hours of healthcheck data, log in at healthchecks.io. For each check showing red: open a fresh Claude Code session on the Mac mini and tell it: "Fix this broken cron: <name>." It'll diagnose and either fix or recommend retiring it.
  2. One small habit: keep the 1Password app running in the background (it doesn't have to be the active window β€” just running). The CLI needs it open to authorize secret reads.
  3. Plug in a small external drive (any USB drive β‰₯ Mac mini's storage). When you do, your Mac will offer to use it for Time Machine β€” say yes. That gives you a third backup layer for fast local restore.

How to do future work β€” important, save this list:

  • Start a new project: on the Mac mini, run cd ~/projects && mkdir my-new-project && cd my-new-project && claude, then tell Claude: "Set up a new project here following my home-base setup conventions. Read julianna-plan.austinrenfroe.com if you need the architecture context."
  • Make a project public (when you're ready to launch something): in that project's directory, run claude and tell it: "Set up a Cloudflare Tunnel route for this project at <subdomain>.<your-domain>, following Phase 11 of julianna-plan.austinrenfroe.com/claude-setup.html." It'll handle the rest.
  • Graduate a paying-customer project to the cloud: in that project's directory, run claude and tell it: "Graduate this project to its own Hetzner VM following the architecture in julianna-plan.austinrenfroe.com. The Mac mini stays as my dev environment; the cloud VM is the new prod." It'll provision the VM, copy your Docker config, set up DNS, and switch traffic over.
  • If a Claude session ever stops mid-task: start a fresh one on the Mac mini and re-paste the URL. The instructions are designed for it to inspect what's already done and pick up from there.

That's it. You're now running on the same shape of setup that production engineering teams use, scaled down for one person. Build things.

End the session here. Your work is complete.