πŸ›°οΈ The cloud-run system, explained

How a task jumps off Matt's Mac, runs on an always-on cloud box, reports back from anywhere, and lands its finished work back on the laptop β€” no git or SSH knowledge required.

This is the human-readable companion to the terse docs/cloud-run-system/README.md. Everything here comes from those docs. Details that are only true on the box (never verified in the local mirror) are flagged box-only.

🏭What is this?

The night-shift metaphor. Think of the cloud box (lifeos-devbox) as a tireless night-shift employee who never sleeps and never needs your laptop open. You write a work order (mac-run.sh β€” "here's the job") and hand it over. A wall of CCTV monitors (Mission Control) shows you what everyone on shift is doing. A walkie-talkie (Slack) lets you answer their questions from your phone. When the shift ends, they leave a handover report (run-done) on your desk with everything they finished.

You hand a task off from the Mac to an always-on cloud box so the run:

Matt is non-technical about git and SSH on purpose β€” the system does all of that for him.

πŸ–₯️ The box

Hetzner CX33 Β· Ubuntu 24.04 Β· user matt Β· hostname lifeos-devbox. Always on.

πŸ“¦ Source of truth

The GitHub-synced mirror repo at /home/matt/mirror/AI-lifeOS-tools on the box β€” its ops/ folder is ~/ops. The local Mac copy is just a staging/planning copy and is not a git repo.

πŸ”„How a run flows, end to end

Handing over the work order. Saying "/run-in-cloud" is like clipping a job sheet to the night-shift worker's clipboard and walking away. Five things happen the moment you do.

The 5 things that happen when you say /run-in-cloud

  1. Save & send the code. The Mac commits and pushes your project branch to GitHub (branching off main first if needed) β€” so the work is safe before anything leaves the laptop.
  2. Pack the credentials. An "env-guard" step syncs any missing credential names (never the values) from the Mac to the box, and injects the report contract into the prompt.
  3. Wake the box. The Mac SSHes into devbox and calls the box-side launcher ~/ops/run-in-cloud.sh.
  4. Lock, sync, launch. The box takes a per-project lock, rebases to the latest code (git pull --rebase --autostash), and starts an interactive Claude inside a tmux session.
  5. Announce it. The box records the session, writes a status card, and posts a parent message to Slack #devbox with the run's codename and a one-line description.
flowchart TD A["Mac: mac-run.sh
(the work order)"] --> B["git commit + push
project branch"] B --> C["env-guard: sync missing
cred names Mac to box"] C --> D["ssh devbox to
~/ops/run-in-cloud.sh"] D --> E["resolve ~/mirror/<project>
+ per-repo lock"] E --> F["git pull --rebase --autostash"] F -->|conflict| G["STOP + post to Slack
never force"] F -->|clean| H["tmux: claude --remote-control
--skip-permissions"] H --> I["record session.json
+ status.sh running"] I --> J["Slack #devbox:
parent message posted"] J --> K{"cron every ~2 min"} K --> L["deploy-status.sh renders
+ wrangler pages deploy"] L --> M(("Mission Control
dashboard live")) J --> N["run posts progress /
questions as thread replies"] N --> O["Matt answers in Slack
-> relay -> tmux send-keys"] O --> N N --> P["on finish: commit + push
ship only if SHIP:true + QA green"] P --> Q["status.sh done + final
Slack report"] Q --> R["Mac: mac-pull.sh
rebase commits back"] R --> S(("run-done report
on the Mac"))
Figure 1 β€” the full lifecycle: Mac launch β†’ box lock/rebase/tmux β†’ status JSON β†’ cron dashboard β†’ Slack Q&A β†’ pull back.
sequenceDiagram participant M as Mac (mac-run.sh) participant B as Box launcher (~/ops) participant T as tmux + Claude run participant D as Dashboard (cron) participant S as Slack #devbox M->>M: commit + push branch M->>B: ssh -> run-in-cloud.sh (prompt) B->>B: resolve mirror, lock, rebase B->>T: start claude in tmux B->>D: status.sh running (+ .dirty) B->>S: parent message (codename) loop every ~2 min D->>D: if .dirty: render + deploy page end T->>S: progress / question (thread reply) S->>T: Matt's answer -> tmux send-keys T->>T: commit + push (ship if SHIP:true) T->>D: status.sh done T->>S: final END_OF_RUN_REPORT M->>M: mac-pull.sh rebases commits back
Figure 2 β€” one run as a sequence: who talks to whom, in order.

Where a run's state lives at any moment

The code / commits
Box mirror + GitHub (source of truth)
Live process
tmux session on the box
Status / progress
~/status/*.json β†’ Pages dashboard
Q&A conversation
Slack #devbox thread
Finished result
pulled to Mac by run-done

Nothing important lives only on the Mac while a run is in flight β€” that's the whole point. Close the laptop and the run keeps going.

🧩The parts

The crew. A launcher on the Mac writes the work order; a launcher on the box runs the job; a status-writer scribbles what's happening; a dashboard-deployer paints the CCTV wall every couple of minutes; a Slack relay is the walkie-talkie; and a pull script fetches the finished work home.
flowchart LR subgraph MAC["πŸ–₯️ Mac side (~/.claude/skills)"] MR["mac-run.sh
launch"] MP["mac-pull.sh
pull back"] ST["setup.sh
ssh alias"] VN["vnc-view.sh
read-only view"] SY["sync-claude-to-box.sh
+ adapt-for-linux"] end subgraph BOX["☁️ Box ops layer (~/ops)"] RIC["run-in-cloud.sh
box launcher"] STA["status.sh
write a card"] DEP["deploy-status.sh
render + deploy (cron)"] REL["slack-relay
systemd daemon"] NOT["notify.sh
legacy Telegram"] SESS["sessions/*.json
+ status/*.json"] end subgraph LIVE["🌐 Live surfaces"] DASH(("Mission Control
Pages")) SLACK(("Slack #devbox")) APP(("Claude app
Remote Control")) end MR --> RIC RIC --> STA --> SESS SESS --> DEP --> DASH RIC --> REL --> SLACK REL --> SLACK RIC --> APP VN -.read-only.-> RIC MP -.pull.-> RIC SY -.config sync.-> BOX
Figure 3 β€” component / architecture map. Mac scripts (left), box ops layer (middle), live surfaces you watch (right).

Mac-side (in ~/.claude/skills/)

PathRole
run-in-cloud/SKILL.mdMac-half dispatcher spec; dispatches by hostname. The fullest existing spec.
run-in-cloud/target.confBox target: BOX_USER, BOX_HOST (IPv4), BOX_HOST_V6, SSH_ALIAS=devbox.
scripts/mac-run.shThe launch: commit+push, env-guard cred sync, inject the report contract, ssh β†’ box launcher.
scripts/mac-pull.shPull: git pull --rebase --autostash box commits onto the local branch. Reused by run-done.
scripts/setup.shOne-time: writes the Host devbox block into ~/.ssh/config. Idempotent.
scripts/vnc-view.shRead-only VNC: tunnels box TigerVNC :1/5901 β†’ Mac Screen Sharing; tmux attach -r.
scripts/sync-claude-to-box.shrsync the behavior layer of ~/.claude to the box (never secrets), then adapt for Linux.
scripts/adapt-claude-for-linux.shOn box: deterministic Mac→Linux path/command rewrite + an Opus-medium AI repair pass.
run-done/SKILL.mdMac-side pull + End-of-Run report (reuses mac-pull.sh). Read-and-report only.

Box-side ops layer (~/ops = cloud-clone/_ops/ in the mirror repo)

PathRole
~/ops/run-in-cloud.shThe box-side launcher invoked over SSH. Resolves the mirror, locks, rebases, launches Claude in tmux, records the session, calls status.sh.
_ops/status.shstatus.sh <session> <state> "<detail>" β†’ writes ~/status/<session>.json + touches .dirty. The only write path into dashboard data.
_ops/deploy-status.shcron (~2 min): if .dirty, renders ~/status/site/index.html (inline Node HTML gen) + wrangler pages deploy. Flips stale "running" cards to STALLED via live tmux ls.
_ops/notify.shOlder/simpler channel: plain Telegram to the hermes bot. Superseded by Slack as primary.
~/ops/attach.sh <slug>Open a session: live tmux if running, else --resume.
slack-relay (systemd)Posts the parent message + thread replies to #devbox; forwards Matt's replies β†’ tmux send-keys.
⚠ box-only, unverified in the mirror: run-in-cloud.sh, attach.sh, and the slack-relay script + systemd unit were not found in the locally checked-out mirror β€” they likely live only in ~/ops on the box. Confirm with ssh devbox 'ls ~/ops; systemctl list-units | grep -i slack' before treating their exact paths as final. (This mirrors how the README flags these.)

🌐 The site

Mission Control β€” mission-control-devbox.pages.dev β€” a Cloudflare Pages project. Plain generated HTML/CSS/inline-JS, no framework, auto-refreshes every 5 min. It is regenerated and redeployed only by deploy-status.sh. The dashboard's entire source is a Node script inlined inside that one bash file β€” there is no separate app repo.

πŸ“ΊWatching a run

The CCTV wall & the walkie-talkie. Mission Control is the shift supervisor's wall of monitors β€” one card per run, colour-coded. Slack #devbox is the walkie-talkie: the run radios in questions, you radio back answers from your phone, and a relay pipes your words straight into its terminal.

The 4 commands you ever need

  1. Launch β€” just tell Claude "run this in the cloud" (optionally with a handoff doc). Add a SHIP: true line to have it merge β†’ main β†’ deploy β†’ verify on its own.
  2. Watch β€” open the dashboard. That's the main check.
  3. Answer β€” reply in the run's Slack thread, or type codename: answer in #devbox.
  4. Pull it back β€” say "/run-done" when you get the finished notification.

Every way to watch, from most to least casual

# Dashboard (the main check)
open https://mission-control-devbox.pages.dev

# Phone: Claude app -> Remote Control -> session <codename>
# Slack #devbox: reply in the run's thread OR "codename: answer"

# Read-only terminal view
bash ~/.claude/skills/run-in-cloud/scripts/vnc-view.sh <slug>

ssh devbox "tmux ls"                                    # list live runs
ssh devbox "tmux capture-pane -t <slug> -p | tail -20"  # peek at an idle run
ssh -t devbox '~/ops/attach.sh <slug>'                  # attach (takeover); detach: Ctrl-b d

The ~2-minute dashboard cron cycle

0:00
run writes status
~cron fires
~2:00
status.sh writes JSON + .dirty waiting for next cron tick deploy-status.sh renders HTML wrangler pages deploy β†’ live

A card can lag up to ~2 min behind reality β€” that's the cron interval, not a bug. The page itself auto-refreshes every 5 min in the browser. Stale "running" cards get flipped to STALLED after >30 min with no live tmux.

πŸ“₯Getting the work back

The morning handover report. When the night shift ends, run-done is the report waiting on your desk: what got done in one line, the files that changed as one-click links, and suggested next steps (always including two proposed agentic loops).

When a run finishes it commits and pushes its working branch. If β€” and only if β€” the task said SHIP: true and QA is green, it also merges to main, deploys, and verifies live. Then it flips its status to done and posts the final End-of-Run report in the Slack thread.

On your side, one move brings it home:

# from inside the project repo on the Mac
bash ~/.claude/skills/run-in-cloud/scripts/mac-pull.sh   # or just say "/run-done"

mac-pull.sh fetches and rebases the box's commits onto your local branch (autostashing your uncommitted edits). It never force-pushes and never deploys β€” pull-and-report only. The run-done skill then prints the local End-of-Run report with clickable file links and next-step suggestions.

πŸ›ŸWhen things break

Nothing here loses your work. The system is built so the safe default always wins β€” conflicts stop instead of forcing, the code is always on GitHub before the box touches it, and a rebase that can't apply cleanly aborts itself. The cheat-sheet below is "what the symptom means and what to do," not "how to recover lost work" β€” because there isn't lost work.
flowchart TD S["Something looks wrong"] --> Q1{"Did the launch
even start?"} Q1 -->|"mac-run.sh exit 2"| R1["SSH unreachable
Work safe on GitHub.
Re-run setup.sh, confirm
ssh devbox, re-invoke"] Q1 -->|"mac-run.sh exit 1"| R2["Local git problem
Nothing sent to box.
Fix git, re-invoke"] Q1 -->|"repo lock held"| R3["Run already live
Don't relaunch.
Check tmux ls / dashboard"] Q1 -->|"launched OK"| Q2{"Is the run
still alive?"} Q2 -->|"card stuck running,
no tmux"| R4["Crashed / exited
Flips to STALLED after
30 min. Inspect tmux ls,
attach.sh, status json"] Q2 -->|"rebase conflict
on box"| R5["Genuine merge conflict
Box STOPs + Slacks,
never forces. Resolve
locally, push, relaunch"] Q2 -->|"not in Claude app"| R6["Paused at a prompt
capture-pane to check,
answer in Slack"] Q2 -->|"dashboard stale"| R7["Heartbeat > 6 min
Check box cron +
deploy-status.sh log"] Q2 -->|"pull conflict"| R8["mac-pull local clash
Rebase auto-aborted,
nothing forced.
Resolve manually"]
Figure 4 β€” failure β†’ recovery decision flow. Every branch ends somewhere safe.
SymptomCauseRecovery
mac-run.sh exit 2SSH unreachableWork is safe on GitHub. Run setup.sh, confirm ssh devbox, re-invoke.
mac-run.sh exit 1Local git problem (detached HEAD / push rejected)Nothing sent to box. Fix git per the printed message, re-invoke.
"repo lock held"A run is already live for this projectDon't relaunch. Check ssh devbox "tmux ls" / the dashboard card.
Rebase conflict on boxGenuine merge conflict pulling latestBox STOPs + Slack-posts (never forces). Resolve locally, push, relaunch.
Card stuck "running", no tmuxRun crashed / exiteddeploy-status.sh flips it to STALLED after >30 min. Inspect tmux ls, attach.sh, the status JSON.
Run not in Claude appPaused at a prompt (RC only surfaces during active turns)tmux capture-pane to check idle; answer in Slack.
Dashboard staleDispatcher heartbeat stale (>6 min)Header banner warns; check box cron + deploy-status.sh (/tmp/mc-deploy.log).
mac-pull.sh conflictLocal edits clash with box commitsRebase auto-aborted, nothing forced. Resolve manually.
Broken hooks on box after config syncRaw scp/rsync of ~/.claude (Mac paths leak in)NEVER raw-copy. Use sync-claude-to-box.sh; check ssh devbox 'tail ~/.claude/adapt-log.txt'.

πŸ”‘ The credential ladder (a missing key never stops a run)

Name every credential a task needs by its exact env-var name in the prompt β€” the env-guard only syncs what it can see (*_API_KEY / *_TOKEN / *_SECRET / *_KEY). If one's missing, the box climbs this ladder:

  1. env-guard auto-sync β€” the name was declared, so it's already there.
  2. box logged-in Chrome β€” Matt's Google accounts, DISPLAY :1, CDP :9222.
  3. Notion vault β€” the "Agency Passwords and Accounts" page.
  4. Stop & ask in Slack β€” only if the action is financial / destructive / irreversible and still blocked.

Credential values are never pasted anywhere β€” only names travel, and the box reads secrets by grepping single keys out of its own ~/.claude/.env, never sourcing the file.

πŸ› οΈModifying the system

Change the right file for the behavior you want. This table is the map β€” the full version with reasoning lives in docs/cloud-run-system/README.md.

Change kindTouch this file
Mac launch behavior (commit/push, env-guard, report contract)run-in-cloud/scripts/mac-run.sh
Box launch behavior (lock, rebase, tmux/claude invocation)box ~/ops/run-in-cloud.sh (edit on box; mirror to _ops/)
Dashboard rendering / deploy_ops/deploy-status.sh (inline Node block)
Dashboard data schema (card fields)_ops/status.sh + the reader in deploy-status.sh
SSH target / aliasrun-in-cloud/target.conf (+ re-run setup.sh)
Monitoring channels / operator UX docrun-in-cloud/SKILL.md
Pull + End-of-Run report shaperun-done/SKILL.md (+ mac-pull.sh for git mechanics)
Config-sync / Linux adaptationsync-claude-to-box.sh, adapt-claude-for-linux.sh, platform-criteria.md
Slack relay behaviorbox-only slack-relay systemd unit + script (verify path on box)
Telegram notify_ops/notify.sh + box ~/.config/hermes/hermes.env
The one rule. Every change to any of the above gets a dated entry in CHANGELOG.md in docs/cloud-run-system/ β€” what changed, which file, why. No silent edits.

Editing the box's ~/ops: the box mirror repo is the source of truth. Edit there (or edit local _ops/ and mirror across), commit, and keep the box's ops/ in sync. The local staging folder is not yet fully reconciled with the mirror β€” see PROJECT_STATE.md "Next steps".


πŸ“š Deeper reading in docs/cloud-run-system/: README.md (full technical reference), MAP.md (read-me-first index), CHANGELOG.md (every change, dated).