Synthesis Mac Sync
A methodology for keeping multiple Macs in sync using iCloud for configuration files and Git for repositories. Designed to run fully automated via an AI coding assistant — the user says "run my mac-sync" and the assistant handles everything.
This skill provides the protocol — the sync methodology, safety rules, conflict resolution, and manifest format. Your personal config file (a README or manifest in your iCloud sync folder) provides the specifics: which files, which machines, which repos.
Configuration
These values are user-specific. Update them for your environment.
| Setting | Value | Description |
|---|---|---|
icloud_sync_folder |
~/Library/Mobile Documents/com~apple~CloudDocs/workspaces/[username]/mac-sync/ |
iCloud Drive folder for synced config files |
git_scan_root |
~/workspaces/ |
Root directory for git repository discovery |
git_scan_max_depth |
3 |
Maximum depth for recursive .git directory search |
git_repos_manifest |
git-repos.yaml |
Legacy central manifest (transitional; see v1.6.0 section) |
workspace_repo_manifest |
<workspace>/.agents/repos.yaml |
Per-workspace repo manifest (v1.6.0; canonical for repo entries) |
Architecture
synthesis-mac-sync (this skill)
= the HOW — sync protocol, safety rules, conflict resolution, manifest format
Your config file (in your iCloud sync folder)
= the WHAT — your specific file list, machine inventory, repo paths
The skill is invoked by the AI assistant. The assistant reads both this skill (for methodology) and your config file (for specifics), then executes the sync.
Agent configuration ownership
Agent instructions, lifecycle hooks, and the stable subset of client configuration use version-controlled sources plus native adapters:
- Public reusable behavior lives in the
synthesis-skillsplugin. - Personal rules, private skills, hook policy, and owned config overlays live in the private personal source repository.
- Repository instructions live in tracked
AGENTS.md;CLAUDE.mdimports it. - Runtime-owned auth, trust databases, marketplace timestamps, caches, and machine-specific paths stay local to each client.
Do not synchronize an entire client configuration file when the client also
writes volatile state into it. Install or merge only the keys owned by the
synthesis configuration source, then run synthesis-agent-conformance.
Setup
1. Create a sync folder in iCloud
Create a folder in iCloud Drive to hold your synced configuration files. Example path:
~/Library/Mobile Documents/com~apple~CloudDocs/workspaces/[username]/mac-sync/
2. Create a config file (README.md)
Your config file lives in the sync folder. It contains:
- Sync manifest — which files to sync and where they map locally
- Machine inventory — your Mac hostnames and details
- Git repo configuration — optional repo sync settings
- One-time actions — machine-specific tasks to run once
See the Config File Format section below for the template.
3. Copy your config files into the sync folder
Mirror the directory structure. For example:
mac-sync/
.gitconfig → syncs to ~/.gitconfig
.zshrc → syncs to ~/.zshrc
.config/app/keys.yaml → syncs to ~/.config/app/keys.yaml
New-machine bootstrap — protection layer first
On any new Mac (or after a macOS / Xcode CLT update, which can replace interpreters and wipe user site-packages), bring up the protection layer BEFORE the first commit, not after:
- Install the synthesis-git-hooks engine and verify it in one step:
~/workspaces/<you>/synthesis-skills/skills/synthesis-git-hooks/scripts/install.sh— the v2 installer runs the--doctorself-check at the end and exits nonzero if the chain is not fully healthy (the engine itself fails closed, so an unhealthy install blocks commits rather than passing them unscanned). - Pull the synced policy config (
~/.synthesis/git-hook-config.yaml) via the config-file sync BEFORE running repos sync — the hooks read it on every commit. - Install the
synthesis-skillsplugin into both Claude and Codex. - Run the private agent-configuration installer. It renders global instructions, installs private skills, merges owned client settings, and installs native hook adapters.
- Run
synthesis-agent-conformance source,runtime, andinstructionsbefore the first commit. - Only then run the git-repos sync and begin work.
Ordering rationale: a fresh machine is exactly where the old failure mode lived — interpreters differ, site-packages are empty, and the first commits of a migration are often bulk imports with the highest leak surface. The v2 engine needs no third-party packages (any python3 works), so step 1 has no pip prerequisite on a fresh macOS with CLT installed.
Sync Modes
Full Sync ("run my mac-sync")
Performs these operations in order:
- Pre-pull iCloud download check —
brctl download "$ICLOUD_BASE"followed by polling for.icloudplaceholder files until none remain (with 5-minute timeout), per the iCloud Propagation Verification section. Ensures any pulls operate on materialized data. - Config file sync — bidirectional, based on modification timestamps
- Post-push iCloud upload check —
brctl monitor --wait-uploaded -t 300 "$ICLOUD_BASE", per the iCloud Propagation Verification section. Ensures local-newer pushes have reached Apple's servers before reporting completion. - Workspace config symlinks — idempotent reconciliation against each workspace-private repo's
.agents/canonical (see Workspace Config Symlinks section below) - Git repo sync — fetch, pull if behind, push if ahead
- One-time actions — execute any pending machine-specific actions
Config-Only Sync
- Pull from iCloud: "sync my Mac config files from iCloud"
- Push to iCloud: "push my Mac config files to iCloud"
Git-Only Sync
- Full: "sync my repos with GitHub"
- Status only: "show me the status of my repos"
Push Policy (v1.3.0)
Each repo in the manifest may have a push_policy field. When set, the sync protocol MUST respect it — refusing to push to any remote that violates the policy.
push_policy |
Meaning |
|---|---|
| unset (default) | Push to all remotes configured in the repo |
owner-only |
Only push to origin when origin points to the owner's personal GitHub account. Never push to a client org, another GitHub org, or a third-party Git host. Used for ai-knowledge-*-private repos that carry Type 3 (personal-client) content. |
pr-required |
Never auto-push; user must review and open a PR |
For owner-only repos:
- Before any
git push, verifygit remote -vshows ONLY the owner's personal-GitHub URLs - If any non-owner remote exists, abort with an error and alert the user
- These repos exist per ADR-013 (workspace-private repos); the
-privatesuffix is also a discovery protocol filter (ADR-014)
Local session end and remote handoff
Ordinary Claude Code and Codex turn boundaries target LOCAL_READY, not a remote push. Synthesis-repo-guard records session-attributed edit manifests and Stop receipts without changing Git. Same-machine switching therefore needs no commit or network call.
- If repo-guard reports local state: another client on this machine can recover it.
- If repo-guard reports unattributed or unsafe state: inspect the detailed report and repair the local handoff before switching clients.
Configure repo-guard hooks per the synthesis-repo-guard skill. Mac-sync owns cross-machine publication and source-repository policy; repo-guard owns attribution, local receipts, and exact private-context publication.
Remote-handoff mode
Invoke this mode before changing computers. It refreshes the coordination lease, updates project tiers from working-tree truth, reads pending manifests, publishes owned source paths under each repository policy, flushes exact private-context paths, and verifies remote readiness. Source paths that are dirty, ahead, behind, detached, or lack an upstream keep the handoff blocked. A pr-required repository remains gated by its review policy.
Run remote-mode context doctor and continuity conformance for every project being transferred, then run the full repo detector and verify intended remote heads. Report REMOTE_READY only when no relevant pending manifest remains. Day-end performs this same transition automatically. The destination computer runs the pull side first, verifies fast-forward state and lease ownership, then uses the normal Session Start Protocol.
The source-machine gate runs the checkpoint flush, context doctor with
readiness remote, and continuity conformance with readiness remote, in that
order after source repositories are published. The destination repeats remote
continuity after fetching and fast-forwarding.
Performance — Minimize Tool Calls
CRITICAL: Mac-sync must run with minimal interactive tool calls. The user should NOT face dozens of approval prompts.
Config file sync — ONE bash call
Write a single bash script that loops through ALL files in the manifest, compares them with diff, checks timestamps with stat -f %m if different, copies the newer version, and applies chmod 600 to sensitive files. Execute this entire script in ONE Bash tool call. Do NOT run separate diff, stat, or cp commands for each file.
Example pattern:
ICLOUD_BASE="$HOME/Library/Mobile Documents/com~apple~CloudDocs/workspaces/[username]/mac-sync"
sync_file() {
local icloud="$1" local_path="$2" sensitive="$3"
if [ ! -f "$icloud" ] && [ ! -f "$local_path" ]; then echo "SKIP (neither exists): $local_path"; return; fi
if [ ! -f "$icloud" ]; then echo "COPY local→iCloud: $local_path"; cp "$local_path" "$icloud"; return; fi
if [ ! -f "$local_path" ]; then echo "COPY iCloud→local: $local_path"; mkdir -p "$(dirname "$local_path")"; cp "$icloud" "$local_path"; [ "$sensitive" = "yes" ] && chmod 600 "$local_path"; return; fi
if diff -q "$icloud" "$local_path" > /dev/null 2>&1; then echo "IDENTICAL: $local_path"; return; fi
local icloud_ts=$(stat -f %m "$icloud") local_ts=$(stat -f %m "$local_path")
if [ "$icloud_ts" -gt "$local_ts" ]; then echo "SYNC iCloud→local (newer): $local_path"; cp "$icloud" "$local_path"; [ "$sensitive" = "yes" ] && chmod 600 "$local_path"
else echo "SYNC local→iCloud (newer): $local_path"; cp "$local_path" "$icloud"; fi
}
# Direct timestamp sync is appropriate for user-owned static files.
sync_file "$ICLOUD_BASE/.gitconfig" "$HOME/.gitconfig" "no"
# Agent instructions, hooks, plugins, and client config use their
# version-controlled installer + conformance flow instead of sync_file.
# Runtime auth may remain an explicit encrypted/sensitive manifest item when
# the user has deliberately chosen cross-machine credential synchronization.
Codex configuration overlay
Codex config.toml contains both declarative preferences and runtime-owned
state. The personal agent-config installer must merge only its declared keys,
such as model role, feature toggles, sandbox policy, and MCP definitions. It
must preserve plugin install state, marketplace update timestamps, hook trust,
project trust, and unknown keys. After the merge:
codex doctor --json
python3 <conformance-skill>/scripts/conformance.py runtime
Treat the source overlay and merged runtime file as separate truth layers.
Git repo sync — TWO to THREE bash calls maximum
- Discovery + fetch all (ONE call):
findrepos, then loop through all of them runninggit fetch originin a single script. - Status + auto-actions (ONE call): Loop through all repos checking branch, ahead/behind, uncommitted changes, stashes. In the SAME script, automatically
git pullrepos that are behind andgit pushrepos that are ahead with clean working trees. Collect all output into a structured summary. - Follow-up actions (ONE call, only if needed): Handle any repos that need individual attention (diverged, conflicts).
Example pattern for step 2:
REPOS=(
"/path/to/repo1"
"/path/to/repo2"
# ...
)
for repo in "${REPOS[@]}"; do
name=$(basename "$repo")
branch=$(git -C "$repo" branch --show-current 2>/dev/null)
if [ -z "$branch" ]; then echo "DETACHED: $name"; continue; fi
counts=$(git -C "$repo" rev-list --left-right --count "origin/$branch...$branch" 2>/dev/null)
behind=$(echo "$counts" | awk '{print $1}')
ahead=$(echo "$counts" | awk '{print $2}')
dirty=$(git -C "$repo" status --porcelain 2>/dev/null | head -5)
if [ "$behind" -gt 0 ]; then git -C "$repo" pull origin "$branch" 2>&1 | sed "s/^/PULLED $name: /"; fi
if [ "$ahead" -gt 0 ] && [ -z "$dirty" ]; then git -C "$repo" push origin "$branch" 2>&1 | sed "s/^/PUSHED $name: /"; fi
[ -n "$dirty" ] && echo "DIRTY $name: $(echo "$dirty" | wc -l | tr -d ' ') files"
done
Do NOT run individual tool calls per repo. The whole point of mac-sync is automation without interaction.
iCloud Propagation Verification (CRITICAL — added v1.5.0)
iCloud sync between Macs is two-stage and both stages take time:
- Upload: source Mac's local file → Apple's iCloud servers (handled by
bird/cloudd). - Download: Apple's iCloud servers → destination Mac's local file (also
bird/cloudd).
The receiving Mac CANNOT pull data that has not completed stage 1. Verifying the receiving side's download state without first verifying the source side's upload state misses the more fundamental question and can result in: stale iCloud content overwriting good local copies, new files silently skipped because they haven't propagated yet, and false "sync complete" reports when actual state diverges.
After every push to iCloud — wait for upload (MANDATORY)
After completing config-file sync (including the bidirectional sync's local-newer cases), the assistant MUST run:
brctl monitor --wait-uploaded -t 300 "$ICLOUD_BASE"
This blocks for up to 5 minutes until iCloud confirms Stopping the query because all items are now uploaded. Do NOT report sync complete — and especially do NOT draft cross-Mac handoff instructions — until this returns successfully.
If the command times out without confirming upload, alert the user. Network issues or Apple-side queue backlog may be at play. Do not proceed with handoff instructions in that case.
Before every pull from iCloud — verify download materialization (MANDATORY)
Before pulling iCloud → local (especially in cross-Mac handoff scenarios where another Mac just pushed), the assistant MUST verify all items have been downloaded from iCloud to the local Mac.
macOS brctl monitor only supports --wait-uploaded — it has NO --wait-downloaded flag. The correct download verification is trigger-then-poll:
# 1. Trigger materialization of all items in the sync folder.
# Idempotent — no-op if everything is already local.
brctl download "$ICLOUD_BASE"
# 2. Poll for .icloud placeholder files until none remain (with timeout).
DEADLINE=$(($(date +%s) + 300))
while true; do
PLACEHOLDERS=$(find "$ICLOUD_BASE" -type f \( -name "*.icloud" -o -name ".*.icloud" \) 2>/dev/null)
if [ -z "$PLACEHOLDERS" ]; then echo "All items materialized."; break; fi
if [ $(date +%s) -ge $DEADLINE ]; then
echo "TIMEOUT: still pending downloads after 5 minutes:"; echo "$PLACEHOLDERS"
break
fi
sleep 5
done
Only proceed with the pull operation when the placeholder list is empty. If the loop times out, alert the user — there may be a network or iCloud-queue issue.
Why .icloud placeholders work as the signal: when iCloud has a file but the local Mac hasn't downloaded its content yet, macOS represents it as a hidden zero-byte placeholder file with a .<filename>.icloud extension instead of the real file. Once iCloud finishes downloading, the placeholder is replaced with the actual file. So an empty placeholder set means full materialization.
Cross-Mac handoff (one-time actions involving another Mac)
When drafting a one-time action that involves running mac-sync on a different Mac (e.g., "the second Mac should now pull what the first Mac just pushed"):
- On the source Mac: complete remote-handoff mode and obtain
REMOTE_READYfor the project and its repositories. - Before drafting the handoff prompt: run
brctl monitor --wait-uploaded -t 300 "$ICLOUD_BASE"and confirm upload complete. - In the handoff prompt itself: include the download trigger and placeholder poll as the first destination step, then fetch, fast-forward, and run remote continuity before project work resumes.
These two together close the two-stage gap.
Pre-response self-check
Before sending any message that includes a cross-Mac handoff prompt, the assistant must ask itself: "have I verified iCloud upload is complete on this Mac?" If no, run the check first.
This is the same discipline as the cache-vs-truth rule (verify state has reached the system that will be read), applied to iCloud propagation.
Full bidirectional sync — when to run which check
| Scenario | Before pulling local from iCloud | After pushing local to iCloud |
|---|---|---|
| Single-Mac everyday sync | brctl download + placeholder poll (cheap no-op if already current) |
brctl monitor --wait-uploaded (cheap no-op if no new content) |
| Cross-Mac handoff (other Mac just pushed) | brctl download + placeholder poll (REQUIRED — waits for propagation) |
brctl monitor --wait-uploaded (REQUIRED if drafting reverse handoff) |
| Pull-only sync ("from iCloud") | brctl download + placeholder poll (REQUIRED) |
N/A (no push) |
| Push-only sync ("to iCloud") | N/A (no pull) | brctl monitor --wait-uploaded (REQUIRED) |
In the everyday single-Mac case the checks are cheap — they return almost immediately when iCloud is current. The cost of always running them is small; the cost of skipping them in a cross-Mac case is data loss.
Config File Sync Protocol
Bidirectional Sync (default)
For each file in the sync manifest (executed as a SINGLE batched script per the Performance section above):
- Compare iCloud version with local version using
diff - If identical → skip silently
- If different → compare modification timestamps using
stat -f %m(macOS) - Copy the newer file over the older one automatically
- For sensitive files, ensure
chmod 600after copying - Report what was synced in the summary
Pull from iCloud
When user explicitly asks to pull:
- Compare each iCloud file with its local counterpart
- If different, copy iCloud → local automatically
- Preserve permissions (chmod 600 for sensitive files)
Push to iCloud
When user explicitly asks to push:
- Compare each local file with its iCloud counterpart
- If different, copy local → iCloud automatically
Template File Expansion
For config files containing machine-specific paths, use template files with placeholders:
- When pulling: Replace
{{HOME}}with$HOMEand{{USERNAME}}with$USER - When pushing: Replace current
$HOMEvalue with{{HOME}}and$USERvalue with{{USERNAME}}
Safety Rules
- Automatic for one-sided changes — if only one side changed, copy automatically
- Prompt only for conflicts — if both sides changed and timestamps can't resolve, show diff and ask which to keep
- Preserve permissions — sensitive files must be
chmod 600 - Quote all paths — iCloud paths contain spaces ("Mobile Documents")
- Never overwrite with empty — if either file is empty or missing, do not overwrite the non-empty version
- Skip machine-specific path differences — if the only differences are username-specific paths (e.g.,
/Users/alice/vs/Users/bob/), skip and note in summary - Verify iCloud propagation — run
brctl monitor --wait-uploadedafter push andbrctl download+.icloudplaceholder polling before pull, per the iCloud Propagation Verification section above. Mandatory for cross-Mac handoff scenarios.
Per-Workspace Repo Manifests (v1.6.0) — the decentralized inventory
As of v1.6.0 (2026-07-08), the repo inventory is decentralized: each workspace owns its own list, and only a thin router stays central.
Canonical file: <workspace-private-repo>/.agents/repos.yaml, symlinked to <workspace>/.agents/repos.yaml (plus .claude/ and .codex/ back-compat) by the Workspace Config Symlinks layer (v1.4.0). A personal workspace without a -private repo carries it in the personal ai-knowledge repo. Cross-machine propagation is git (the context repo pushes/pulls, with its usual history, diffs, and conflict detection) — not iCloud.
Schema — facts vs policy. Fact fields (path, remotes, default_branches) are refreshed by scans. Policy fields (workspace-level status: active | dormant, per-repo ritual_sync, push_policy, category, notes) are declared by the user or their agent and are NEVER changed by a scan.
Scan behavior per workspace (replaces the central-yaml scan for repo entries):
- Refresh fact fields from
git remote -vand local branch listings. - Repo on disk but not in the manifest → PROPOSE adding it (facts pre-filled); the user confirms the policy fields.
- Repo in the manifest but not on disk → a clone decision, not an auto-clone (see Clone semantics).
- NEVER remove an entry, and NEVER delete a local clone. Retention rule: leaving a client, or a client shutting down, retires a workspace to
status: dormant— retained on disk, sync paused, nothing deleted. Deletion is a rare, explicit, manual act outside every sync flow. - Dead remotes (org deleted, Git host sunset): report once, keep the clone indefinitely — the local clone is the durable record.
Clone semantics (selective-cloning rule):
- The manifest records CHOSEN clones. Never enumerate a remote org (GitHub/Bitbucket) to clone or list everything the user can access.
- New-machine bootstrap (an explicit "set this machine up" act): read
machines.yamlfor the machine's subscribed workspaces, clone each workspace's context repo, read itsrepos.yaml, clone the curated list (minus anyexclude:entries). - Ongoing: a repo listed in a workspace manifest but missing locally is surfaced as a decision; a machine subscription may set
auto_clone: trueto mirror automatically (default: false).
Router file — machines.yaml (same folder as the legacy central yaml): machine inventory, per-machine workspaces: subscriptions (all or an explicit list — supports restricted machines such as client-issued hardware that must never hold other clients' names), optional auto_clone and exclude:, and the workspace → context-repo bootstrap map. This is the only repo-related state that stays centralized.
Transition: while the legacy repositories: section of git-repos.yaml still exists, each sync cross-checks it against the per-workspace manifests and REPORTS any drift — no silent divergence. After a drift-free window (at least two clean daily-ritual code-syncs plus one clean mac-sync), archive that section to a dated file; the Manifest Merge Protocol below then applies only to machines.yaml and any remaining central state.
Consumers: synthesis-daily-rituals v2.13.0+ enumerates day-start/day-end source-code sync from ritual_sync: yes entries; this skill reads the same files for repo and remote reconciliation; synthesis-repo-guard scans disk independently (unaffected, and may later soften alerts for status: dormant workspaces).
Git Repository Sync Protocol
Repository Discovery
Scan a configured directory (e.g., ~/workspaces/) recursively:
find ~/workspaces -maxdepth 3 -name ".git" -type d 2>/dev/null
Maintain a manifest file (git-repos.yaml) that caches discovered repos, their categories, and their remote configurations for quick status checks and cross-machine remote sync. Update using the merge protocol on each sync — never overwrite the yaml from scratch. See Git Remote Sync Protocol and Manifest Merge Protocol.
v1.6.0: repo entries now live per workspace in <workspace>/.agents/repos.yaml (see Per-Workspace Repo Manifests above); the central repositories: section is transitional and the scan's write target is the workspace manifest's fact fields.
Per-Repo Sync Procedure
IMPORTANT: All steps below must be executed as batched shell scripts, NOT individual tool calls per repo. See the Performance — Minimize Tool Calls section above.
Step 1: Fetch all — loop through every discovered repo and git fetch origin in a single script.
Step 2: Status + auto-actions — in a single script, loop through all repos:
- Get current branch, ahead/behind counts, uncommitted changes, stashes
- Pull if behind (automatic)
- Push if ahead and clean working tree (check push policy first —
pr-requiredrepos are reported, not pushed) - If push fails (non-fast-forward), report but do not force push
Step 3: Handle repos with uncommitted changes — show the user what's uncommitted (file list per repo) and ask whether to commit and push each dirty repo. Present a suggested commit message for each. Do NOT silently skip — uncommitted changes are unsynced state.
Safety Rules
- ALWAYS fetch first — run
git fetch originbefore checking any status - NEVER force push — always use regular
git push - Never sweep uncommitted work — ordinary sync reports dirty files. In explicit remote-handoff or day-end mode, commit only session-attributed paths authorized by the repository workflow. Exact private-context batching uses synthesis-repo-guard. Unattributed, overlapping, or policy-gated paths block REMOTE_READY.
- NEVER bypass hooks without explicit permission — if a pre-commit hook blocks a commit, STOP and ask the user. Never use
--no-verifyon your own. If the user approves bypassing for a specific commit, that approval does not extend to other commits - Sanitize ALL commit messages — never include people's names, company names, project codenames, article titles, or meeting topics in commit messages. Use generic descriptions like "Add meeting transcript", "Update context", "Add new blog post". Git history is persistent and can leak confidential information
- Push automatically if ahead — if working tree is clean and repo is ahead, push without asking
- Pull automatically if behind — pull new commits without asking
- Skip repos with no remote — report them but don't fail
- Skip repos with conflicts — report and let user resolve manually
- Skip repos mid-rebase/merge — report the state, don't interfere
- One branch only — only sync the current branch, don't switch branches
- Prompt only for diverged repos — when both ahead and behind, ask for merge strategy
Git Remote Sync Protocol
Remote configurations (name + URL pairs) are per-machine state stored in each repo's .git/config. Without explicit sync, a remote added on one Mac won't exist on the other.
v1.6.0: the reconciliation source for a repo's remotes is its workspace repos.yaml remotes: map (fact fields), not the central yaml.
Capture (during scan/refresh)
When refreshing git-repos.yaml, capture all remotes per repo:
# For each discovered repo:
git -C "$repo_path" remote -v | grep '(fetch)' | awk '{print $1, $2}'
CRITICAL: Use the Manifest Merge Protocol (below) to update git-repos.yaml. NEVER generate a fresh yaml from scratch and overwrite the existing file. The existing yaml may contain repos, remotes, categories, notes, and metadata contributed by other machines that this machine doesn't have locally. A destructive overwrite causes data loss.
This runs as part of the single batched scan script — not as separate tool calls.
Reconcile (during pull/sync)
When syncing to a Mac, reconcile local remotes against the manifest:
# For each repo in git-repos.yaml, for each remote in the manifest:
existing_url=$(git -C "$repo_path" remote get-url "$remote_name" 2>/dev/null)
if [ -z "$existing_url" ]; then
git -C "$repo_path" remote add "$remote_name" "$manifest_url"
echo "ADDED $repo_name: $remote_name → $manifest_url"
elif [ "$existing_url" != "$manifest_url" ]; then
git -C "$repo_path" remote set-url "$remote_name" "$manifest_url"
echo "UPDATED $repo_name: $remote_name → $manifest_url (was: $existing_url)"
fi
Execute this as a SINGLE batched script across all repos — not individual tool calls.
Safety Rules
- Never auto-remove remotes — if a local remote exists but isn't in the manifest, flag it in the summary but do not delete. The manifest captures the last scan from one machine; the local remote may be intentionally machine-specific.
- Never overwrite origin with empty — if the manifest has no
remotes:field for a repo, skip reconciliation for that repo. - Report all changes — every add/update goes in the sync summary.
- Credentials in URLs are expected — some remotes include usernames (e.g.,
user@bitbucket.org). These are not secrets (the password is in the credential helper, not the URL). Do not strip or redact them.
Relationship to setup-git-remotes.sh
If you maintain a setup-git-remotes.sh bootstrap script, it serves as a fallback for initial machine setup (before repos are cloned and before the first mac-sync). Once git-repos.yaml captures remotes, the yaml is the authoritative source of truth. Keep the bootstrap script consistent with the yaml, or auto-generate it from the yaml during push.
Manifest Merge Protocol
v1.6.0 scope note: with per-workspace repos.yaml files live, this protocol applies to machines.yaml and the transitional central yaml only — per-workspace manifests merge via git in their context repos.
CRITICAL SAFETY RULE: git-repos.yaml must NEVER be generated from scratch and overwritten. The yaml is a shared manifest across multiple machines. Each machine may have repos, remotes, or metadata that other machines don't. A destructive overwrite from one machine's scan silently destroys data contributed by other machines.
The merge procedure
When refreshing git-repos.yaml on any machine:
Read the existing yaml first. Parse all entries, their paths, categories, notes, remotes, and any other fields.
Scan the local machine for repos (
find ~/workspaces -maxdepth 3 -name ".git"). For each discovered repo, capture its remotes.Merge — additive updates only:
- Repo exists in yaml AND locally: Update remotes from local scan (add new remotes, update changed URLs). Preserve all existing fields (category, notes, etc.) unless the local scan has a reason to change them.
- Repo exists locally but NOT in yaml: Add it as a new entry.
- Repo exists in yaml but NOT locally: Keep it. This machine may not have it cloned. Do NOT remove it.
- Remote exists in yaml but NOT locally: Keep it. Another machine may have added it. Do NOT remove it.
Update metadata: Refresh the header comment (date, machine name). Recount total_repos as the number of entries in the merged yaml (not the local scan count).
Write the merged result.
What this prevents
- Repo discovered on Machine A doesn't disappear when Machine B runs a scan
- Remotes configured on Machine A survive Machine B's refresh
- Categories, notes, and other metadata contributed by any machine persist
- A machine running an older version of the skill that doesn't know about
remotes:doesn't strip the field
Never generate yaml from local scan alone
The correct mental model: the yaml is a multi-machine document that each machine contributes to. It is not a point-in-time capture of any single machine's state.
Automation Policy
Mac-sync is designed to run fully automated. The assistant should complete the entire sync without prompting, except in specific situations.
Automated (never prompt)
- Config files identical → skip silently
- Config file changed on one side only → copy the changed version
- Git fetch → always do
- Git pull when behind → always do (fast-forward only)
- Git push when ahead, clean working tree → always do
- Clean repos → skip silently
- Repos with no remote → skip, note in summary
- Manifest update → always refresh
- One-time actions for a different machine → skip silently
Must prompt (show details + ask for action)
- Repos with uncommitted changes → show file list per repo, suggest commit message, ask whether to commit+push. Uncommitted local changes won't reach the other Mac — treating them as informational breaks the sync guarantee
- Config file conflict — both sides changed, can't determine winner from timestamps
- Git repo diverged — both ahead of AND behind remote
- Git merge conflict during pull — automatic merge failed
- Repo in unexpected state — mid-rebase, mid-merge, or detached HEAD
- Auth failure across multiple repos — stop and alert
- Destructive action needed — force push, hard reset, or branch deletion
Report but take no action (never prompt)
- Repos with stashes → note in summary
- Non-fast-forward push failures → report, move on
- Repos with
push_policy: pr-required→ report unpushed commits
Machine Inventory
Use LocalHostName as the unique machine identifier:
scutil --get LocalHostName
Why LocalHostName:
- Set explicitly per machine in System Settings → Sharing → Local hostname
- Won't accidentally collide (unlike usernames)
- Persists across OS updates
- Easy to check
Machine Inventory Table Format
| LocalHostName | Model | Username | Notes |
|---------------|-------|----------|-------|
| my-mac-mini | Mac mini M2 Pro | alice | Primary development |
| my-macbook | MacBook Pro M4 | alice | Secondary laptop |
One-Time Actions
Machine-specific tasks that should run once per machine.
Safety Protocol
- Detect current machine:
scutil --get LocalHostName - Check eligibility: Each action has a
Target:field —machine-name,ALL, orALL EXCEPT machine-name - Execute or skip based on match
- Update status after execution:
machine-name [COMPLETED 2026-01-15] - Delete action only when ALL targeted machines show
[COMPLETED]
Template
### YYYY-MM-DD: Brief Description
**Target:** [LocalHostName(s) or ALL or ALL EXCEPT LocalHostName]
**Status:** [LocalHostName] [PENDING]
**Background:** Why this action is needed
**Action Required:**
\`\`\`bash
# Commands to run
\`\`\`
**Verification:** How to confirm it worked
Workspace Config Symlinks (v1.4.0)
For workspace-scoped agent configs that should be identical across all your Macs AND available to any agent tool (Claude Code, Codex, Cursor, etc.), the canonical source lives in the workspace-private repo at <workspace>/<workspace-private>/.agents/. mac-sync ensures the expected symlinks exist at the workspace level (and known repos) on every invocation.
Why this layer (vs iCloud-based config sync)
iCloud-based sync is the right pattern for global personal configs (.gitconfig, .zshrc, .codex/config.toml). Workspace-scoped agent configs are different:
- They're already in a git repo (the workspace-private repo) that pushes to GitHub — so cross-machine sync is "free" via git
- They're agent-tool-agnostic —
.agents/is the convention multiple tools should read from, not.claude/ - They're version-controlled with diff history per change (which iCloud sync isn't)
So: keep iCloud sync for global personal files; use git + symlinks for workspace agent configs.
Discovery
For each workspace under ~/workspaces/<workspace>/:
- Check if a workspace-private repo exists at
~/workspaces/<workspace>/ai-knowledge-<workspace>-<owner>-private/.agents/ - If yes, list all
*.yamland*.jsonfiles in that.agents/directory (skipREADME.mdand any non-config files) - For each canonical file: ensure the symlinks below exist
Symlinks to create per canonical file
| Symlink path | Target (relative) | Purpose |
|---|---|---|
<workspace>/.agents/<file> |
../<workspace-private>/.agents/<file> |
Agent-agnostic location (preferred) |
<workspace>/.claude/<file> |
../<workspace-private>/.agents/<file> |
Back-compat for tools that look in .claude/ |
<workspace>/.codex/<file> |
../<workspace-private>/.agents/<file> |
Back-compat for Codex |
All paths use relative symlinks so they work on any Mac regardless of ~/<username>.
Per-repo symlinks (conservative)
A canonical config may also need to appear inside a specific repo (e.g., a skill that walks up from CWD looking for .claude/<file>.yaml in the current repo first). The skill handles this conservatively — it does NOT auto-create per-repo symlinks for arbitrary repos. Instead:
- If a repo already has
.claude/<file>.yaml(or.agents/<file>.yaml) AS A REGULAR FILE, AND its content matches the canonical exactly: replace with a symlink to canonical. - If a repo already has
.claude/<file>.yamlAS A SYMLINK pointing to canonical: leave alone (already correct). - If a repo already has
.claude/<file>.yamlAS A SYMLINK pointing somewhere else, OR as a regular file with different content: WARN. Do not overwrite — manual decision. - If neither exists in the repo: do nothing. Per-repo symlinks for "new" repos are an explicit setup step, not auto-created.
Idempotence rules (apply to every symlink action)
| Current state at symlink path | Action |
|---|---|
| Symlink exists, points correctly | Skip silently |
| Symlink exists, points wrong | Replace with ln -sf |
| Regular file exists, content matches canonical | rm then create symlink |
| Regular file exists, content differs from canonical | WARN — do not overwrite |
| Nothing exists at the path | Create symlink |
| Parent directory doesn't exist | mkdir -p parent, then create symlink |
The skill must report what it did per file, not silently overwrite anything that diverged from canonical.
Bootstrap script (single batched bash call, per the Performance section)
Run this with bash -c '...' (not zsh) so shopt -s nullglob is available — that makes empty globs expand to nothing instead of throwing or expanding to the literal pattern.
shopt -s nullglob 2>/dev/null # or: [ -n "$ZSH_VERSION" ] && setopt NULL_GLOB
ensure_symlink() {
local link_path="$1" target_relative="$2"
local link_dir=$(dirname "$link_path")
mkdir -p "$link_dir"
if [ -L "$link_path" ]; then
local current_target=$(readlink "$link_path")
if [ "$current_target" = "$target_relative" ]; then
echo "OK (symlink correct): $link_path"
return
fi
echo "FIX (symlink wrong target): $link_path → $target_relative (was: $current_target)"
ln -sf "$target_relative" "$link_path"
return
fi
if [ -f "$link_path" ]; then
local canonical_abs="$link_dir/$target_relative"
if diff -q "$link_path" "$canonical_abs" > /dev/null 2>&1; then
echo "MIGRATE (file matches canonical, replacing with symlink): $link_path"
rm "$link_path"
ln -s "$target_relative" "$link_path"
return
fi
echo "WARN (regular file differs from canonical, NOT touching): $link_path"
return
fi
echo "CREATE: $link_path → $target_relative"
ln -s "$target_relative" "$link_path"
}
# Discover workspace-private repos and process each canonical file
for workspace_dir in "$HOME"/workspaces/*/; do
workspace=$(basename "$workspace_dir")
# Look for any ai-knowledge-<
…(truncated)