Skip to content

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-token broker: 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-next selects + 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-branch is the only branch the attached credential may push.
  • The token prints once. Inject it into the routine's environment as the --run-token value. 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-only to 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 (or gh 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 — see cloud-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.com that 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 permits POST .../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; the overnight-cloud-worker-routines skill.
  • 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-token path, so a run-token-only worker pushes with --push-only and the PR is opened in an attended step (see Opening the PR).

Variants: when to use them

  • The /gh-token broker (daytime / trusted, zero App). If a workspace only runs while your own machine is awake, skip the App entirely: mship serve's /gh-token proxies the host's gh 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 oldest unattended + ready + approved item, claims it (git-backed), and emits a dispatch prompt; the host runs one item per tick and mship 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-token broker + 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.