Skip to content

gh repo: Managing Repositories from the Terminal

Lesson 3 of 10Beginner → Intermediate10 min readGitHub Engineering · GitHub CLIVerified: gh 2.98.0 on Ubuntu 24.04, August 2026

gh repo covers the operations you would otherwise perform on a repository’s settings pages, plus cloning and forking.

Most are unremarkable. Two — delete and archive — deserve care, and one, set-default, prevents a whole class of confusion in repositories with multiple remotes.

Run gh repo --help for the authoritative list on your version. The ones worth knowing:

CommandDoes
createCreate a repository, optionally from a local directory
cloneClone, with fork remotes configured automatically
forkFork, optionally cloning and adding upstream
viewShow a repository, or fields of it as JSON
listList repositories for a user or organisation
editChange settings — description, visibility, features
syncBring a fork up to date with its upstream
renameRename a repository
archive / unarchiveMake read-only, or restore
deleteDelete permanently
set-defaultChoose which remote gh targets in this directory
gitignore / licenseList and view templates
Terminal window
gh repo create my-project --public --add-readme --license mit --clone
gh repo create my-project --private --source=. --remote=origin --push
gh repo create my-org/service --internal --description "Shared tooling"
gh repo create my-service --template my-org/service-template --private --clone

The second form is the one to remember: it publishes a project that already exists locally, creating the repository, wiring the remote, and pushing in a single command.

Terminal window
gh repo clone cli/cli
gh repo clone OWNER/REPO -- --depth=1
gh repo fork OWNER/REPO --clone

Arguments after -- are passed to git clone, so shallow clones and other Git options work normally.

gh repo fork --clone does four things at once: forks, clones your fork, sets origin to it, and adds upstream pointing at the original. That naming convention is the one Forks relies on.

Viewing and listing with structured output

Section titled “Viewing and listing with structured output”

This is where gh repo becomes useful for more than convenience.

Terminal window
gh repo view OWNER/REPO --json name,visibility,defaultBranchRef,licenseInfo,isArchived

What it doesPrints selected repository fields as JSON rather than a rendered page.

Why we run itStructured output is what makes gh scriptable. The same command with no --json prints a README preview, which is useless to a script and fine for a human.

Expected resultA JSON object with exactly the requested fields.

Output:

{
"defaultBranchRef": { "name": "main" },
"isArchived": false,
"licenseInfo": { "key": "mit" },
"name": "REPO",
"visibility": "PUBLIC"
}

Listing supports the same, plus filters that make inventory queries trivial:

Terminal window
gh repo list OWNER --limit 200 --json nameWithOwner,visibility,pushedAt
gh repo list OWNER --visibility public --no-archived --json nameWithOwner
gh repo list OWNER --fork --json nameWithOwner,parent
gh repo list OWNER --language go --limit 50

A genuinely useful audit — public repositories not touched in a long time:

Terminal window
gh repo list OWNER --visibility public --no-archived --limit 200 \
--json nameWithOwner,pushedAt \
--jq '.[] | select(.pushedAt < "2025-08-01") | .nameWithOwner'
Terminal window
gh repo edit --description "A better description"
gh repo edit OWNER/REPO --visibility private
gh repo edit OWNER/REPO --enable-wiki=false --enable-projects=false
gh repo edit OWNER/REPO --enable-discussions
gh repo edit OWNER/REPO --default-branch main
gh repo edit OWNER/REPO --delete-branch-on-merge

--delete-branch-on-merge is worth setting on every repository you create. Stale merged branches accumulate quickly and make the branch list unusable.

Terminal window
gh repo sync
gh repo sync OWNER/FORK --branch main
gh repo sync --source UPSTREAM_OWNER/REPO --branch main

Run inside a fork’s clone, gh repo sync updates it from upstream. It refuses when histories have diverged, which is the correct behaviour — a fork’s main that has its own commits needs a decision, not a silent merge.

Three operations with increasing consequence.

Rename. GitHub redirects the old URL, but other people’s configured remotes, CI references and package names do not follow.

Terminal window
gh repo rename new-name
gh repo rename new-name --repo OWNER/OLD-NAME

Archive. Makes a repository read-only: no pushes, no new Issues or pull requests, everything still readable. This is the right end state for a project that is finished rather than abandoned — it signals the state honestly and prevents the appearance of an unmaintained-but-open tracker.

Terminal window
gh repo archive OWNER/REPO
gh repo unarchive OWNER/REPO

Delete. Permanent.

Terminal window
gh repo delete OWNER/REPO # prompts for confirmation

Deleting also requires the delete_repo scope, which gh auth login does not grant by default — a deliberate piece of friction. gh auth refresh --scopes delete_repo adds it.

In a repository with several remotes — typically a fork, with origin and upstreamgh cannot know which one you mean. It asks, once, and remembers:

Terminal window
gh repo set-default
gh repo set-default OWNER/REPO

Without this, gh pr list in a fork may show pull requests from the wrong repository, which is confusing precisely because it looks like it worked.

gh repo list with --json turns repository management from clicking through settings pages into answering questions.

Which repositories are public that should not be?

Terminal window
gh repo list ORG --visibility public --no-archived --limit 300 \
--json nameWithOwner,description,pushedAt \
--jq '.[] | [.nameWithOwner, .pushedAt] | @tsv'

Run this quarterly. Repositories become public by accident more often than anyone expects, usually during a migration or a hurried “let’s just open-source it” decision that was never reviewed.

Which are unmaintained but still open?

Terminal window
gh repo list ORG --no-archived --limit 300 \
--json nameWithOwner,pushedAt,isFork \
--jq --arg cutoff "$(date -u -d '18 months ago' +%Y-%m-%d)" \
'.[] | select(.isFork | not) | select(.pushedAt < $cutoff) | [.pushedAt[:10], .nameWithOwner] | @tsv' \
| sort

Anything on that list should probably be archived. An unmaintained repository with an open Issue tracker invites reports nobody will read, which is worse for the reporter than an honest read-only archive.

Which have no licence?

Terminal window
gh repo list ORG --visibility public --limit 300 \
--json nameWithOwner,licenseInfo \
--jq '.[] | select(.licenseInfo == null) | .nameWithOwner'

Public with no licence means nobody may legally use the code — a state that is almost never intentional.

Which are missing basic hygiene settings?

Terminal window
gh repo list ORG --no-archived --limit 300 \
--json nameWithOwner,deleteBranchOnMerge,hasWikiEnabled,hasIssuesEnabled \
--jq '.[] | select(.deleteBranchOnMerge | not) | .nameWithOwner'

Each of these is one command and answers a question that would otherwise take an afternoon of clicking.

Cloning an organisation’s repositories is a common setup task and a common way to accidentally download a hundred gigabytes.

Terminal window
gh repo list ORG --limit 200 --no-archived --source --json nameWithOwner \
--jq '.[].nameWithOwner' \
| while read -r repo; do
if [ -d "$(basename "$repo")" ]; then
printf 'skipping %s (already cloned)\n' "$repo"
else
gh repo clone "$repo" -- --filter=blob:none
fi
done

Two things make this survivable. --source excludes forks. And --filter=blob:none is a partial clone — it fetches history and trees but downloads file contents on demand, which for a large organisation is the difference between minutes and hours.

For genuinely large monorepos, combine it with sparse checkout so you materialise only the directories you work in.

gh repo edit covers most of what you would otherwise click through. The settings worth deciding deliberately on any repository people collaborate in:

Terminal window
gh repo edit OWNER/REPO \
--enable-issues \
--enable-wiki=false \
--enable-projects=false \
--delete-branch-on-merge \
--allow-update-branch \
--enable-squash-merge \
--enable-merge-commit=false \
--enable-rebase-merge=false

That combination expresses a specific opinion: pull requests squash-merge, branches clean themselves up, and the wiki is off because documentation lives with the code. It suits many teams and not all — the point is that these are decisions, and leaving them at defaults is also a decision, just an unexamined one.

Restricting merge methods is more consequential than it looks. Allowing only squash means your main history is one commit per pull request, permanently — the trade-offs are in Squash Merging.

Topics are the tags shown on a repository page and used by GitHub’s search.

Terminal window
gh api repos/OWNER/REPO/topics -H "Accept: application/vnd.github+json" --jq '.names'
gh api --method PUT repos/OWNER/REPO/topics \
-H "Accept: application/vnd.github+json" \
--input - <<'JSON'
{"names": ["python", "cli", "automation"]}
JSON

Note this endpoint replaces the full list rather than appending, so read before writing if you are adding one.

For a public repository, topics plus a clear description are most of what determines whether anyone finds the project. They are worth two minutes and are usually skipped entirely.

Terminal window
gh api --method POST repos/OWNER/REPO/transfer -f new_owner=NEW-OWNER

Transfer moves a repository between accounts or organisations. GitHub redirects the old URL, and Issues, pull requests, releases and wiki content move with it.

What does not move cleanly: collaborator permissions are re-derived from the new owner’s model, webhook secrets may need reconfiguring, and anything referencing the repository by its old path through the API needs updating. Treat it as a migration with a checklist rather than a setting change.

--yes on delete in a script. One bad variable is an unrecoverable deletion.

Deleting when archiving was meant. Archive preserves everything and is reversible.

Renaming without checking dependants. Redirects cover URLs, not everything else.

Not setting a default in a fork. gh targets the wrong repository silently.

Parsing gh repo list output. Use --json.

Forgetting --delete-branch-on-merge at creation. Cheap then, tedious later.

“Could not resolve to a Repository.” Either the name is wrong or your token cannot see it. Those are indistinguishable from the outside, by design — GitHub does not disclose the existence of private repositories to unauthorised callers. Check gh auth status for the active account before assuming the name is wrong.

gh repo clone produces an HTTPS URL when you expected SSH. The protocol comes from your gh configuration, not from the repository. Change it with gh config set git_protocol ssh, or per clone by passing the URL form you want.

Commands operate on the wrong repository. You are in a directory whose remotes point elsewhere, or a fork without a default set. gh repo set-default fixes the second; being explicit with --repo fixes both, and is what scripts should do regardless.

gh repo delete fails with a permissions error. The delete_repo scope is not granted by default. gh auth refresh --scopes delete_repo adds it — deliberate friction on an unrecoverable operation.

Settings changes silently do nothing. Some settings are constrained by organisation policy. The API accepts the request and the policy overrides it; reading the value back is the only way to confirm it took effect. This is a good argument for the read-after-write habit in any configuration sweep.

A rename appears to break clones. GitHub redirects the old URL for Git operations, so existing clones keep working — but anything referencing the repository through the API by its old path does not follow. Update remotes explicitly with git remote set-url rather than relying on the redirect indefinitely.

gh repo covers the common operations. The API covers everything, and there is a reasonable dividing line.

Use gh repoUse gh api / the REST API
Create, clone, fork, view, listTopics, transfer, autolinks, deploy keys
Edit the settings it exposesSettings with no flag
Interactive and scripted workAnything needing a field the porcelain omits
Anything where you want validationBulk operations where you control the loop

The practical test: if gh repo edit --help has a flag for it, use the flag — it is validated, readable and stable. If it does not, gh api reaches the same endpoint with no guard rails, which is covered in gh api.

A worked example of crossing that line — enabling automated security fixes, which has no porcelain flag:

Terminal window
gh api --method PUT repos/OWNER/REPO/automated-security-fixes
gh api --method PUT repos/OWNER/REPO/vulnerability-alerts

Both return 204 No Content on success, which gh api prints as nothing at all. Silence is success here, and a script checking for output rather than exit status will report a false failure.

  1. Create a repository with gh repo create --private --add-readme --clone.
  2. Inspect it with gh repo view --json name,visibility,defaultBranchRef.
  3. Change the description and disable the wiki with gh repo edit, then re-inspect.
  4. Fork a public repository with gh repo fork --clone and confirm origin and upstream with git remote -v.
  5. Run gh repo set-default in the fork and note what it asks.
  6. Archive your practice repository, confirm pushes are refused, then unarchive and delete it.

Step 6 demonstrates the difference between archive and delete more convincingly than any description.

  • gh repo create --source=. --push publishes an existing local project in one command.
  • gh repo fork --clone sets up the origin/upstream convention automatically.
  • --json with no value lists available fields for the version you have installed.
  • --delete-branch-on-merge is worth enabling on every new repository.
  • Archive is reversible and preserves everything; delete is neither.
  • delete_repo is not granted by default, which is deliberate friction.
  • set-default prevents gh targeting the wrong remote in a fork.

The commands in this lesson combine into something worth keeping — a script that creates a repository configured the way your team expects, rather than the way GitHub defaults.

#!/usr/bin/env bash
set -euo pipefail
NAME="${1:?usage: new-repo.sh NAME [TEAM]}"
TEAM="${2:-}"
ORG="${ORG:-acme}"
gh repo create "$ORG/$NAME" \
--private \
--template "$ORG/service-template" \
--description "TODO: describe this repository" \
--clone
gh repo edit "$ORG/$NAME" \
--delete-branch-on-merge \
--enable-wiki=false \
--enable-projects=false
gh label clone "$ORG/service-template" --repo "$ORG/$NAME"
if [ -n "$TEAM" ]; then
gh api --method PUT "orgs/$ORG/teams/$TEAM/repos/$ORG/$NAME" -f permission=push
fi
echo "created $ORG/$NAME"

Four decisions encoded once rather than remembered each time: private by default, a consistent skeleton from a template, branches that clean themselves up, and access granted to a team rather than to individuals.

The description placeholder is deliberate — it is visible and slightly annoying, which is more likely to get a real description written than an empty field is.

One gh repo subcommand has no counterpart elsewhere in this pillar: autolink, which turns references to an external tracker into clickable links throughout the repository.

Terminal window
gh repo autolink list
gh repo autolink create "JIRA-" "https://acme.atlassian.net/browse/JIRA-<num>"
gh repo autolink create "INC-" "https://acme.pagerduty.com/incidents/<num>" --numeric
gh repo autolink delete AUTOLINK_ID

With the first configured, writing JIRA-1234 in a commit message, an Issue, a pull request description or a comment renders as a link to that ticket. Nothing is stored — the rendering is computed from the prefix.

This is worth setting up on any repository whose work is tracked somewhere other than GitHub Issues. It costs one command and removes the friction of people pasting full URLs, which they mostly do not, leaving bare ticket numbers that later readers must look up by hand.

The --numeric flag constrains the suffix to digits; without it, alphanumeric suffixes match, which suits systems using non-numeric identifiers.

Autolinks are per repository, so an organisation standardising on one tracker will want this in its provisioning script alongside the label scheme and the branch protection.

Professional ToolkitThe gh api recipes and the PR triage and release-notes scripts are in the Professional Toolkit.