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.

Architecture Flow
flowchart LR
    LocalCommit["Local Commits (HEAD)"] --> Push["git push origin "]
    Push --> Auth["Authenticate via SSH / PAT"]
    Auth --> RemoteUpdate["Remote Ref Fast-Forwarded"]

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