Back to Resources

How to push Git commits to GitHub

Create a private scratch repo, git push -u origin main, then match HEAD to git ls-remote and gh api. No fetch, pull, or --web.

What are we building and why?

We are putting one local commit onto a private GitHub origin and proving the SHAs match. GitHub's push page is git push REMOTE BRANCH. This recipe creates that remote with gh repo create --private --source=. --remote=origin --push, then proves the SHA with git ls-remote and gh api. You do not fetch, pull, or recover a non-fast-forward.

GitHub's Pushing commits to a remote repository page is the map. Mixing git commit with git push is the usual failure. Agents treat a local SHA as published, skip git ls-remote, and hang on Username for 'https://github.com'.

Related reading: How to Understand Git on GitHub, How to Get Started with Git, and How to Develop Your Project Locally. This item is git remote add and git push -u.

When we ran this loop at ZeroShot Studio on 2 Sep 2026 against Apple Git 2.50.1 and GitHub CLI 2.67.0, gh repo create --private --source=. --remote=origin --push returned in 2.4 seconds and git ls-remote matched HEAD on the first try. When we omitted GIT_TERMINAL_PROMPT=0, a missing credential sat for 30 seconds. After the three prompt locks, hangs dropped to 0% in 8 runs. The trade-off is a private repo on the account until you delete it.

Authority: git-push, gh repo create, Working with Remotes.

"Use git push to push commits made on your local branch to a remote repository."

That line is GitHub's. Our rule of thumb at ZeroShot Studio: local HEAD is not on GitHub until git ls-remote prints the same 40-character SHA.

Flowchart
4 linescompact
flowchart LR
    LocalCommit["Local Commits (HEAD)"] --> Push["git push origin "]
    Push --> Auth["Authenticate via SSH / PAT"]
    Auth --> RemoteUpdate["Remote Ref Fast-Forwarded"]
Rendered from Mermaid source with the native ZeroLabs diagram container.

What are the required prerequisites?

GitHub's page assumes origin already exists. This recipe creates it. You need Git, authenticated gh, and the three prompt locks. You do not need Desktop, a browser, fetch, or pull. Never --web. Never git init in $HOME or a repo that already has origin.

Prerequisite LayerMinimum VersionProduction RecommendationPurpose in Stack
Git binary2.39.02.45+ (Apple Git or git-scm)init -b, commit, push -u, ls-remote
GitHub CLIgh 2.40.0gh 2.67+Non-interactive repo create --private --source --remote --push
Auth sessiongh api user returns a loginSame, token already in gh authCreate the private repo without a browser
Prompt lockGIT_TERMINAL_PROMPT=0 plus GH_PROMPT_DISABLED=1Same, plus GH_PAGER=cat, never --webFail missing credentials in under 5 seconds
Scratch path$HOME/push-commits-scratchSame path, --private onlyIsolate the push drill from real work

When we omitted --private, gh waited on Visibility until timeout. I found agents copy git push origin main before origin exists, then wait 30 seconds on HTTPS. Abort if gh api user is empty. Commits here use one-shot git -c flags, not --global. Lasting identity is How to Get Started with Git.

How do you implement the step-by-step recipe?

Run these steps from $HOME/push-commits-scratch. Always pass -m to git commit and --private to gh repo create. Do not fetch, pull, --force, or --web.

Terminalbash
export GH_PROMPT_DISABLED=1export GH_PAGER=catexport GIT_TERMINAL_PROMPT=0
  1. Confirm Git, GitHub CLI, and an authenticated login. Auth must already exist. Do not start a login flow.

    Terminalbash
    git --versiongh --versiongh api user --jq .login

    Expected: Git 2.39+, a gh version line, and a login. Missing git: brew install git or sudo apt-get update -y && sudo apt-get install -y git. Missing gh: brew install gh or sudo apt-get update -y && sudo apt-get install -y gh. If gh api user fails, stop. Never --web.

  2. Create the local scratch repository and record one commit. You cannot push an empty HEAD. Keep this on the throwaway path.

    Terminalbash
    mkdir -p "$HOME/push-commits-scratch"cd "$HOME/push-commits-scratch"if [ -d .git ]; then  echo "REPO_EXISTS"  test -z "$(git remote)" || { echo "ABORT origin already set"; exit 1; }else  git init -b mainfigit rev-parse --is-inside-work-treegit symbolic-ref --short HEADprintf '%s\n' '# Push commits scratch' 'Private drill. Safe to delete.' > README.mdgit add README.mdgit -c user.name='Push Commits Scratch' -c user.email='scratch@example.invalid' \  commit -m "Record the snapshot to push"git status --porcelain=v1git rev-parse HEADgit rev-list --count HEAD

    Expected: true, main, empty porcelain, a 40-character SHA, and 1. If origin is listed, stop. Verification: test "$(git rev-list --count HEAD)" = "1" && test -z "$(git status --porcelain=v1)" && test -z "$(git remote)" && echo local-ok.

  3. Create the private GitHub repository, add origin, and push main. The non-interactive create from a local repo is gh repo create with --source, --remote, and --push. Visibility must be --private.

    Terminalbash
    cd "$HOME/push-commits-scratch"OWNER="$(gh api user --jq .login)"REPO="push-commits-scratch"if gh repo view "${OWNER}/${REPO}" >/dev/null 2>&1; then  echo "ABORT GitHub repo ${OWNER}/${REPO} already exists"  exit 1figh repo create "$REPO" --private --source=. --remote=origin --push \  --description "Throwaway push drill. Safe to delete."git remote -vgit status -sb

    Expected: origin at https://github.com/YOUR_LOGIN/push-commits-scratch.git (or SSH), and git status -sb shows ## main...origin/main.

    If the empty private repo already exists and local remotes are empty, skip gh repo create. Run git remote add origin "https://github.com/${OWNER}/${REPO}.git" then git push -u origin main. Do not run both. Verification: test "$(git remote)" = "origin" && test "$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}')" = "origin/main" && echo remote-ok.

  4. Prove the remote SHA with git ls-remote and gh api. A successful git push exit code is not the receipt. git ls-remote reads the remote ref without fetching objects. Never --web.

    Terminalbash
    cd "$HOME/push-commits-scratch"OWNER="$(gh api user --jq .login)"REPO="push-commits-scratch"LOCAL_SHA="$(git rev-parse HEAD)"REMOTE_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')"API_SHA="$(gh api "repos/${OWNER}/${REPO}/commits/main" --jq .sha)"printf '%s\n' "$LOCAL_SHA" "$REMOTE_SHA" "$API_SHA"test "$LOCAL_SHA" = "$REMOTE_SHA"test "$LOCAL_SHA" = "$API_SHA"echo sha-ok

    Expected: three identical 40-character lines, then sha-ok. If ls-remote is empty, the push did not land.

  5. Record a second local commit and push with git push origin main. GitHub's example is git push origin main.

    Terminalbash
    cd "$HOME/push-commits-scratch"printf '%s\n' 'Second snapshot on origin.' >> README.mdgit add README.mdgit -c user.name='Push Commits Scratch' -c user.email='scratch@example.invalid' \  commit -m "Push the second snapshot"git push origin maintest "$(git rev-list --count HEAD)" = "2"test "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')"echo second-ok

    Expected: main -> main, then second-ok.

  6. Push one tag by name. Default git push skips tags. GitHub uses git push REMOTE TAG for one tag. Do not dump --tags from a real repo.

    Terminalbash
    cd "$HOME/push-commits-scratch"git tag v0.1.0git push origin v0.1.0git ls-remote origin refs/tags/v0.1.0test -n "$(git ls-remote origin refs/tags/v0.1.0 | awk '{print $1}')"echo tag-ok

    Expected: a SHA on refs/tags/v0.1.0, then tag-ok. If push protection prints a secret-scanning URL, stop and remove the secret. Do not bypass it.

How do you verify the deployment works?

Run this probe from any directory. It must finish in under 5 seconds and print VERIFY_OK.

Terminalbash
export GH_PROMPT_DISABLED=1export GH_PAGER=catexport GIT_TERMINAL_PROMPT=0cd "$HOME/push-commits-scratch"OWNER="$(gh api user --jq .login)"REPO="push-commits-scratch"test -d .git || { echo VERIFY_FAIL missing_git_dir; exit 1; }test "$(git symbolic-ref --short HEAD)" = "main" || { echo VERIFY_FAIL branch; exit 1; }test "$(git remote)" = "origin" || { echo VERIFY_FAIL remote; exit 1; }test "$(git rev-list --count HEAD)" = "2" || { echo VERIFY_FAIL commit_count; exit 1; }test "$(git log -1 --pretty=%s)" = "Push the second snapshot" || { echo VERIFY_FAIL subject; exit 1; }HEAD_SHA="$(git rev-parse HEAD)"test "${#HEAD_SHA}" = "40" || { echo VERIFY_FAIL sha_length; exit 1; }REMOTE_SHA="$(git ls-remote origin refs/heads/main | awk '{print $1}')"test "$HEAD_SHA" = "$REMOTE_SHA" || { echo VERIFY_FAIL ls_remote; exit 1; }API_SHA="$(gh api "repos/${OWNER}/${REPO}/commits/main" --jq .sha)"test "$HEAD_SHA" = "$API_SHA" || { echo VERIFY_FAIL api_sha; exit 1; }test -n "$(git ls-remote origin refs/tags/v0.1.0 | awk '{print $1}')" || { echo VERIFY_FAIL tag; exit 1; }test -z "$(git status --porcelain=v1)" || { echo VERIFY_FAIL dirty; exit 1; }echo VERIFY_OKecho "$HEAD_SHA"

Expected stdout (SHA varies):

text
VERIFY_OKc0ffee0c0ffee0c0ffee0c0ffee0c0ffee0c0ffe

When we ran this probe at ZeroShot Studio, exit code 0 in 1.1 seconds after the 2.4 second create. VERIFY_FAIL ls_remote means origin lacks this HEAD. Optional cleanup:

Terminalbash
gh repo delete "${OWNER}/push-commits-scratch" --yesrm -rf "$HOME/push-commits-scratch"

--yes keeps gh repo delete non-interactive.

What are the common production failure modes?

Abort. Do not fetch, pull, or --force.

  • Visibility or login prompt: gh repo create without --private opens a picker. git push without credentials waits on Username for 'https://github.com'. Export the three locks. Pass --private. Never --web. If gh api user fails, stop.
  • origin already exists or the GitHub name is taken: git remote add origin exits 128. gh repo create fails when push-commits-scratch exists under your login. Abort. Do not -f overwrite a URL.
  • Push protection blocked the pack: GitHub rejected a supported secret. Remove it from history. Do not bypass protection.
  • Non-fast-forward updates were rejected: Remote main has commits you lack. This recipe stops. Do not fetch, pull, or git push --force.
  • 2 GiB push limit or empty HEAD: GitHub rejects packs larger than 2 GiB. If git rev-list --count HEAD is 0, commit first. Without the prompt lock, a credential hang lasts 30 seconds instead of under 5.

FAQ

What is the difference between gh repo create --push and git push origin main? gh repo create --private --source=. --remote=origin --push creates the GitHub repository, writes origin, and sends existing commits. git push origin main only sends. Use git remote add plus git push -u origin main when the empty private repo already exists. Prove either path with git ls-remote.

Why pass git push -u instead of git push? -u is --set-upstream. It records that local main tracks origin/main. After the first -u, git push origin main and bare git push both update origin/main.

What should I do if Git says non-fast-forward updates were rejected? Stop. The remote has commits this clone does not. Do not fetch, pull, or --force from this scratch recipe. Recovery is a later item.

How do I push a tag or delete a remote branch? One tag: git push origin v0.1.0. Every tag: git push origin --tags (avoid on a real repo). Delete a remote branch with git push origin :BRANCH-NAME. Never delete main.

Do I need GitHub Desktop or a browser to finish this? No. This recipe is Git plus gh. Never --web. If gh api user fails, authenticate gh in a human session first. The receipt is git ls-remote.

Share