Cookbook-first reference for every Git and GitHub workflow Joe runs. Read the Safety Rules once. After that, jump to the scenario you're in.
All examples assume you are in the repo: cd ~/Documents/Development/<repo>. Worktree paths look like ~/Documents/Development/<repo>/.claude/worktrees/<name> and the same commands apply inside them.
If you find yourself reaching for any ⚠ command, pause and read the matching scenario in §6 first.
| Rule | Why |
|---|---|
Never git push --force to main / master / shared branches | Rewrites history other people depend on. Use --force-with-lease only on your own feature branches. |
Never git reset --hard without running git status and git stash first if there's anything uncommitted | --hard discards working-dir changes silently. No undo unless they were committed or stashed. |
Never git rebase a branch that's already been merged into main or pulled by someone else | Rewrites SHAs; collaborators (or other machines) will diverge. |
Never commit secrets (.env, keys, tokens). If you do, rotate the secret first, then scrub history | History rewrite alone doesn't help — the secret is already exposed. |
Always git fetch before assuming you know the remote state | Local view of origin/main is only as fresh as your last fetch. |
Always create a new commit instead of --amend after a push | Amend rewrites the last commit. If it's pushed, others may have it. |
Always prefer --force-with-lease over --force when force-push is unavoidable | Aborts if the remote moved since your last fetch; --force blindly overwrites. |
Never skip pre-commit hooks (--no-verify) without a documented reason | Hooks exist to catch real problems; bypassing them shifts the bug to CI or prod. |
Git tracks state across three trees.
Working Directory ← files you can see and edit
↓ git add
Index (Staging) ← what will be in your next commit
↓ git commit
HEAD (Committed) ← the current commit on your current branch
↓ git push
Remote ← origin/<branch> on GitHub
| Concept | What it is |
|---|---|
| Commit | An immutable snapshot of the index, identified by a SHA-1 hash. |
| Branch | A movable pointer to a commit. main, feature/x, etc. |
| HEAD | Pointer to the commit currently checked out (usually via a branch ref). |
| Ref | Any name that points at a commit: branch, tag, HEAD, HEAD~3, HEAD@{2}. |
| Remote | A named URL to another repo. origin is default; upstream is convention for the repo you forked from. |
| Upstream branch | The remote branch your local branch tracks. Set with git push -u origin <branch>. |
| Fast-forward | Merge where the target's tip is a direct ancestor of source. No merge commit needed. |
git switch other-branch doesn't copy files — it moves HEAD and updates your working dir. This is why cherry-pick and rebase can shuffle commits between branches so easily.The 90% workflow.
| Step | Command | Notes |
|---|---|---|
| 1. Sync main | git switch main && git pull | Always start from current main |
| 2. Branch | git switch -c feat/short-name | One branch per logical change |
| 3. Edit | (work) | Small, focused commits |
| 4. Stage | git add <file> or git add -p | Prefer named files over git add . |
| 5. Commit | git commit -m "feat: short imperative summary" | First line ≤72 chars |
| 6. Push | git push -u origin feat/short-name | -u only the first time |
| 7. Open PR | gh pr create --title "..." --body "..." | See §9 for the heredoc body pattern |
| 8. Iterate | edit → add → commit → push | No -u after first push |
| 9. Merge | gh pr merge --squash --delete-branch | Or merge in UI after review |
| 10. Clean up | git switch main && git pull && git branch -d feat/short-name | -d refuses if unmerged; safe |
Commit message style: <type>: <imperative summary> where type ∈ {feat, fix, refactor, docs, test, chore}. Body: why, not what — the diff shows what.
| Situation | Use |
|---|---|
| Integrating a long-lived feature into main, want to preserve branch history | git merge --no-ff |
| Updating your in-flight feature branch with latest main | git rebase main (cleaner) or git merge main (safer if shared) |
| Branch is shared with another machine or another person | git merge — never rebase shared history |
| Branch exists locally on multiple of your own machines but nothing's been pulled from origin yet | Rebase OK — "shared" means pulled by someone else, not just pushed |
| Cleaning up local commits before opening a PR | git rebase -i to squash/reorder |
| One commit on main per feature | PR's "Squash and merge" button (or gh pr merge --squash) |
Default for solo feature branches: rebase locally for cleanup, then gh pr merge --squash for the final integration.
| Mode | HEAD moves? | Index reset? | Working dir? | When |
|---|---|---|---|---|
--soft | yes | no | no | Undo commit, keep changes staged. Common for "redo my last commit." |
--mixed (default) | yes | yes | no | Undo commit, unstage changes, keep edits in working dir. |
--hard ⚠ | yes | yes | yes | Throw everything away back to the target ref. Destructive — only reflog can save you. |
HEAD~1 = one commit back. HEAD~3 = three back. HEAD@{2} = where HEAD was 2 movements ago (reflog).
| Goal | Command |
|---|---|
| Undo a commit on a private branch (rewrite history) | git reset |
| Undo a commit on a shared/pushed branch (preserve history) | git revert (new commit that inverts) |
| Discard uncommitted changes to one file | git restore <file> |
| Discard all uncommitted changes | git restore . ⚠ (or git stash if you might want them later) |
| Switch to a different branch | git switch <branch> |
| Switch to a specific commit (detached HEAD) | git switch --detach <sha> |
git checkout still works for all of these; switch and restore (Git ≥2.23) are clearer — checkout was overloaded.
Need to push but remote rejects?
├─ Did you rebase or amend?
│ ├─ Yes, branch is YOURS only → git push --force-with-lease ✓
│ ├─ Yes, branch is shared → STOP. Talk to collaborators first.
│ └─ Yes, branch is main/master → STOP. Never force-push main.
└─ No, just behind → git pull --rebase, then git push
Always prefer --force-with-lease over --force. It refuses if the remote has commits you haven't fetched, preventing you from clobbering someone else's work.
git status | What's staged, modified, untracked. First command of every session. |
git status -s | Short format |
git log --oneline -20 | Last 20 commits, one per line |
git log --graph --oneline --all -20 | Visual branch graph |
git log -p <file> | Every commit that touched a file, with diffs |
git log -S "string" | "Pickaxe" — every commit that added/removed that string |
git log -G "regex" | Pickaxe with regex |
git diff | Unstaged changes |
git diff --staged | Staged but uncommitted |
git diff main..HEAD | Everything different between main and current branch |
git show <sha> | Diff and message of one commit |
git blame <file> | Per-line, who last changed it |
git reflog | Every HEAD movement (the "undo journal") |
git add <file> | Stage a specific file |
git add -p | Stage hunks interactively (review every change) |
git add . | Stage everything in current dir ⚠ risk of secrets/junk |
git restore --staged <file> | Unstage a file (keeps edits) |
git commit -m "msg" | Commit staged changes |
git commit -a -m "msg" | Stage all tracked-file changes & commit (skips untracked) |
git commit --amend | Replace last commit (message + staged) ⚠ if pushed |
git commit --amend --no-edit | Add staged changes to last commit, keep message |
git branch | List local branches |
git branch -a | List local + remote-tracking |
git branch -vv | List with upstream tracking |
git switch <branch> | Switch to existing branch |
git switch -c <new> | Create + switch from current HEAD |
git switch -c <new> origin/<remote> | Create local tracking a remote |
git branch -d <branch> | Delete (refuses if unmerged) |
git branch -D <branch> ⚠ | Force-delete (loses commits if unmerged) |
git branch -m <old> <new> | Rename branch |
git fetch | Download remote refs, don't merge. Safe anytime. |
git fetch --all --prune | Fetch all; remove tracking refs for deleted remote branches |
git pull | fetch + merge on current branch |
git pull --rebase | fetch + rebase — cleaner history when collaborating |
git push | Push current branch to its upstream |
git push -u origin <branch> | First push: set upstream |
git push --force-with-lease | Force-push with safety check (§4.4) |
git push --tags | Push annotated tags (not pushed by default) |
Set pull.rebase = true globally if you want pulls to always rebase: git config --global pull.rebase true.
git merge <branch> | Merge into current. FF if possible, else merge commit. |
git merge --no-ff <branch> | Always create merge commit (preserves branch shape) |
git merge --squash <branch> | Squash into staged changes on current; commit manually |
git merge --abort | Bail out of an in-progress merge |
git rebase <base> | Replay current branch's commits on top of <base> |
git rebase -i <base> | Interactive: pick/squash/reword/edit/drop |
git rebase --onto <new> <old> <branch> | Surgical: move <branch> from old-base to new-base |
git rebase --continue / --skip / --abort | Continue after resolving / skip current / bail out |
git cherry-pick <sha> | Apply that commit to current branch (new SHA) |
git cherry-pick <sha1>..<sha2> | Pick a range (exclusive of sha1; use <sha1>^..<sha2> to include) |
git cherry-pick -x <sha> | Add "(cherry picked from commit ...)" to message |
git cherry-pick --abort / --continue | Bail / continue after conflict |
git revert <sha> | New commit that undoes <sha>. Safe on shared history. |
git revert -m 1 <merge-sha> | Revert a merge commit (must pick parent line; -m 1 = first parent) |
git revert -n <sha> | Stage revert without committing (combine multiple) |
git revert <sha1>..<sha2> | Revert a range |
git stash | Save unstaged + staged; revert working dir to HEAD |
git stash push -m "msg" | Stash with a label |
git stash push -u | Include untracked files |
git stash list | Show all stashes |
git stash show -p 'stash@{0}' | Show diff of a stash |
git stash pop | Apply latest and drop it |
git stash pop 'stash@{2}' | Apply specific and drop it |
git stash apply 'stash@{0}' | Apply but don't drop (safer) |
git stash drop 'stash@{0}' | Delete a specific stash |
git stash clear ⚠ | Delete all stashes |
zsh quoting: wrap stash@{N} or HEAD@{N} in single quotes — zsh treats {...} as brace expansion and bare forms error. Bash users can drop the quotes.
git reflog | Every HEAD movement, newest first. Your time machine. |
git reflog show <branch> | Reflog for one branch |
git reset --hard 'HEAD@{2}' ⚠ | Jump HEAD back 2 movements ago |
git reset --hard <sha> ⚠ | Jump HEAD to a specific commit |
git fsck --lost-found | Find dangling commits orphaned by rebases/resets |
git cat-file -p <sha> | Inspect any object by SHA |
Reflog entries persist ~90 days by default (gc.reflogExpire). If you "lose" a commit via reset/rebase, it's almost always still in the reflog.
git bisect start | Begin a binary-search bug hunt |
git bisect bad / good <sha> | Mark current bad / a known-good sha |
git bisect reset | Exit bisect, return to original HEAD |
git bisect run <script> | Automated — script exits 0 = good, non-zero = bad |
git blame -L 10,20 <file> | Blame lines 10–20 |
git log --follow <file> | Log including history before file was renamed |
git tag | List all tags |
git tag -a v1.2.0 -m "Release 1.2.0" | Annotated tag (recommended for releases) |
git tag v1.2.0 | Lightweight tag (no metadata; personal markers) |
git tag -d v1.2.0 | Delete local tag |
git push origin v1.2.0 / --tags | Push one tag / all annotated tags |
git push --delete origin v1.2.0 | Delete remote tag |
gh release create v1.2.0 --notes "..." | GitHub Release attached to the tag |
gh release create v1.2.0 dist/*.zip | Release with binary assets |
Separate working directory backed by the same .git. Lets you have main in one folder and feat/x in another. Claude Code uses these per-task.
git worktree list | Show all worktrees |
git worktree add .claude/worktrees/feat-x feat/x | Create at that path checked out to feat/x (Joe's convention; see §7) |
git worktree add -b feat/y .claude/worktrees/feat-y main | Create worktree + new branch off main |
git worktree remove <path> | Remove (refuses if dirty) |
git worktree remove --force <path> ⚠ | Force-remove dirty worktree |
git worktree prune | Clean up records for worktrees whose dirs are gone |
Avoid submodules unless absolutely necessary. They complicate every workflow. Prefer monorepo or package managers.
git submodule add <url> <path> | Add a submodule |
git submodule update --init --recursive | Initialize + check out all after a fresh clone |
git clone --recurse-submodules <url> | Clone with submodules in one shot |
Format: Situation → What's happening → Run this → Verify → Gotchas.
You meant to be on feat/x but you were on main when you committed.
git log -1 # note the SHA of the commit to move
git log --oneline origin/main..HEAD # ⚠ MUST show exactly one commit
git switch feat/x # or: git switch -c feat/x
git cherry-pick <sha> # use the SHA, not the branch name
git switch main
git reset --hard HEAD~1 # ⚠ destructive; previous --oneline check is the precondition
git log --oneline -3 main # bad commit gone
git log --oneline -3 feat/x # bad commit landed here as new SHA
main, do not reset main. Use revert (§6.4) and cherry-pick to feat/x instead.git reflog → find the pre-reset HEAD → git reset --hard 'HEAD@{1}'. See §5.8.Last commit was wrong, but the file edits are fine — you want to redo the commit.
git reset --soft HEAD~1 # commit gone, changes still staged
# edit, restage as needed
git commit -m "better message"
git status # should show staged changes
git log --oneline -3
Last commit was wrong AND the changes are bad — throw it all away.
git status # confirm nothing else uncommitted you'd lose
git reset --hard HEAD~1 # ⚠ destructive
git reflog → find the lost commit → git reset --hard 'HEAD@{1}' brings it back (quote braces in zsh).Bad commit is on a shared branch (or main). Rewriting history is unsafe.
git revert <sha> # creates a new commit that inverts the bad one
git push
For merge commits, plain git revert errors. You must pick a parent:
git revert -m 1 <merge-sha> # -m 1 = keep first-parent line (branch that received the merge)
git commit --amend # opens editor for new message
git commit --amend
git push --force-with-lease # ⚠ never on main/shared
Already pushed to main / shared branch: leave it. Don't rewrite history others may have pulled.
git rm --cached <file> # untrack but keep on disk
echo "<file>" >> .gitignore
git commit --amend --no-edit
brew install git-filter-repo # or: pip install git-filter-repo
git filter-repo --path <file> --invert-paths # ⚠ rewrites entire history
git push --force # ⚠ see note
Note: this is the one place the bible uses bare --force instead of --force-with-lease (§1). filter-repo deliberately rewrites every ref; lease checks fight that intent and can leave dangling refs holding the secret.
git switch main
git pull
git switch feat/x
git rebase main # replay your commits on top of new main
# resolve any conflicts (see §6.11)
git push --force-with-lease # ⚠ rebase rewrote your SHAs
git switch feat/x
git merge main # creates a merge commit; no force-push needed
git push
merge over rebase — rebases can detach review comments from their commits. The GitHub PR UI's "Update branch" button defaults to a merge and is the safest one-click option for a shared PR.git rebase -i HEAD~3
# In the editor: leave first as `pick`, change others to `squash` (or `s`)
# Save, then edit the combined message in the next editor view
If already pushed (your own branch only): git push --force-with-lease ⚠
fixup (or f) is like squash but discards the squashed commits' messages — useful when they were just "wip" or "fix".git rebase -i HEAD~N # N puts the big commit in range
# Mark the commit `edit` (or `e`) and save. Rebase pauses there:
git reset HEAD~ # un-commit but keep changes in working dir
git add -p # stage one logical chunk
git commit -m "first piece"
git add -p
git commit -m "second piece"
# repeat until clean
git rebase --continue
git rebase --abort
.git/rebase-merge/ or .git/rebase-apply/ exists). After --continue finishes, --abort is no longer available — recover via reflog (§5.8): find the pre-rebase HEAD with git reflog, then git reset --hard 'HEAD@{N}'.Git can't auto-merge two changes to the same lines.
git status # lists files "both modified"
# Open each conflicted file. Find conflict markers:
# <<<<<<< HEAD
# your version
# =======
# their version
# >>>>>>> branch-name
# Edit to the final version. Remove all markers.
git add <resolved-file>
# When all conflicts resolved:
git merge --continue # if mid-merge
git rebase --continue # if mid-rebase
git cherry-pick --continue # if mid-cherry-pick
git mergetool # 3-way merge tool (configurable)
git diff # shows conflict hunks before resolving
git checkout --ours <file> # take your version wholesale
git checkout --theirs <file> # take their version wholesale
--ours means the branch you're rebasing onto, --theirs means your commits. This trips everyone up.git reflog # find the last commit on that branch
# Look for "checkout: moving from <branch>" or its tip
git switch -c <branch> <sha>
git fetch and check git branch -a — it may still be there.git log --oneline other-branch # find the SHA
git switch feat/x
git cherry-pick <sha>
git cherry-pick <sha1> <sha2> <sha3>
git cherry-pick <sha1>..<sha2> # range, exclusive of sha1
<sha1>^..<sha2> to include <sha1>.git revert <sha> # creates an undo commit
git push
Broke production: revert immediately as above. Root-cause separately. Do not force-push main.
You committed on Mac, then committed on Windows without pulling first. Both have local commits the other doesn't.
git fetch
git status # will say "diverged"
git pull --rebase # replays your local commits on top of remote's
# resolve conflicts if any (§6.11)
git push
git pull # creates a merge commit
git push
git push --force-with-lease on machine A, the remote SHA changed. On machine B, git pull --rebase can surface phantom conflicts. If A is canonical: git fetch && git reset --hard origin/<branch> ⚠ (discards local commits).Prevention: push at end of every session; pull at start. See Multi-Instance Protocol.
worktree path is gone but git worktree list still lists it; or you can't switch branches because Git says it's checked out elsewhere.
git worktree list # see what Git thinks exists
git worktree prune # remove records for worktrees whose dirs are gone
git worktree remove <path> # remove a specific one
git worktree remove --force <path> # ⚠ if dirty
Claude Code worktrees live at ~/Documents/Development/<repo>/.claude/worktrees/<adjective-name> with branches claude/<adjective-name>. They persist after the task ends. Prune periodically with git worktree prune.
Giant file accidentally committed, blowing up clone size. Or a leaked secret (but rotate first — §6.6).
brew install git-filter-repo # or: pip install git-filter-repo
git filter-repo --path path/to/file --invert-paths # ⚠ rewrites all history
git push --force --all # ⚠ bare --force intentional — see §6.6
git push --force --tags # ⚠ rewrites tags
cp -r repo repo.bak first).main/master of a high-traffic repo without team coordination.git filter-branch is deprecated and slow. Always use git-filter-repo.gh pr create failed because branch isn't pushedgit push -u origin <current-branch>
gh pr create --title "..." --body "..."
-u only needed the first time per branch.
gh pr checkout <pr-number> # switches to PR's branch
git fetch origin main
git rebase origin/main # or: git merge origin/main
# resolve conflicts (§6.11)
git push --force-with-lease # ⚠ rebase only; plain push if you merged
Option B — in GitHub UI: click "Resolve conflicts" on the PR. Only works for simple cases.
| Diff between two commits | git diff <sha1>..<sha2> |
| Diff between two branches | git diff main..feat/x |
| Just the file list | git diff --name-only main..feat/x |
| Just the stat summary | git diff --stat main..feat/x |
| Commits in B but not A | git log A..B --oneline |
| Commits unique to each | git log A...B --oneline --left-right (note 3 dots) |
| Compare two PRs | gh pr diff <pr-number> |
git bisect)git bisect start
git bisect bad # current commit is broken
git bisect good <known-good-sha>
# Git checks out a midpoint. Test it.
git bisect good # or: git bisect bad
# Repeat until Git names the first bad commit.
git bisect reset # return to original HEAD
git bisect start HEAD <good-sha>
git bisect run ./test.sh # exit 0 = good, non-zero = bad
git bisect reset
bisect run requires a deterministic, fast test. Flaky tests produce wrong results.Claude Code creates a worktree per task at ~/Documents/Development/<repo>/.claude/worktrees/<adjective-name>. Each has its own branch claude/<adjective-name> but shares the underlying .git.
main checked out in one window for builds/testing while editing in anothercd ~/Documents/Development/<repo>
git worktree add ../<repo>-feat-x feat/x # existing branch
git worktree add -b feat/y ../<repo>-feat-y main # new branch off main
git worktree list
git worktree remove ~/Documents/Development/<repo>-feat-x
git worktree prune # stale entries
rm -rf without first running git worktree remove. Leaves Git's records inconsistent. If you already did, run git worktree prune.| When | Run |
|---|---|
| Starting work on machine A | git fetch && git pull on every active branch |
| Pausing work | git status (verify clean) → git push |
| Resuming on machine B | git fetch && git pull |
If you forget to push on machine A and start working on machine B:
# On machine B, after fetch:
git status # "diverged"
git pull --rebase # cleanest history
# OR
git pull # merge commit; safer if rebase scares you
Stash doesn't sync via Git. Either:
wip: commit and revert it on the other machine, orgit diff > /tmp/wip.patch, transfer, git apply /tmp/wip.patchgh CLIgh pr create --title "feat: short summary" --body "$(cat <<'EOF'
## Summary
- bullet 1
- bullet 2
## Test plan
- [ ] manual test step
- [ ] CI passes
EOF
)"
gh pr list | All open PRs in this repo |
gh pr list --author @me | Just yours |
gh pr view | Current branch's PR |
gh pr view <num> | Specific PR |
gh pr view <num> --web | Open in browser |
gh pr diff <num> | Show the diff |
gh pr checks | CI status for current branch's PR |
gh pr checkout <num> # creates/switches to local branch tracking the PR
gh pr comment <num> --body "LGTM"
gh pr review <num> --approve
gh pr review <num> --request-changes --body "see comments"
gh pr merge <num> --squash --delete-branch # solo workflow default
gh pr merge <num> --merge # preserves all commits
gh pr merge <num> --rebase # rebases onto base, no merge commit
gh pr create --draft --title "..." --body "..."
gh pr ready <num> # convert draft → ready for review
gh api repos/{owner}/{repo}/pulls/<num>/comments # all review comments on a PR
gh api repos/{owner}/{repo}/issues/<num>/comments # general PR comments
{owner} / {repo} / {branch} are documented placeholders gh api expands from current repo context. The older :owner/:repo colon form sometimes still works but is undocumented — don't rely on it.
When Claude Code drives a commit, the convention (from ~/.claude/CLAUDE.md) is to add a Co-Authored-By trailer. Use a heredoc so the multiline message renders correctly.
git commit -m "$(cat <<'EOF'
feat(audio): add onset detector hints
Why: tempo and valence cues need a single source of truth across
web and iOS. This wires the C2 detector to both targets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Add the trailer when: Claude wrote substantive code in the commit. Mechanical edits (rename, formatter run) don't need it.
Skip when: purely human commits, version bumps, merge commits.
| Term | Meaning |
|---|---|
| HEAD | Pointer to the current commit (usually via a branch) |
| detached HEAD | HEAD points directly at a commit, not a branch — commits made here are easy to lose |
| fast-forward (FF) | Merge that just moves the branch pointer because the target is an ancestor of the source |
| upstream | The remote branch your local branch tracks |
| origin | Convention for the default remote |
| fork | A copy of a repo on GitHub under your account; you push to your fork, then PR upstream |
| squash | Combine multiple commits into one |
| rebase | Replay commits on top of a different base |
| cherry-pick | Apply one commit's diff onto current branch as a new commit |
| revert | Create a new commit that inverts another commit's diff |
| reflog | Log of every HEAD movement; the undo history |
| dangling commit | A commit no branch/tag references; reachable only via reflog or fsck |
| annotated tag | Tag stored as a full Git object with author/date/message; preferred for releases |
| lightweight tag | Tag that's just a name pointing at a commit; for personal markers |
git help <command> or git <command> --help — man pages are authoritative