Cloud · page 1 of 3
Keep claims, decisions and settings on a git branch so two machines share them.
By default the run state (claims, decisions, story progress, run records, settings) is plain files in .foreman/, on one machine. To share it between machines, such as a laptop and a runner in the cloud, keep it on a git branch. This page is for anyone setting up a cloud runner or a GitHub Actions job, which needs it, and for anyone who wants foreman status on a laptop to show what a runner is doing.
Turning it on
"state": { "store": "branch", "branch": "foreman-state" }
state.store is files (the default) or branch; state.branch is a plain git branch name, foreman-state by default, and may not be the base branch. The Set up tab’s Cloud runner step writes this for you when you choose GitHub Actions (Run in the cloud).
.foreman/ then becomes a worktree of foreman-state, an orphan branch that holds only Foreman’s state, with no history shared with the project. The first command that uses it creates the branch with one root commit and pushes it to the project’s remote; another clone that finds the branch already on the remote checks it out instead. Files already in .foreman/ are moved onto the branch once. A file the branch already has keeps the branch’s copy, and the local one is left in a folder beside the state folder named .foreman.local-<stamp>, which the command points out. The state folder is added to the project’s .git/info/exclude so the project never sees it.
What goes on the branch
Only small state files are ever committed: state.json, control.json, settings.json, learn.json, the events and findings logs, STOP, last-stop and HALT, and the folders decisions/, claims/, locks/, stories/ (the overlays and stories/new/*.md), parks/, runs/, slots/, jira-outbox/, waits/, the learning ledgers and learned/, and claude-signin.json (which sign-in the sessions use, never a key). Logs, slot worktrees, sync/, caches, raw console copies, the lessons scan, status.json and temp files stay on each machine; the branch’s own .gitignore keeps them out of git status too.
A file that looks like it holds a token is never committed. That covers names such as .env*, *.pem, *.key, credentials and secrets, and any file whose text has the shape of a GitHub, Anthropic, Stripe, AWS or Slack token or a private key. The command says which files it refused: “not sent to the state branch, because they look like they hold a secret”. Git runs with hooks and commit signing off, as the project’s user, or as “Foreman” when the machine has no git identity (a fresh cloud runner).
Which commands pull and push
Every command that changes state pulls the branch first and pushes after, as one commit named for the command. The read-only ones pull first, so foreman decide list on a laptop shows what a runner left.
| Command | What it does to the branch |
|---|---|
pause, resume, stop, workers, slot, handback, next, fixed, learn |
Pull, change, push |
decide answer, decide applied, decide add |
Pull, change, push |
story start, block, done, todo, end, set, depend, finalize |
Pull, change, push |
settings set, settings reset (without --team) |
Pull, change, push |
release watch, claude use-key |
Pull, change, push |
learning undo, keep, run, tick |
Pull, change, push |
status, report, decide list, decide pending, story next, story plan, learning list, show, scan |
Pull only |
settings --team |
Neither: team settings edit foreman.config.json, which is the project’s |
cloud tick, cloud sync |
Sync themselves, around a whole run (see below) |
A write whose pull fails does not run: “cannot get the state from <remote>/<branch>, so nothing was changed”. A read whose pull fails carries on with this machine’s copy and says so. A push that keeps failing leaves the change committed on this machine for the next command to send, and the command exits 1. foreman cloud sync pushes the state folder by hand; it is the last step of the GitHub Actions workflow, so a cancelled job keeps its state.
Two machines at once
A pull commits any local changes, fetches, and rebases them onto the remote. A push commits and pushes; a rejected push fetches and rebases again, up to 5 tries with a short wait between them. When the rebase conflicts, the remote’s copy is taken and this command’s own writes are put back on top, so for a file both machines changed at once, the machine that pushes last keeps its version of that file. The .jsonl logs are merged line by line (merge=union), so events appended on two machines both survive. Syncs on one machine take turns through a lock in the project’s git folder; a rebase a crash left behind is dropped at the next sync, its commits still on the branch.
FOREMAN_STATE_SYNC=off
The supervisor does not sync on every command. In GitHub Actions, foreman cloud tick pulls the branch once at the start of the job and pushes once at the end, and runs the supervisor and everything it starts with FOREMAN_STATE_SYNC=off, so the commands a story session calls work on the runner’s copy without pulling and pushing each time. You can set the same variable yourself to run a command against this machine’s copy alone. The container runner (foreman cloud run) keeps its state in its own volume and needs no branch unless you want a laptop to read it.
Checks
foreman cloud doctor on a machine with state.store: "branch" checks that the branch can be fetched from the remote and that a push to the remote would succeed, with a dry run. foreman doctor on the laptop fails the cloud workflow check when .github/workflows/foreman.yml exists but the state is still in files, since the workflow needs the branch. See Cloud doctor.
Was this page useful? Something missing or wrong?Email us and say which section.