GitHub sync
GitHub sync is opt-in. Nothing syncs until this skill enables it. Once
enabled, three pieces work together (see system/libs/github_sync/README.md):
origin points at a dedicated private GitHub repo for this workspace.
- Global git wiring routes all
https://github.com/... traffic through the
latchkey gateway (credential injected server-side; no token in the
container) and activates the post-commit hook, so every commit on any
checkout -- main repo and worker worktrees -- auto-pushes its branch.
- The
[program:github-sync] service is a watchdog: every 60s it re-applies
the git wiring (self-healing gateway port changes) and re-verifies the repo
stays private, halting the hook's pushes if it ever isn't.
What is synced is exactly what is committed to git. Workspace data under
data/ (memories, tickets, uploads, per-app data) is gitignored and is
NOT shipped to GitHub -- the restic host-backup service covers it.
Hard rules
- Private repos only. Never create a public repo, never point sync at a
public repo, and never work around a visibility halt. If the user asks for
a public sync repo, decline and explain: agents can push secrets or other
sensitive data without realizing it.
- Everything flows through latchkey. Never ask the user for a GitHub
token and never embed credentials in URLs or git config.
origin is reserved for the sync repo. Upstream-template operations keep
using system/config/parent.toml (see the update-self skill) and are unaffected.
Enable
Check current state: uv run github-sync status. If is_configured
is already true, jump to "Status" (or "Repair" if the service is
unhealthy). Also run supervisorctl status github-sync (it errors when no
such program exists -- expected before enable).
Request GitHub permissions through latchkey (see the latchkey skill
for the permission-request mechanics). GitHub exposes two latchkey scopes
and a permission request carries exactly one scope, so this is two
requests. A request must be the only command in its tool call, so these
are two calls -- send them back to back; you do not have to wait for the
first verdict to file the second.
Do both before any other GitHub call, and say up front that two
approvals are coming so the user is not surprised by the second. Never
dribble out further requests later in the flow.
First call, on its own:
latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \
-H 'Content-Type: application/json' \
-d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-git", "permissions": ["github-git-read", "github-git-write"]}, "rationale": "GitHub sync: push this workspace'"'"'s branches to your private sync repo."}'
Then the second call, on its own:
latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \
-H 'Content-Type: application/json' \
-d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-rest-api", "permissions": ["github-read-user", "github-read-repos", "github-write-all"]}, "rationale": "GitHub sync: create the private sync repo (needs github-write-all), confirm which GitHub account it lands under (github-read-user), and verify it stays private (github-read-repos)."}'
This exact permission set is what the flow needs -- do not trim it, or the
user gets asked again mid-flow:
github-write-all -- repo creation (POST /user/repos). The narrower
github-write-repos covers only existing-repo (/repos/{owner}/{repo})
paths and is not enough to create a repo. It also covers the
optional repo deletion on disable.
github-read-repos -- the recurring private-visibility check
(GET /repos/{owner}/{repo}), which the service repeats forever.
github-read-user -- GET /user, to name the account the repo will be
created under (step 3) and as the one legitimate "did the grants land?"
probe (below).
github-git-read / github-git-write -- clone/fetch and push.
Then wait for both approval system messages ("Your permission request
for GitHub (git) / (REST API) was granted..."). Those messages are the
authoritative signal; they are what tells you to proceed.
Do not treat a rejected API call as evidence that a grant is missing.
{"error": "Error: Request not permitted by the user."} means that
endpoint is not covered by the granted permissions -- it does not mean
the approval failed to arrive. Probing an endpoint outside the set above
(e.g. GET /user/repos, which needs broader read access) will be rejected
even when everything is granted correctly. If you want to sanity-check
after the grant messages arrive, the only probe to use is:
latchkey curl -s https://api.github.com/user
Never re-ask the user for a permission you have already been told was
granted; re-read this list and check which endpoint you called instead.
Pick the repo. Default: create a brand-new private repo named after
the workspace ($MINDS_WORKSPACE_NAME), owned by the authenticated GitHub
account (read its login from latchkey curl -s https://api.github.com/user).
Confirm the name and owner with the user first; they can name an org
instead. One exception: if git remote get-url origin already points at a
user-owned repo (not imbue-ai/default-workspace-template or another
shared template), ask whether to reuse it or create a fresh one --
recommend a fresh dedicated repo unless they have a specific reason.
Reused repos must be verified private and writable like new ones.
Create the repo (skip if reusing):
latchkey curl -s -X POST https://api.github.com/user/repos \
-H 'Content-Type: application/json' \
-d '{"name": "<repo-name>", "private": true, "description": "Private sync repo for the <workspace> minds workspace"}'
(For an org: POST https://api.github.com/orgs/<org>/repos.) On a 422
name-taken error, append -2, -3, ... and retry. The response JSON must
contain "private": true -- if it does not, delete/abandon the repo and
stop; do not proceed with a public repo. The response's full_name is the
authoritative <owner>/<repo> to use from here on.
Point origin at it and record the config:
git remote set-url origin https://github.com/<owner>/<repo>.git \
|| git remote add origin https://github.com/<owner>/<repo>.git
Write data/system/github_sync.toml (this file is the "sync is
enabled" marker for the service and the post-commit hook; it lives under
the gitignored data/system/ with the other runtime-written config):
# Written by the github-sync skill. Presence of this file enables GitHub
# sync; `uv run github-sync status` reports on it.
repo_url = "https://github.com/<owner>/<repo>"
Wire git through the gateway: uv run github-sync wire-git. From now
on plain git push/git fetch against github.com works in every
checkout, and the post-commit auto-push hook is active.
Verify private before any push: uv run github-sync check-visibility
must print private (exit 0). If not, stop and surface the problem.
Initial sync: push the current branch and any existing worker
branches:
git push --set-upstream origin "$(git branch --show-current)"
for b in $(git for-each-ref --format='%(refname:short)' refs/heads/ | grep -v -x -e "$(git branch --show-current)"); do git push origin "$b"; done
Add the service by appending this block to system/supervisord.conf, then
supervisorctl reread && supervisorctl update (see the update-app
skill):
# Opt-in GitHub sync (added by the github-sync skill): keeps the gateway
# git wiring fresh and re-verifies the sync repo stays private (the
# post-commit hook does the pushing). See system/libs/github_sync/README.md.
# The oom_tag_service.py prefix sets its OOM shed-priority band (see
# system/services/oom_priority).
[program:github-sync]
command=python3 system/services/oom_priority/bin/oom_tag_service.py github-sync uv run github-sync run
directory=/home/user/workspace
autostart=true
autorestart=true
startretries=1000000
stopasgroup=true
killasgroup=true
stdout_logfile=/var/log/supervisor/github-sync-stdout.log
stderr_logfile=/var/log/supervisor/github-sync-stderr.log
stdout_logfile_maxbytes=10MB
stderr_logfile_maxbytes=10MB
stdout_logfile_backups=3
stderr_logfile_backups=3
Commit the enablement (system/supervisord.conf; the config file is
gitignored under data/system/). The now-active hook pushes the commit.
Report: the repo URL, that every commit now auto-pushes, that
workspace data under data/ stays out of GitHub (the restic host backup
covers it), and that pushes queue while the latchkey gateway is
unreachable and go out with the next commit once it is back.
Status
uv run github-sync status prints config + the service's latest status
(visibility, errors); supervisorctl status github-sync shows the
process; logs are at /var/log/supervisor/github-sync-*.log and
/tmp/github-sync.log, hook output at /tmp/post-commit-push.log. Explain
findings in plain language. If is_push_allowed is false, the repo is public
or unverifiable -- tell the user to make it private again; sync resumes
automatically.
Repair (workspace recreated from a synced repo)
A workspace created from a previously-synced private repo inherits the
service block, but not the gitignored data/system/github_sync.toml, the
latchkey permissions, or the container-local wiring. To repair: re-write the
config file (step 5), then run step 2 (permission requests); the service
self-heals within a tick (re-wires git). Verify with "Status", or accelerate
with uv run github-sync wire-git. Workspace data under data/ comes back
via a restic backup restore, not via GitHub.
Disable
Confirm with the user first, and ask separately whether to keep the remote
repo (recommend keeping it -- it costs nothing and preserves history).
supervisorctl stop github-sync, remove the [program:github-sync] block
from system/supervisord.conf, then supervisorctl reread && supervisorctl update.
uv run github-sync unwire-git (removes the gateway git config and the
hooks path -- auto-push stops).
- Delete
data/system/github_sync.toml.
- If the user chose to delete the remote repo:
latchkey curl -s -X DELETE https://api.github.com/repos/<owner>/<repo>
(covered by the github-write-all granted at enable). If the grant has
since been revoked, do not re-request it just for this -- point them at
the repo's GitHub settings page to delete it themselves.
- Commit the removal. Note that this commit is NOT auto-pushed (the hook is
inert again).
1---2name: github-sync3description: Enable, check, or disable GitHub sync for this workspace. Enabling creates a dedicated PRIVATE GitHub repo via latchkey, points origin at it, and auto-pushes every commit from every checkout. Workspace data under data/ is NOT synced to GitHub (the restic host backup covers it). Use when the user asks to back up / sync the workspace to GitHub, enable auto-push, or asks about GitHub sync status.4---56# GitHub sync78GitHub sync is opt-in. Nothing syncs until this skill enables it. Once9enabled, three pieces work together (see `system/libs/github_sync/README.md`):10111. `origin` points at a dedicated **private** GitHub repo for this workspace.122. Global git wiring routes all `https://github.com/...` traffic through the13 latchkey gateway (credential injected server-side; no token in the14 container) and activates the `post-commit` hook, so every commit on any15 checkout -- main repo and worker worktrees -- auto-pushes its branch.163. The `[program:github-sync]` service is a watchdog: every 60s it re-applies17 the git wiring (self-healing gateway port changes) and re-verifies the repo18 stays private, halting the hook's pushes if it ever isn't.1920What is synced is exactly what is committed to git. Workspace data under21`data/` (memories, tickets, uploads, per-app data) is gitignored and is22NOT shipped to GitHub -- the restic `host-backup` service covers it.2324## Hard rules2526- **Private repos only.** Never create a public repo, never point sync at a27 public repo, and never work around a visibility halt. If the user asks for28 a public sync repo, decline and explain: agents can push secrets or other29 sensitive data without realizing it.30- **Everything flows through latchkey.** Never ask the user for a GitHub31 token and never embed credentials in URLs or git config.32- `origin` is reserved for the sync repo. Upstream-template operations keep33 using `system/config/parent.toml` (see the update-self skill) and are unaffected.3435## Enable36371. **Check current state**: `uv run github-sync status`. If `is_configured`38 is already true, jump to "Status" (or "Repair" if the service is39 unhealthy). Also run `supervisorctl status github-sync` (it errors when no40 such program exists -- expected before enable).41422. **Request GitHub permissions** through latchkey (see the latchkey skill43 for the permission-request mechanics). GitHub exposes two latchkey scopes44 and a permission request carries exactly one scope, so this is two45 requests. A request must be the **only** command in its tool call, so these46 are two calls -- send them back to back; you do not have to wait for the47 first verdict to file the second.48 Do both **before any other GitHub call**, and say up front that two49 approvals are coming so the user is not surprised by the second. Never50 dribble out further requests later in the flow.5152 First call, on its own:5354 ```bash55 latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \56 -H 'Content-Type: application/json' \57 -d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-git", "permissions": ["github-git-read", "github-git-write"]}, "rationale": "GitHub sync: push this workspace'"'"'s branches to your private sync repo."}'58 ```5960 Then the second call, on its own:6162 ```bash63 latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \64 -H 'Content-Type: application/json' \65 -d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-rest-api", "permissions": ["github-read-user", "github-read-repos", "github-write-all"]}, "rationale": "GitHub sync: create the private sync repo (needs github-write-all), confirm which GitHub account it lands under (github-read-user), and verify it stays private (github-read-repos)."}'66 ```6768 This exact permission set is what the flow needs -- do not trim it, or the69 user gets asked again mid-flow:7071 - `github-write-all` -- repo creation (`POST /user/repos`). The narrower72 `github-write-repos` covers only existing-repo (`/repos/{owner}/{repo}`)73 paths and is **not** enough to create a repo. It also covers the74 optional repo deletion on disable.75 - `github-read-repos` -- the recurring private-visibility check76 (`GET /repos/{owner}/{repo}`), which the service repeats forever.77 - `github-read-user` -- `GET /user`, to name the account the repo will be78 created under (step 3) and as the one legitimate "did the grants land?"79 probe (below).80 - `github-git-read` / `github-git-write` -- clone/fetch and push.8182 Then **wait for both approval system messages** ("Your permission request83 for GitHub (git) / (REST API) was granted..."). Those messages are the84 authoritative signal; they are what tells you to proceed.8586 **Do not treat a rejected API call as evidence that a grant is missing.**87 `{"error": "Error: Request not permitted by the user."}` means *that88 endpoint* is not covered by the granted permissions -- it does not mean89 the approval failed to arrive. Probing an endpoint outside the set above90 (e.g. `GET /user/repos`, which needs broader read access) will be rejected91 even when everything is granted correctly. If you want to sanity-check92 after the grant messages arrive, the only probe to use is:9394 ```bash95 latchkey curl -s https://api.github.com/user96 ```9798 Never re-ask the user for a permission you have already been told was99 granted; re-read this list and check *which endpoint* you called instead.1001013. **Pick the repo.** Default: create a brand-new private repo named after102 the workspace (`$MINDS_WORKSPACE_NAME`), owned by the authenticated GitHub103 account (read its `login` from `latchkey curl -s https://api.github.com/user`).104 Confirm the name and owner with the user first; they can name an org105 instead. One exception: if `git remote get-url origin` already points at a106 user-owned repo (not `imbue-ai/default-workspace-template` or another107 shared template), ask whether to reuse it or create a fresh one --108 recommend a fresh dedicated repo unless they have a specific reason.109 Reused repos must be verified private and writable like new ones.1101114. **Create the repo** (skip if reusing):112113 ```bash114 latchkey curl -s -X POST https://api.github.com/user/repos \115 -H 'Content-Type: application/json' \116 -d '{"name": "<repo-name>", "private": true, "description": "Private sync repo for the <workspace> minds workspace"}'117 ```118119 (For an org: `POST https://api.github.com/orgs/<org>/repos`.) On a 422120 name-taken error, append `-2`, `-3`, ... and retry. The response JSON must121 contain `"private": true` -- if it does not, delete/abandon the repo and122 stop; do not proceed with a public repo. The response's `full_name` is the123 authoritative `<owner>/<repo>` to use from here on.1241255. **Point origin at it and record the config**:126127 ```bash128 git remote set-url origin https://github.com/<owner>/<repo>.git \129 || git remote add origin https://github.com/<owner>/<repo>.git130 ```131132 Write `data/system/github_sync.toml` (this file is the "sync is133 enabled" marker for the service and the post-commit hook; it lives under134 the gitignored `data/system/` with the other runtime-written config):135136 ```toml137 # Written by the github-sync skill. Presence of this file enables GitHub138 # sync; `uv run github-sync status` reports on it.139 repo_url = "https://github.com/<owner>/<repo>"140 ```1411426. **Wire git through the gateway**: `uv run github-sync wire-git`. From now143 on plain `git push`/`git fetch` against github.com works in every144 checkout, and the post-commit auto-push hook is active.1451467. **Verify private before any push**: `uv run github-sync check-visibility`147 must print `private` (exit 0). If not, stop and surface the problem.1481498. **Initial sync**: push the current branch and any existing worker150 branches:151152 ```bash153 git push --set-upstream origin "$(git branch --show-current)"154 for b in $(git for-each-ref --format='%(refname:short)' refs/heads/ | grep -v -x -e "$(git branch --show-current)"); do git push origin "$b"; done155 ```1561579. **Add the service** by appending this block to `system/supervisord.conf`, then158 `supervisorctl reread && supervisorctl update` (see the update-app159 skill):160161 ```ini162 # Opt-in GitHub sync (added by the github-sync skill): keeps the gateway163 # git wiring fresh and re-verifies the sync repo stays private (the164 # post-commit hook does the pushing). See system/libs/github_sync/README.md.165 # The oom_tag_service.py prefix sets its OOM shed-priority band (see166 # system/services/oom_priority).167 [program:github-sync]168 command=python3 system/services/oom_priority/bin/oom_tag_service.py github-sync uv run github-sync run169 directory=/home/user/workspace170 autostart=true171 autorestart=true172 startretries=1000000173 stopasgroup=true174 killasgroup=true175 stdout_logfile=/var/log/supervisor/github-sync-stdout.log176 stderr_logfile=/var/log/supervisor/github-sync-stderr.log177 stdout_logfile_maxbytes=10MB178 stderr_logfile_maxbytes=10MB179 stdout_logfile_backups=3180 stderr_logfile_backups=3181 ```18218310. **Commit the enablement** (`system/supervisord.conf`; the config file is184 gitignored under `data/system/`). The now-active hook pushes the commit.18518611. **Report**: the repo URL, that every commit now auto-pushes, that187 workspace data under `data/` stays out of GitHub (the restic host backup188 covers it), and that pushes queue while the latchkey gateway is189 unreachable and go out with the next commit once it is back.190191## Status192193`uv run github-sync status` prints config + the service's latest status194(visibility, errors); `supervisorctl status github-sync` shows the195process; logs are at `/var/log/supervisor/github-sync-*.log` and196`/tmp/github-sync.log`, hook output at `/tmp/post-commit-push.log`. Explain197findings in plain language. If `is_push_allowed` is false, the repo is public198or unverifiable -- tell the user to make it private again; sync resumes199automatically.200201## Repair (workspace recreated from a synced repo)202203A workspace created from a previously-synced private repo inherits the204service block, but not the gitignored `data/system/github_sync.toml`, the205latchkey permissions, or the container-local wiring. To repair: re-write the206config file (step 5), then run step 2 (permission requests); the service207self-heals within a tick (re-wires git). Verify with "Status", or accelerate208with `uv run github-sync wire-git`. Workspace data under `data/` comes back209via a restic backup restore, not via GitHub.210211## Disable212213Confirm with the user first, and ask separately whether to keep the remote214repo (recommend keeping it -- it costs nothing and preserves history).2152161. `supervisorctl stop github-sync`, remove the `[program:github-sync]` block217 from `system/supervisord.conf`, then `supervisorctl reread && supervisorctl update`.2182. `uv run github-sync unwire-git` (removes the gateway git config and the219 hooks path -- auto-push stops).2203. Delete `data/system/github_sync.toml`.2214. If the user chose to delete the remote repo:222 `latchkey curl -s -X DELETE https://api.github.com/repos/<owner>/<repo>`223 (covered by the `github-write-all` granted at enable). If the grant has224 since been revoked, do not re-request it just for this -- point them at225 the repo's GitHub settings page to delete it themselves.2265. Commit the removal. Note that this commit is NOT auto-pushed (the hook is227 inert again).