# Github Sync

> 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.

- Skill: `imbue-ai/github-sync` (Agent Skill)
- Install (CLI): `npx skillmds@latest add imbue-ai/github-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/imbue-ai/github-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: imbue-ai (https://skillmd.com/u/imbue-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/imbue-ai/github-sync

---


# 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`):

1. `origin` points at a dedicated **private** GitHub repo for this workspace.
2. 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.
3. 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

1. **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).

2. **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:

   ```bash
   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:

   ```bash
   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:

   ```bash
   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.

3. **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.

4. **Create the repo** (skip if reusing):

   ```bash
   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.

5. **Point origin at it and record the config**:

   ```bash
   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):

   ```toml
   # 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>"
   ```

6. **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.

7. **Verify private before any push**: `uv run github-sync check-visibility`
   must print `private` (exit 0). If not, stop and surface the problem.

8. **Initial sync**: push the current branch and any existing worker
   branches:

   ```bash
   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
   ```

9. **Add the service** by appending this block to `system/supervisord.conf`, then
    `supervisorctl reread && supervisorctl update` (see the update-app
    skill):

    ```ini
    # 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
    ```

10. **Commit the enablement** (`system/supervisord.conf`; the config file is
    gitignored under `data/system/`). The now-active hook pushes the commit.

11. **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).

1. `supervisorctl stop github-sync`, remove the `[program:github-sync]` block
   from `system/supervisord.conf`, then `supervisorctl reread && supervisorctl update`.
2. `uv run github-sync unwire-git` (removes the gateway git config and the
   hooks path -- auto-push stops).
3. Delete `data/system/github_sync.toml`.
4. 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.
5. Commit the removal. Note that this commit is NOT auto-pushed (the hook is
   inert again).

