Git Bash and MSYS2 can load different Git executables while sharing the same ~/.gitconfig. http.sslbackend=schannel works with Git for Windows, but an MSYS2-native Git may support only OpenSSL and fail with Unsupported SSL backend.
Remove the MSYS2 Git package and put Git for Windows first in MSYS2's PATH:
pacman -R --noconfirm git
printf '%s\n' 'export PATH="/c/Program Files/Git/cmd:$PATH"' >> ~/.bashrc
source ~/.bashrc
hash -r
Use one shared configuration:
git config --global http.sslbackend schannel
git config --global credential.helper manager
Verify both shells resolve the same executable:
type -a git
git --version
git ls-remote origin HEAD
This keeps certificate verification in the Windows certificate store and avoids shell-specific OpenSSL CA or stale credential-store differences. Do not disable TLS verification with http.sslVerify=false.
Same task — "show me what this branch changed" — but the two tools take opposite dot conventions. Get it backwards and your review fills with commits the author never touched.
The diff: use three dots
git diff origin/main...HEAD
Three-dot diff is git diff $(git merge-base origin/main HEAD) HEAD — it diffs against the branch point, not the tip of main. That's exactly what you want: the author's delta and nothing else.
The nice property: it's immune to origin/main moving forward. New commits landing on main after the branch point aren't ancestors of HEAD, so they don't shift the merge-base. The diff stays clean.
The trap is a stale base, not a newer one. If your local main is older than the branch point, the merge-base slides back to an older ancestor and the diff swallows unrelated upstream commits — making the author look like they changed far more than they did. So fetch first:
git fetch origin
git diff origin/main...HEAD
Want to see the branch point itself? git merge-base origin/main HEAD.
The log: use two dots
git log origin/main..HEAD
Two-dot log = commits reachable from HEAD but not from origin/main = the branch's own commits, exactly.
Don't reach for three dots here out of habit — git log A...B is the symmetric difference, so it also lists the commits main picked up that HEAD doesn't have. That's the noise you were trying to avoid.
So: diff three-dot, log two-dot. Different tools, opposite defaults, same job.
On a corporate Windows box, git pull/git fetch keeps stopping to ask:
Unlink of file '.git/objects/pack/pack-305a05....idx' failed. Should I try again? (y/n)
The cause is a security agent — SentinelOne, ZScaler, Defender — holding an open handle on the old .idx files while Git tries to repack. Git for Windows wraps unlink/rename failures in a retry prompt, and you end up babysitting every pull, mashing n.
yes n | git pull works but you have to remember to prefix it every time. The permanent fix is one line in ~/.bashrc (the Git Bash one):
export GIT_ASK_YESNO=false
Git runs the value of GIT_ASK_YESNO as a command to decide whether to retry — a non-zero exit is treated as "n". false always exits non-zero, so every prompt is silently answered "no". It's cleaner than </dev/null redirection (works regardless of whether stdin is a tty) and doesn't touch the other interactive bits — commit-message editor, credential prompts — which go through different machinery.
Answering "n" just means Git leaves the locked old pack file on disk; the new pack is already live, so the repo is fine. Once the security agent lets go, a git gc sweeps up the leftovers.
If the prompts are frequent, this cuts down how often they fire — sometimes the lock is Git's own multi-threaded pack-objects, not the AV:
git config --global pack.threads 1
This is the silence-it companion to the heavier "it's an open handle, not a permission" diagnosis — same root cause (a process holding a handle), but here you just want the nagging to stop, not to hunt the locker down.
When you build tooling around git repos, you eventually hit "where do I store a per-repo setting?" Committing a .toolrc pollutes the repo and needs a PR; a sidecar file in ~/.config can't easily be per-repo. The clean answer: put it straight into git config under your own namespace.
# per-repo (lands in <repo>/.git/config — local, never committed)
git config --local mux.adhocWorktree false
# per-machine, all repos (lands in ~/.gitconfig)
git config --global mux.adhocWorktree false
# read it back, normalized to true/false
git config --bool --get mux.adhocWorktree # -> false ; exit 1 if unset
git lets you invent any <section>.<key> it doesn't recognize (mux.* here) and just stores it. That hands you three things for free that you'd otherwise have to build:
- A per-repo store that never leaks —
.git/config is local to the checkout and never committed, so it won't pollute someone else's repo.
- Scope layering with native precedence —
--local (repo) transparently overrides --global (machine). One git config --get resolves the hierarchy; you don't write any "check repo, then fall back to machine" logic.
- Type parsing —
--bool normalizes false/0/no/off → false, and a non-zero exit cleanly signals "not configured" so you can fall through to your built-in default.
So a full precedence chain for a feature flag becomes: CLI flag → env var → git config (local→global) → built-in default — and the middle two scopes are entirely git's job.
One gotcha with worktrees: git config --local writes to the shared common .git/config, not the linked worktree, because worktrees share config. If your setting is meant to key off the main checkout (it usually is), that's exactly right — but don't expect two worktrees of the same repo to hold different --local values.
The decision that surfaced this: an agent launcher was hardwiring a git worktree per session (hundreds of MB on big repos, painful on a VM). The whole "make it opt-out, but where does the per-repo marker live?" problem evaporated the moment we realized git config already is a per-repo + per-machine settings store with precedence baked in.
When you want Git to ignore something that's yours — a local scratch dir, editor cruft, a personal plans/ folder, machine-local config — don't add it to the tracked .gitignore. That file is shared: a rule like plans/ or wiki-drafts/ forces your personal directory conventions onto everyone, and they don't have those dirs.
Put it in .git/info/exclude instead. Same syntax as .gitignore, but it's per-clone and never committed — so it's invisible to teammates and travels with nothing:
# .git/info/exclude
.claude/settings.local.json
.claude/plans/
.claude/wiki-drafts/
The rule of thumb: **.gitignore is for what the project should ignore (build output, node_modules/, *.user); .git/info/exclude is for what you happen to ignore on this machine.** If a teammate cloning fresh wouldn't also produce the file, it doesn't belong in the shared list.
One gotcha with worktrees: info/exclude lives in the common git dir, so it's shared across all linked worktrees of the same clone — but still not across separate clones. Find it with git rev-parse --git-common-dir if you're not sure where it is.
And if you ever need to start tracking something a blanket exclude was hiding (e.g. you had .claude excluded but now want .claude/skills/ committed), narrow the exclude line to the specific paths you still want ignored — don't delete it and shove replacement rules into the shared .gitignore.