The Unattended Cloud Runner — Workflow & Setup¶
An unattended cloud runner is a scheduled Claude Code routine that becomes a disposable mship worker: when it fires it clones the workspace, implements one approved spec, and opens the PR(s) — all routed through the relay, which attaches and enforces the real GitHub credential at egress. The worker never holds a GitHub token; it carries only a low-value, short-lived per-run token. Nothing auto-merges — a human reviews and merges in the morning.
This doc is the operator entry point: the end-to-end workflow, plus the one-time go-live setup. It stitches together three deep-dive docs and one skill; each is linked at the point it becomes relevant, and their internals are not restated here.
cloud-worker-auth-spine.md— attach-at-relay: the credential egress proxy (trust model, enforcers, module boundary).cloud-agent-auth.md— the/gh-tokenbroker: GitHub auth for trusted cloud sessions, plus the GitHub App setup both auth models share.adapters/claude-routine-runner.md— the pull-API runner: a Claude routine as the unattended-run host (mship item run-nextselects + claims from a backlog).- skill
overnight-cloud-worker-routines— the agent-facing pattern for minting the token and standing up the routine.
Mental model: three planes¶
| Plane | Who | Responsibility |
|---|---|---|
| Control | mship |
Spec approval, WorkItem state, the finish gates (approved spec + passing tests + audit). Host-agnostic — mship never spawns an agent. |
| Execution | the Claude Code routine | The disposable worker: clone → implement one spec → open PR(s). Swappable for any scheduler that can run shell + invoke an agent. |
| Credential | the relay egress proxy | Attaches the repo-scoped GitHub App token at egress and enforces the run's scope (which repos, which push branch). The worker never sees it. |
The recommended path below is attach-at-relay (worker holds no credential), the model built for untrusted, prompt-injectable overnight workers. It is one point in a small decision space — see the next section before committing to a setup.
Choosing your setup¶
Two independent choices define a deployment. Pick one from each axis — any combination works.
Axis 1 — how the worker authenticates to GitHub:
| Model | Worker holds | Use when | Doc |
|---|---|---|---|
Raw env token (GH_TOKEN) |
a real GitHub token | trusted CI/container you fully control | pull-API runner prerequisites |
The /gh-token broker |
a serve bearer; pulls short-lived repo-scoped tokens on use | daytime/trusted runs; a machine with mship serve is awake |
cloud-agent-auth.md |
| Attach-at-relay (this runbook's default) | no GitHub credential — only a per-run relay token | untrusted, prompt-injectable overnight workers | cloud-worker-auth-spine.md |
Axis 2 — how work is selected:
| Model | You schedule | Use when | Doc |
|---|---|---|---|
| Per-spec push (this runbook's default) | one routine per named approved spec | you decide each night what runs | this doc, Per-run lifecycle |
| Pull-API backlog | one recurring tick; mship item run-next picks + claims |
you keep an unattended-flagged backlog and want it drained |
adapters/claude-routine-runner.md |
One caveat couples the axes: with attach-at-relay the worker cannot open its own
PR yet — finish runs --push-only and the PR is opened in an attended step
(Opening the PR). The other
two auth models let finish open the PR directly.
One-time setup (operator go-live checklist)¶
Done once; does not repeat per run. All the code these steps drive is shipped — this is deployment, not development.
A. GitHub App — the credential source¶
The App mints short-lived, repo-scoped installation tokens without any dev machine being awake, and one App can span every account/org you own.
- Create the App (GitHub → Settings → Developer settings → GitHub Apps → New): Contents = Read & write, Pull requests = Read & write (nothing else), and Where can this app be installed = Any account.
- Install it on every account/org whose repos your workspaces touch.
- Download the private key (
.pem) — GitHub lets you download it once. - Put the creds on the egress host (key under a gitignored path, never in git):
MSHIP_GH_APP_ID=<app-id>
MSHIP_GH_APP_KEY=/path/to/app.private-key.pem # file PATH, not the PEM text
There is no installation id to set — the egress proxy resolves the
installation per repo from the App key. Full detail: cloud-agent-auth.md §2.
B. Relay egress proxy — the credential-attaching front door¶
Front the egress proxy with Caddy and run it with the App creds in its env:
# Caddy: egress.<RELAY_DOMAIN> -> 127.0.0.1:47280 (on-demand TLS;
# the `egress` label is allow-listed in tls_ask).
MSHIP_GH_APP_ID=<app-id> MSHIP_GH_APP_KEY=/path/to/app.pem \
mship relay egress-server \
--grant-store-dir ./grants-store \
--run-token-dir ./run-tokens-store
With no App creds the egress-server fails closed — every request returns
503, it never forwards unauthenticated. Full detail: cloud-worker-auth-spine.md
§5. The api.github.com leg rides the same subdomain (no extra route/TLS): §8.
C. Enroll a worker identity + set its ceiling¶
RELAY_STORE=/path/to/docker/relay/pending-store
RELAY_PUBKEYS=/path/to/docker/relay/pubkeys
# The worker device requests access; you approve it (existing enroll flow):
mship relay enroll # from the worker device — requests relay access
mship relay requests --store-dir "$RELAY_STORE" # on relay host: pending id/host/fp
mship relay approve <request-id> \
--store-dir "$RELAY_STORE" --pubkeys-dir "$RELAY_PUBKEYS"
# Set the CEILING — the repos this enrollment may EVER touch (superset of any
# single run's repos):
mship relay grant <enrollment-id> --store-dir "$RELAY_STORE" \
--provider github-app \
--repos owner/workspace-repo,owner/member-a,owner/member-b
Verify auth actually covers every repo before trusting the setup:
mship gh preflight
# ✓ auth OK — broker covers: owner/member-a, owner/member-b, ...
gh preflight is strict by design — it fails fast and names the exact repo the
App isn't installed on, so a run never burns AI tokens on code it then can't
push. Detail: cloud-agent-auth.md §4.
Per-run lifecycle (once per approved spec)¶
1. Approve the spec¶
Normal flow: brainstorm → mship spec → review → approve. The runner
implements exactly one approved spec per routine. Keep it review-gated — do not
wire any auto-merge.
2. Mint a per-run token (scoped to this run)¶
mship relay issue-run-token <enrollment-id> \
--repos "owner/workspace-repo,owner/member-a" \
--push-branch "feat/<slug>" \
--ttl 86400
--repos⊆ the enrollment's ceiling. Must include the workspace repo (so the worker can clone the workspace to read the spec/plan) and every affected member repo (to clone, push, and open PRs).--push-branchis the only branch the attached credential may push.- The token prints once. Inject it into the routine's environment as the
--run-tokenvalue. It is low-value: scoped, short-lived, useless without the relay.
3. Schedule the Claude Code routine¶
Scheduling and creating a routine is agent behaviour (your own Claude Code
/schedule ability), not mship code — mship does not automate it. Give the
routine an environment that installs mship and carries the relay URL + the run
token, and a prompt naming the spec to implement. See the
overnight-cloud-worker-routines skill for the exact pattern.
4. The worker (inside the fired routine)¶
# a. Clone the workspace repo through the relay, then from its root:
# b. Configure git for the relay and clone members with NO GitHub token:
mship bootstrap --relay-url "<relay-url>" --run-token "<run-token>"
# c. Fail-fast BEFORE spend — verify relay-routed auth can actually push:
mship gh preflight --relay-url "<relay-url>" --run-token "<run-token>"
# d. Implement the assigned spec (normal mship phase workflow).
# e. Land the branch THROUGH THE RELAY, then open the PR in an attended step.
# A run-token-only worker can push (git is relay-routed) but cannot open the
# PR itself — see "Opening the PR" below. So the worker pushes only:
mship finish --push-only # push the run branch via the relay; skip PR creation
# NEVER merges, NEVER pushes the base branch.
5. Morning: review + merge¶
You review and merge in Ground Control (the Queue / Review cockpit). This is the only merge step — the runner always stops at an open PR.
Opening the PR (why it is a separate step today)¶
mship finish's PR-open is not relay-aware. It creates PRs via the gh CLI
(or a direct api.github.com REST call) using a real GitHub credential, and has
no --relay-url/--run-token path — and git's insteadOf/extraHeader rewrite
only git transport, not that PR-open REST call. So a run-token-only worker can
push its branch through the relay but cannot open the PR itself. Complete the
run one of two ways:
- Attended PR-open (works today). The worker runs
mship finish --push-onlyto land the run branch through the relay; the PR is then opened by a step that holds real GitHub auth — the operator, or the orchestrator that scheduled the run — e.g.mship finish(orgh pr create) against the pushed branch. A human is in the loop at PR time anyway, so this fits the review-gated model. - Relay-wired PR-open (not yet shipped). Route finish's PR-open through the
relay's
/api/leg (the default-deny, PR-only enforcer is already built and host-locked — seecloud-worker-auth-spine.md§8), so the worker opens its own PR carrying only the run token. Tracked as follow-up.
Guarantees (the security posture, in one place)¶
- The worker holds no GitHub credential. No App key, no broker bearer, no
installation token — only the low-value per-run token. Presented directly to
github.comthat token is rejected; presented to the relay it unlocks only a run-branch push to the run's repos. - The minted App token is repo-scoped and short-TTL, and its Attachment is
host-locked to
github.com/api.github.com— a route misconfig cannot send it anywhere else. - The git leg enforces run-branch-only push (clone/fetch pass; other branches, other repos, and branch deletes are refused).
- The relay's
/api/(REST) leg is default-deny, PR-open only. It permitsPOST .../pulls+ scoped reads and refuses every write to an existing PR/issue, every ref/content mutation, and every merge. This enforcer is built and host-locked, ready for when PR-open is routed through the relay (see Opening the PR); today it is the worker's git push that rides the relay, and PR creation is a separate attended step. Nothing auto-merges — the fan-out is review-gated end to end (#393).
What's shipped vs what you set up¶
- Shipped (code, 2026-07-22):
bootstrap/gh preflight--relay-url+--run-token;mship relay grant/issue-run-token/egress-server; the git-receive-pack run-branch enforcer and the default-deny REST enforcer; theovernight-cloud-worker-routinesskill. - Operator go-live (once): create + install the GitHub App and put its creds
on the egress host (A); deploy the Caddy egress block and run
egress-server(B); enroll + grant a worker identity (C). Then, per run: mint a token and schedule the routine. - Not yet wired:
mship finish's PR-open through the relay/api/leg — the enforcer is built but finish has no--relay-url/--run-tokenpath, so a run-token-only worker pushes with--push-onlyand the PR is opened in an attended step (see Opening the PR).
Variants: when to use them¶
- The
/gh-tokenbroker (daytime / trusted, zero App). If a workspace only runs while your own machine is awake, skip the App entirely:mship serve's/gh-tokenproxies the host'sgh auth token, and the worker pulls a short-lived token itself. Simpler, but single-identity and needs a machine awake + logged in. Detail:cloud-agent-auth.md§3. - Pull-API runner —
mship item run-next. Instead of scheduling one routine per named spec, let mship pick the next eligible item from a backlog: it selects the oldestunattended+ready+ approved item, claims it (git-backed), and emits a dispatch prompt; the host runs one item per tick andmship item bails on an unresolvable fork. Use when you want backlog-draining rather than per-spec scheduling. Full contract + smoke test:adapters/claude-routine-runner.md.
See also¶
cloud-worker-auth-spine.md— attach-at-relay: the credential egress proxy (trust model, seams, enforcers).cloud-agent-auth.md— the/gh-tokenbroker + the GitHub App setup both auth models share.adapters/claude-routine-runner.md— the pull-API runner (Claude routine host) + smoke test.- skill
overnight-cloud-worker-routines— the agent-facing routine pattern. - specs:
cloud-worker-auth-spine,worker-pr-egress,relay-aware-worker-boot,cloud-agent-github-auth,unattended-runner.