sync-repos
Skill0 starsChanged 2 months agoMITView on GitHub
Use when the user wants to update or sync many local git repositories at once — "pull latest for all repos", "sync all repos from main", "walk through the repos and pull", "update all my git repos", "fetch all clones", or bulk fast-forward across a folder of git checkouts. Non-destructive, fast-forward-only; never force/reset/stash/push.
3 other projects use a version of this.
Adopted by
Most recently taken up by
- egregore-labs/egregoretheir own version, first seen Nov 6, 2026
- codemonkey85/PKMDS-Blazortheir own version, first seen Oct 8, 2026
- SmartAssets-io/skillstheir own version, first seen Aug 12, 2026
Other versions3 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 3
- egregore-labs/egregore★ 293same name
- codemonkey85/PKMDS-Blazor★ 240same name
- SmartAssets-io/skills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository5 other files it gives its agents.
- ado-pr-build-monitorSkill
- adversarial-reviewSkill
- plan-exit-reviewSkill
- plan-mega-reviewSkill
- project-roomSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
14 rules. 0 are common, 8 are distinctive, and 6 are very specific to this project.
0
Common
Most of the 3 edited versions we read keep these.
8
Distinctive
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Update a folder full of git clones in one pass: fetch each, fast-forward the selected default or current branch when safe, then report …
- Distinctive: line 23
Do not use this to reconcile a fork with heavy local divergence, resolve merge conflicts, or rebase — those need a per-repo interactive …
- Distinctive: line 33
If none of these clearly applies, ask the user for the root before scanning.
- Very specific: line 51
Read local state and fetch only needed remotes: Read the current branch, HEAD commit, and dirty flag (git status --porcelain).
- Very specific: line 62
Do not use fetch --all: it exits non-zero when *any* remote fails, so a single unrelated broken remote would mark a perfectly healthy repo …
- Distinctive: line 85
Fast-forward safely: Every result must be distinguishable — always capture the branch tip before and after the operation and derive the …
- Distinctive: line 90
After remote reads, require the checkout branch, HEAD, and porcelain state to match the captured values before reporting checkout-dependent …
- Very specific: line 109
Classify history before updating: Use git merge-base --is-ancestor in both directions.
- Very specific: line 115
Never: run git reset, git checkout -f, git stash, git rebase, or any push.
- Very specific: line 123
When the default branch was updated while another branch is checked out, suffix (on <current>).
- Distinctive: line 268
Prefer running the loop and then presenting a clean table to the user rather than dumping raw tab output.
- Very specific: line 290
Stale/missing
origin/HEAD: use the server's advertised symbolic HEAD, not the cached alias. - Distinctive: line 296
Do not remove lock files or suggest a manual merge for non-divergent history.
- Distinctive: line 301
Auth prompts: if a fetch would block on credentials, record it as an error and move on — never hang the whole run on one repo.
“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Sync Repos — bulk fetch + fast-forward across many local clones
Update a folder full of git clones in one pass: fetch each, fast-forward the selected default or current branch when safe, then report exactly what advanced, what was skipped, and what needs attention.
This is deliberately safe. It only ever runs git fetch and
git merge --ff-only / git pull --ff-only. It never force-pushes, resets,
checks out over dirty state, stashes, or discards anything. Repos with local
changes or diverged history are reported, not touched.
When to use
- "walk through the repos and pull latest" / "sync all repos from main"
- "update all my git repos" / "fetch everything under ~/git"
- Any request to refresh a directory of clones before starting work.
Do not use this to reconcile a fork with heavy local divergence, resolve merge conflicts, or rebase — those need a per-repo interactive decision. Flag such repos in the report and stop.
Inputs
- root (optional): directory to scan. Default order:
- An explicit path the user gave.
- The current working directory if it contains 2+ git clones.
~/gitas a fallback. If none of these clearly applies, ask the user for the root before scanning.
- scope (optional):
default-branch(default) orcurrent-branch.default-branch— update each repo's main/default branch (matches "sync from main"). Does not switch away from a dirty or feature branch.current-branch— fast-forward whatever branch is checked out (matches "pull latest").
Procedure
- Discover clones. A clone is
rootitself, or a direct child ofroot, that contains a.gitdirectory — nothing deeper is scanned. Linked worktrees and submodules, where.gitis a file, are skipped (see Edge cases), andnode_modulesis pruned explicitly because a depth cap alone still matches<root>/node_modules/.git. Pointing at a single clone therefore syncs that clone; pointing at a folder of clones syncs each of them. Ifrootis itself a clone and holds nested clones, both levels are in scope and every one of them appears in the report — nothing is updated invisibly, and every update is still fast-forward-only. - Read local state and fetch only needed remotes. Read the current branch,
HEAD commit, and dirty flag (
git status --porcelain). Confirm anoriginremote exists. Everything here resolves throughorigin, so checkgit remote get-url origin: a clone whose remote is namedupstreamwould otherwise pass a bare "some remote exists" guard and then be misreported asno default branch. Thengit fetch --prune --quiet origin, plus the current branch's tracking remote when needed forcurrent-branchscope or a dirty checkout's behind-count. Readbranch.<current>.remotedirectly: remote names can contain/, and.means a local upstream branch, not a remote to fetch. A cleandefault-branchupdate does not need the feature branch's remote. Do not usefetch --all: it exits non-zero when any remote fails, so a single unrelated broken remote would mark a perfectly healthy repoerror: fetch failed. Recorderror: no origin remotewhenoriginis missing, anderror: fetch failedwhen a remote this run genuinely needs is unreachable. - Resolve the server's default branch when needed, using
git ls-remote --symref origin HEADafter fetching. Cachedorigin/HEADcan retain the old default after a rename; do not guess frommainormaster. Record both its branch name and advertised HEAD commit for verification. Missing symbolic HEAD meansno default branch; a lookup failure or an unfetched advertised branch is an explicit error. A cleancurrent-branchupdate does not depend on discovering a default branch. - Verify the selected commit before using it. Resolve the selected
tracking ref to a commit. For the default branch, compare it with the
advertised HEAD commit. For a configured remote upstream, compare it with
the exact
branch.<current>.mergeref returned bygit ls-remote --exit-code <tracking-remote> <merge-ref>. A local upstream (branch.<current>.remote = .) needs no remote check. Preserve configured fetch refspecs: never override an exclusion or fetch an excluded branch separately to make verification pass. A missing, unmapped, stale, or unverifiable selected ref iserror: <reason>, before any update or behind-count. Use the verified commit ID for every later count, ancestry check, and update, not a tracking ref that could move. - Fast-forward safely. Every result must be distinguishable — always capture
the branch tip before and after the operation and derive the result
from the difference. Never report success on exit code alone:
merge --ff-onlyandfetch <b>:<b>both exit 0 when nothing moved, so an exit-code-only check cannot telladvancedfromup-to-date. After remote reads, require the checkout branch, HEAD, and porcelain state to match the captured values before reporting checkout-dependent results. Recheck that same condition immediately before any branch update; only initially clean checkouts reach an update. A mismatch ischeckout changed (skipped)without retrying that repo.- Dirty working tree → do not pull. Record
dirty (skipped), N behind, counting withgit rev-list --count <captured-HEAD>..<verified-commit>for its upstream, or the default branch when no upstream is configured. Report a verification/counting failure as an error rather than inventing a count. scope=current-branch: if the current branch has no upstream, recordno upstream (skipped)— do not silently fall back to the default branch. Otherwise fast-forward to the fetched upstream commit.scope=default-branch, default branch is checked out: fast-forward to the fetchedorigin/<default>commit.scope=default-branch, default branch is not checked out: update it without checkout by fetching the verified commit from the local repository (git fetch . <verified-commit>:refs/heads/<default>). This avoids a second network fetch racing the ancestry check. A branch checked out in another worktree isin use by another worktree (skipped).- Classify history before updating. Use
git merge-base --is-ancestorin both directions. If the target already contains the upstream commit, reportup-to-dateand retain local commits. Only two non-ancestor tips meandiverged. An ancestry error, lock failure, permission failure, or failed update iserror: <reason>, with diagnostic stderr preserved — never infer divergence from an operation's exit code alone.
- Dirty working tree → do not pull. Record
- Never run
git reset,git checkout -f,git stash,git rebase, or any push. Never pass--force. Report (table + one-line summary). The full result vocabulary is:
advanced N commits·up-to-date·created local <default>·dirty (skipped), N behind·diverged (needs manual merge)·in use by another worktree (skipped)·detached (skipped)·no upstream (skipped)·checkout changed (skipped)·no default branch·error: <reason>. When the default branch was updated while another branch is checked out, suffix(on <current>).repo branch result api-service main advanced 6 commits web-client feature/x dirty (skipped), 2 behind shared-lib main up-to-date infra-tools main diverged (needs manual merge) legacy-svc main error: fetch failed End with:
N repos: X advanced, Y up-to-date, Z skipped, W diverged, V errors.List the diverged/error/dirty repos again as an explicit "needs attention" line so nothing important scrolls off.
Ready-to-run reference
Adapt the root as needed. This is read-only except for fetch + ff-only.
ROOT="${1:-$PWD}"; SCOPE="${2:-default-branch}"
# Never block on a credential prompt — one private repo must not hang the run.
export GIT_TERMINAL_PROMPT=0
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes}"
export LC_ALL=C
case "$SCOPE" in default-branch|current-branch) ;;
*) printf 'error: unsupported scope: %s\n' "$SCOPE" >&2; exit 2;; esac
r() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; }
error() {
r "$name" "$target" "error: $1"
[ -z "$2" ] || printf '%s: %s\n' "$name" "$2" >&2
}
checkout_unchanged() {
local branch head state
if branch="$(git -C "$repo" branch --show-current)"; then :;
else error "cannot revalidate checkout branch" ""; return 1; fi
if head="$(git -C "$repo" rev-parse --verify HEAD)"; then :;
else error "cannot revalidate checkout HEAD" ""; return 1; fi
if state="$(git -C "$repo" status --porcelain)"; then :;
else error "cannot revalidate working tree" ""; return 1; fi
if [ "$branch" != "$cur" ] || [ "$head" != "$initial_head" ] || [ "$state" != "$dirty" ]; then
r "$name" "$target" "checkout changed (skipped)"; return 1
fi
return 0
}
find "$ROOT" -maxdepth 2 -name node_modules -prune -o -name .git -type d -print | while IFS= read -r g; do
repo="$(dirname "$g")"; name="$(basename "$repo")"
cur="$(git -C "$repo" branch --show-current 2>/dev/null)"
[ -z "$cur" ] && { r "$name" "detached" "detached (skipped)"; continue; }
target="$cur"
git -C "$repo" remote get-url origin >/dev/null 2>&1 || { r "$name" "$cur" "error: no origin remote"; continue; }
if initial_head="$(git -C "$repo" rev-parse --verify HEAD)"; then :;
else error "cannot read checkout HEAD" ""; continue; fi
# Git diagnostics stay on stderr, never inside values parsed as data.
if dirty="$(git -C "$repo" status --porcelain)"; then :;
else error "cannot read working-tree state" ""; continue; fi
if err="$(git -C "$repo" fetch --prune --quiet origin 2>&1)"; then :;
else error "fetch failed (origin)" "$err"; continue; fi
upr="$(git -C "$repo" config --get "branch.$cur.remote")"
if { [ "$SCOPE" = current-branch ] || [ -n "$dirty" ]; } &&
[ -n "$upr" ] && [ "$upr" != origin ] && [ "$upr" != . ]; then
if err="$(git -C "$repo" fetch --prune --quiet "$upr" 2>&1)"; then :;
else error "fetch failed ($upr)" "$err"; continue; fi
fi
upstream="$(git -C "$repo" rev-parse --verify --quiet --symbolic-full-name '@{u}' 2>/dev/null)"
merge_ref="$(git -C "$repo" config --get "branch.$cur.merge")"
if { [ "$SCOPE" = current-branch ] || [ -n "$dirty" ]; } &&
[ -z "$upstream" ] && { [ -n "$upr" ] || [ -n "$merge_ref" ]; }; then
error "configured upstream ref unavailable (check fetch filters)" ""; continue
fi
ref="$upstream"; remote="$upr"; expected=""
if { [ "$SCOPE" = default-branch ] && [ -z "$dirty" ]; } ||
{ [ -n "$dirty" ] && [ -z "$upstream" ]; }; then
if advertised="$(git -C "$repo" ls-remote --symref origin HEAD)"; then :;
else error "default branch lookup failed" ""; continue; fi
def="$(printf '%s\n' "$advertised" | sed -n 's#^ref: refs/heads/\([^[:space:]]*\)[[:space:]]HEAD$#\1#p')"
[ -n "$def" ] || { r "$name" "$cur" "no default branch"; continue; }
ref="refs/remotes/origin/$def"; remote=origin
expected="$(printf '%s\n' "$advertised" | awk '$2 == "HEAD" {print $1}')"
[ -n "$dirty" ] || target="$def"
else
[ -n "$ref" ] || { r "$name" "$cur" "no upstream (skipped)"; continue; }
if [ "$remote" != . ]; then
[ -n "$remote" ] && [ -n "$merge_ref" ] ||
{ error "cannot identify configured upstream" ""; continue; }
if advertised="$(git -C "$repo" ls-remote --exit-code "$remote" "$merge_ref")"; then :;
else error "upstream lookup failed ($remote $merge_ref)" ""; continue; fi
expected="$(printf '%s\n' "$advertised" | awk -v ref="$merge_ref" '$2 == ref {print $1}')"
fi
fi
if desired="$(git -C "$repo" rev-parse --verify "$ref^{commit}")"; then :;
else error "selected ref unavailable ($ref); check fetch filters" ""; continue; fi
if [ "$remote" != . ]; then
[ -n "$expected" ] || { error "selected remote commit was not advertised" ""; continue; }
[ "$desired" = "$expected" ] ||
{ error "stale tracking ref ($ref); check fetch filters or retry" ""; continue; }
fi
checkout_unchanged || continue
if [ -n "$dirty" ]; then
if behind="$(git -C "$repo" rev-list --count "$initial_head..$desired")"; then
r "$name" "$cur" "dirty (skipped), $behind behind"
else error "cannot count commits behind" ""; fi
continue
fi
suffix=""; [ "$target" = "$cur" ] || suffix=" (on $cur)"
if [ "$target" = "$cur" ]; then before="$initial_head"
else before="$(git -C "$repo" rev-parse --verify --quiet "refs/heads/$target")"; fi
if [ -n "$before" ]; then
if git -C "$repo" merge-base --is-ancestor "$desired" "$before"; then
r "$name" "$target" "up-to-date$suffix"; continue
else
rc=$?; [ "$rc" -eq 1 ] || { error "cannot compare history" ""; continue; }
fi
if git -C "$repo" merge-base --is-ancestor "$before" "$desired"; then :;
else
rc=$?
if [ "$rc" -eq 1 ]; then r "$name" "$target" "diverged (needs manual merge)"
else error "cannot compare history" ""; fi
continue
fi
fi
checkout_unchanged || continue
if [ "$target" = "$cur" ]; then
if err="$(git -C "$repo" merge --ff-only "$desired" --quiet 2>&1)"; then :;
else error "merge failed" "$err"; continue; fi
else
if err="$(git -C "$repo" fetch --quiet . "$desired:refs/heads/$target" 2>&1)"; then :;
else
case "$err" in
*"checked out at"*|*"current branch"*) r "$name" "$target" "in use by another worktree (skipped)";;
*) error "local branch update failed" "$err";;
esac
continue
fi
fi
if after="$(git -C "$repo" rev-parse --verify "refs/heads/$target")"; then :;
else error "cannot read updated branch" ""; continue; fi
if [ -z "$before" ]; then r "$name" "$target" "created local $target$suffix"
elif [ "$before" = "$after" ]; then r "$name" "$target" "up-to-date$suffix"
elif count="$(git -C "$repo" rev-list --count "$before..$after")"; then
r "$name" "$target" "advanced $count commits$suffix"
else error "cannot count advanced commits" ""; fi
donePrefer running the loop and then presenting a clean table to the user rather than dumping raw tab output. If there are many repos, run the fetches and summarize; do not narrate each repo.
Edge cases
- Worktrees / submodules: discovery matches only a
.gitdirectory, so linked worktrees and submodules (where.gitis a file) are skipped by construction. Do not auto-update submodules unless asked. - Branch checked out elsewhere: a needed local ref update refuses when the
branch is checked out in another worktree. Report
in use by another worktree (skipped), notdiverged. If the branch already contains the upstream tip, no update is needed and it isup-to-date. - No
originremote: everything here resolves throughorigin, so a repo whose only remote is named something else (upstreamon a fork, a renamed remote) must be reported, not silently misread asno default branch. Guard withgit remote get-url origin. Reporterror: no origin remote. - Several remotes: fetch only
originand, when needed for a current-branch merge or dirty behind-count, the configured tracking remote..is local; names containing/are used intact.fetch --allcouples success to remotes it never reads — one broken remote fails the whole fetch and the repo is reported aserror: fetch failedwhileoriginis perfectly healthy. - Stale/missing
origin/HEAD: use the server's advertised symbolic HEAD, not the cached alias. Missing server HEAD isno default branch; a network failure is an error. Current-branch scope needs neither when clean. - Ahead-only history: preserve the extra local commits and report
up-to-date— nothing from the upstream is missing. - Locks or permissions: report the operational error and its diagnostic. Do not remove lock files or suggest a manual merge for non-divergent history.
- Detached HEAD: report
detached (skipped), never fast-forward. - No upstream (current-branch scope): report
no upstream (skipped)rather than quietly switching to the default branch — a silent scope change is worse than a skip. - Auth prompts: if a fetch would block on credentials, record it as an error and move on — never hang the whole run on one repo.
- Large trees: cap discovery depth at 2; if the user points at a huge root, confirm scope before scanning thousands of directories.
Stop condition
Done when every discovered repo has a recorded result and the summary line is produced. Anything requiring a human decision (diverged, dirty with important changes, auth error) is surfaced in the "needs attention" line — do not attempt to resolve it automatically.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

