Skip to content
NewFinding Instagram reels in years of video, fully local on a Mac
Logbook
Nov 1, 2026·4 MIN READ
Moving 305 GitHub repos from an org to my own account

Moving 305 GitHub repos from an org to my own account

I moved almost every repo out of an org into my personal account, because I will look after them from now on. The only surprise was a redirect that looked like a name clash.

Today I moved 305 private repos from a GitHub org to my personal account. I will be the one looking after these projects in the future, so I want them in one place I own. In this note the org is example-org and my account is my-account. A few client repos and one website stay in the org.

The whole job was one shell script, a dry run, a pilot of three repos and one full run. Only one repo failed. Here is what I learned on the way.

The script

The script lists every repo in the org, filters out the ones that stay, and calls the transfer API for the rest. It is a dry run by default and only acts with --go.

transfer.sh
SRC=example-org; DST=my-account

gh repo list $SRC --limit 1000 --json name -q '.[].name' \
  | grep -vE '^(client-a-|client-b-|website$)' | sort > move.txt

while read -r repo; do
  # A GET on an old name follows the redirect, so only a matching full_name is a real clash.
  if [ "$(gh api "repos/$DST/$repo" --jq .full_name 2>/dev/null)" = "$DST/$repo" ]; then
    echo "SKIP $repo (exists on $DST)"; continue
  fi
  run gh api -X POST "repos/$SRC/$repo/transfer" -f new_owner=$DST
done < move.txt

The org had 321 repos. 306 of them were on the move list.

A redirect is not a clash

The first version of the check was simply "does my-account/<repo> answer?". The dry run then skipped one repo, saying it already existed on my account.

It did not. Some time ago that repo was moved from my account into the org. GitHub keeps the old name as a redirect, so a request for my-account/<repo> came back with example-org/<repo>. Same repo, same creation date. The check saw a 200 and called it a clash.

When you check if a repo name is free, compare full_name in the response with the name you asked for. A redirect answers with a different owner.

A real transfer replaces the redirect, so after the fix the repo moved like the others.

Pilot first, then everything

I moved three old repos by hand first and checked that they landed on my account, still private and not archived, and that the old org URLs redirect. Then I ran the full script. The website went from my account back to the org in the same run.

One repo failed with 422 Repository can't be transferred. It was a private fork, and its parent repo is not visible to me. I left it in the org.

What does not follow the repo

Repo secrets, webhooks and deploy keys move with the repo. Org level things do not: GitHub App installs (Vercel, Sentry, Codemagic, DigitalOcean and so on), org secrets and ghcr packages. Transfers also keep visibility as it was, so four repos that were public in the org stayed public. I made them private afterwards.

To see what would break, I scanned the 41 repos with a push in 2025 or 2026. I checked their workflow files and the check runs on the last commit. Three workflows still pushed images to ghcr.io/example-org/..., one app used Codemagic and two mentioned Sentry.

None of these projects is in active development right now, so I did not fix CI. I turned GitHub Actions off on all 305 repos instead. When I pick one up again, I turn Actions back on for that repo and fix its workflows then.

one repo
gh api -X PUT repos/my-account/<repo>/actions/permissions -F enabled=false

Private repo Actions minutes now count against my personal plan. With Actions off, a stray push to an old project cannot run a workflow and use them.

Last step: five local clones still pointed at the org. git remote set-url fixed them. The redirect would work for now, but it breaks the day someone creates a repo with the same name in the org.

What it took

Most of the time was waiting for the API, because the script sleeps two seconds between transfers. If I did it again, I would write the redirect check right from the start, and I would turn off Actions before the move instead of after it. I would also check the GitHub token scopes up front: reading org secrets and packages needs admin:org and read:packages.