Background
As a developer constantly jumping across multiple projects, I frequently run into a headache-inducing issue: committing code in a company repository with my personal email, or mistakenly using my work email in a personal repo. Seeing a personal avatar show up in the commit history of a corporate project is always embarrassing.
Recently, I made this mistake again and had already pushed it to the remote repository. This time, I decided to fix the root cause once and for all so this careless error never happens again.
As usual, I turned to Gemini for guidance.
Emergency Fix: Handling Pushed Commits with Wrong Author Details
Sure, this is a very common yet tricky issue in Git because you have already pushed the incorrect commit to the remote repository. Rewriting publicly shared history requires caution, especially when collaborating with teammates.
Don’t worry—this is completely solvable. Here are two approaches:
- Option 1: Fix only the latest commit (the simplest and most common scenario)
- Option 2: Fix multiple historical commits (the more complex scenario)
Before proceeding, please read this important note:
Warning The following operations will rewrite remote repository history. If you work in a team, make sure to coordinate with your colleagues first. A force push will diverge their local branch history from the remote, requiring them to run extra commands to sync back up.
Option 1: Fix Only the Latest Commit
If only the very last commit you just pushed is incorrect, this is the simplest scenario.
Step 1: Update the author info on the latest commit
In your local repository, use git commit --amend to modify the last commit.
We can use the GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL environment variables to temporarily override the committer settings.
1# --amend opens editor for the last commit message, but we won't change the message
2# --author directly updates the author information
3# --no-edit keeps the previous commit message without opening an editor
4GIT_COMMITTER_NAME="Your Correct Username" GIT_COMMITTER_EMAIL="[email protected]" git commit --amend --author="Your Correct Username <[email protected]>" --no-editAfter running this, your local latest commit is fixed.
Step 2: Force push to the remote repository
Because you modified your local history, it has diverged from the remote repository. You cannot run a standard git push; you must force push.
It is recommended to use --force-with-lease, which is safer than --force because it verifies that no one else has updated the remote branch since your last fetch.
1git push --force-with-leaseDone! The author info on the latest commit on the remote repository is now corrected.
Option 2: Fix Multiple Historical Commits
If there are multiple past commits with the wrong email, or the entire repository history contains the wrong address, you can fix it using the following method:
Method A: Recommended Standard Tool (git-filter-repo)
Git has officially deprecated legacy git filter-branch and recommends git-filter-repo as the official standard tool for rewriting history (it is dramatically faster and safer).
-
Install
git-filter-repo(e.g., via Homebrew on macOS):1brew install git-filter-repo -
Run the following command in the repository root to bulk-replace the old email and username with the correct ones:
1git filter-repo --email-callback ' 2return b"[email protected]" if email == b"[email protected]" else email 3' --name-callback ' 4return b"Your Correct Username" if email == b"[email protected]" else name 5' --force -
After history is rewritten, force push to the remote:
1git push --force-with-lease
(Note: If your current development environment is restricted and cannot install extra Python tools, you can also fall back to Git’s built-in git filter-branch --env-filter ... command.)
Root Solution: Automatically Switch Identities with Git Conditional Includes
Fixing the immediate error is good, but we need a fundamental solution to prevent this recurring headache.
I want Git to automatically select the right identity across broad directory scopes. How can I achieve that?
Automatically switching Git identities (username and email) based on different project directories (such as “work” vs. “personal”) is an excellent practice! It effectively prevents the awkwardness of committing personal emails into company repositories.
Git provides an elegant built-in solution called “Conditional Includes” (includeIf). This feature was introduced in Git 2.13 and has long since become a standard capability.
How it works: you define a condition in your global ~/.gitconfig. When the path of your Git repository matches this condition, Git loads an extra, dedicated configuration file that overrides the identity settings in your global configuration.
Here is a step-by-step setup guide using a typical “work” vs. “personal” scenario.
Scenario Assumption
Suppose your directory structure looks like this:
- All work projects live in
~/dev/work/. - All personal projects live in
~/dev/personal/.
Step 1: Create Separate Configuration Files for Each Identity
First, create dedicated configuration files for each identity. These files can live anywhere in your home directory, but placing them alongside ~/.gitconfig prefixed with .gitconfig- makes them easy to manage.
- Create the work identity config
~/.gitconfig-work
Run the following command in your terminal, or create and edit the file directly:
1# Write the following to ~/.gitconfig-work
2cat > ~/.gitconfig-work << EOF
3[user]
4 name = Your Work Username
5 email = <your-work-email>
6EOF- Create the personal identity config
~/.gitconfig-personal
Similarly, create a file for your personal identity:
1# Write the following to ~/.gitconfig-personal
2cat > ~/.gitconfig-personal << EOF
3[user]
4 name = Your Personal Username
5 email = <your-personal-email>
6EOFNow you have two separate identity configurations.
Step 2: Update Your Main ~/.gitconfig
Next, edit your global ~/.gitconfig to tell Git when to load those identity files.
- Set a default identity
In~/.gitconfig, specify your most frequently used identity or a fallback default. Git uses this when a project path matches no special condition. Leaving it empty is also valid.
1[user]If your ~/.gitconfig already has a [user] section, ensure it reflects your intended default.
- Add
includeIfconditions
At the end of~/.gitconfig, add the[includeIf]directives:
1# When repo path is under ~/dev/work/, load work config
2[includeIf "gitdir:~/dev/work/"]
3path = ~/.gitconfig-work
4
5# When repo path is under ~/dev/personal/, load personal config
6[includeIf "gitdir:~/dev/personal/"]
7path = ~/.gitconfig-personal
Note The path following
gitdir:must end with a trailing slash/to match directories rather than files of the same name.~/is recognized by Git as your home directory.
How to Verify the Setup
Verification is simple:
- Enter a Git repository under your work folder:
1cd ~/dev/work/some-work-repo- Check the active
user.email:
1git config user.emailThe output should be your work email (<your-work-email>).
- Enter a Git repository under your personal folder:
1cd ~/dev/personal/some-personal-repo- Check the active
user.emailagain:
1git config user.emailThe output should be your personal email (<your-personal-email>).
With this setup, you solve the multi-identity issue once and for all. As long as projects live in their designated parent directories, Git picks the right identity automatically.
Note This setting applies only to future commits and will not alter any existing commit history. To update older commits, you still need methods like
git rebaseorgit commit --amenddiscussed earlier.
Advanced Setup: Integrating SSH Key Management
Now my SSH config only has authorization for my github.com-personal account.
In individual Git repositories, I also have local
[user]settings (some do, some don’t).How can I migrate this step by step into clean “Conditional Includes”?
Refactoring SSH Configuration (~/.ssh/config)
To isolate work and personal assets cleanly, define explicit, symmetric Host aliases in ~/.ssh/config bound to their corresponding keys:
1Host github.com-work
2 HostName github.com
3 User git
4 IdentityFile ~/.ssh/id_ed25519_work
5 IdentitiesOnly yes
6
7Host github.com-personal
8 HostName github.com
9 User git
10 IdentityFile ~/.ssh/id_ed25519_personal
11 IdentitiesOnly yesThe advantage here is that work and personal keys are fully symmetric and independent, eliminating reliance on a global fallback while keeping the structure straightforward.
Updating Git Identity Files: Symmetric URL Rewriting
Next, configure “symmetric bidirectional URL rewriting” inside your respective Git identity configs.
- Update the work identity config (
~/.gitconfig-work):
1[user]
2 name = Your Work Username
3 email = <your-work-email>
4
5# Under work directory, automatically rewrite standard [email protected]: to github.com-work
6[url "[email protected]:"]
7 insteadOf = git@github.com:- Update the personal identity config (
~/.gitconfig-personal):
1[user]
2 name = Your Personal Username
3 email = <your-personal-email>
4
5# Under personal directory, automatically rewrite standard [email protected]: to github.com-personal
6[url "[email protected]:"]
7 insteadOf = git@github.com:This url...insteadOf rule is incredibly powerful: whether in work or personal directories, you can clone repositories using standard GitHub URLs like git clone [email protected]:org/repo.git. Git rewrites it on the fly to github.com-work or github.com-personal based on the active path, automatically routing to the right SSH key.
Clean Up: Removing Legacy Per-Repository Configurations
The final step is to purge existing per-repository [user] settings and let the automated system take over.
- Remove local
userconfigurations:
1# --local affects only the .git/config file inside the current project
2git config --local --unset-all user.name
3git config --local --unset-all user.emailAfter this, the repository no longer carries local overrides.
- (Recommended) Normalize remote URLs:
With url...insteadOf in place, you no longer need hardcoded github.com-personal URLs in your remotes. Switching back to standard addresses makes repos portable:
1# Check current remote url
2git remote -v
3# output: origin [email protected]:CheerChen/konakore.git (fetch)
4# ...
5
6# Update to standard URL
7git remote set-url origin [email protected]:CheerChen/konakore.git
8
9# Verify update
10git remote -v
11# output: origin [email protected]:CheerChen/konakore.git (fetch)
12# ...- Final Verification:
Inside the project directory, run:
1# Check which email Git will use
2git config user.email
3
4# Test if push/pull still works with the correct SSH key
5git fetchRepeat this cleanup across your other repositories. All Git actions will adapt to the parent folder automatically with zero manual switching needed.
Modern Upgrade: Zero-Friction Isolation in the Era of gh and AI Agents
After sorting out Git conditional includes and SSH keys, I thought the problem was solved permanently. However, as I started using the GitHub CLI (gh) extensively from the command line, alongside AI coding agents like Claude Code and Devin to execute tasks, a new issue quickly popped up:
While in a personal project directory, I asked an AI agent to open a PR. To my dismay, gh defaulted to my corporate GitHub account and opened the PR under my company identity!
My initial instinct was to add instructions to the agent’s prompt guidelines (such as CLAUDE.md or AGENTS.md):
1## gh multi-account auto-select
2
3When running any gh command, manually specify the token based on current working directory:
4- `~/dev/personal/` -> GH_TOKEN="$(gh auth token --user personal-user)" gh <command>
5- `~/dev/work/` -> GH_TOKEN="$(gh auth token --user work-user)" gh <command>I assumed this would be foolproof. But after using it for a while, three major pain points surfaced:
- Useless for humans in the terminal: When I manually type
gh pr createorgh repo viewin my shell, prompt instructions do nothing. The CLI still used whatever default account was active, resulting in frequent mistakes. - Wastes agent context window: Forcing token-injection rules into system prompts wastes context tokens and distracts the model from core coding problems.
- Maintenance nightmare across multiple tools: Claude Code (
CLAUDE.md), Devin (AGENTS.md), Codex, Cursor (.cursorrules)—every agent has its own configuration. Repeating and syncing these rules across every CLI tool and agent config is tedious and error-prone.
Realizing that relying on prompt instructions is fragile and high-maintenance, I consulted Gemini again:
Prompt rules for agents are painful to maintain and eat context tokens, and they don’t fix manual terminal usage. Is there a way to auto-switch gh accounts based on directory at the Shell level?
This is a classic modern pain point!
Git’s includeIf is scoped strictly within Git itself and only affects git commands. GitHub CLI (gh), however, is an independent tool that maintains a single active account per host (such as github.com). When you run gh auth status, only one account has Active account: true.
The best engineering principle here: Do not test LLMs with prompts, and do not test humans with memory. Intercept physically at the Shell environment layer.
We can achieve seamless isolation using a transparent Shell wrapper for gh.
Writing the Transparent gh Wrapper Script
Place an executable named gh in a directory prioritized in your PATH (such as ~/bin/gh), having it inject the appropriate GH_TOKEN into the real /opt/homebrew/bin/gh based on the current working directory ($PWD):
1#!/bin/bash
2# Automatically select GitHub account token based on current working directory
3# Directory mapping:
4# ~/dev/personal/ -> personal account
5# ~/dev/work/ -> work account
6
7# Absolute path to real Homebrew-installed gh binary
8REAL_GH="/opt/homebrew/bin/gh"
9
10case "$PWD/" in
11 *"$HOME/dev/work/"*)
12 TARGET_USER="work-user"
13 ;;
14 *"$HOME/dev/personal/"*)
15 TARGET_USER="personal-user"
16 ;;
17 *)
18 TARGET_USER=""
19 ;;
20esac
21
22if [ -n "$TARGET_USER" ]; then
23 TOKEN="$("$REAL_GH" auth token --user "$TARGET_USER" 2>/dev/null)"
24 if [ -n "$TOKEN" ]; then
25 export GH_TOKEN="$TOKEN"
26 fi
27fi
28
29exec "$REAL_GH" "$@"Make the script executable: chmod +x ~/bin/gh.
Verification and Results Comparison
Once configured, verify behavior across directories to observe the difference between native gh and the wrapper:
-
Verify personal directory (terminal and agents alike):
1cd ~/dev/personal/my-repo 2gh api user --jq .login 3# Native gh: returns default active account (e.g. work-user, risking wrong account) 4# With Wrapper: returns personal-user (automatically matches personal token) -
Verify work directory (terminal and agents alike):
1cd ~/dev/work/company-repo 2gh api user --jq .login 3# Native gh: returns default active account 4# With Wrapper: returns work-user (automatically matches work token) -
Verify AI Agent subshell execution (non-interactive subshell):
1zsh -c "cd ~/dev/personal/my-repo && gh api user --jq .login" 2# Output: personal-user
Now, whether typing gh manually in the terminal or letting an AI agent run child processes in the background, the system intercepts calls via this wrapper based on directory—achieving frictionless bidirectional isolation.
Crucial Details: Subshell and Zsh Startup Priorities
When implementing this shell wrapper in practice, two subtleties regarding shell environment variable inheritance are critical. Overlooking them can result in the wrapper working in your interactive terminal while background AI agents silently bypass it:
1. Non-Interactive Subshells Require ~/.zshenv
When AI coding agents like Claude Code or Devin run shell commands via child processes (e.g. exec("gh ...")), they spawn non-interactive subshells.
Under Zsh:
~/.zshrc: Loaded only in interactive shells (e.g., standard interactive terminal windows).~/.zshenv: Always loaded on any shell startup, including non-interactive subshells.
If you export your PATH only in ~/.zshrc, interactive terminals intercept calls properly, but background agents bypass the wrapper and run the default /opt/homebrew/bin/gh.
Therefore, make sure ~/bin is prepended inside ~/.zshenv:
1# Prepend PATH in ~/.zshenv so subshells and agents use the wrapper
2export PATH="$HOME/bin:$HOME/.local/bin:$PATH"2. Resetting PATH Preemption from brew shellenv
If your ~/.zshrc executes eval "$(/opt/homebrew/bin/brew shellenv)", Homebrew will prepend /opt/homebrew/bin to the front of PATH.
Be sure to re-prepend ~/bin to PATH after brew shellenv runs:
1# ~/.zshrc
2eval "$(/opt/homebrew/bin/brew shellenv)"
3
4# Ensure ~/bin stays ahead of /opt/homebrew/bin
5export PATH="$HOME/bin:$HOME/.local/bin:$PATH"Summary
With this end-to-end setup, we not only fixed past commit mistakes but also established an automated multi-identity system:
- Emergency Fix Layer: Use
git commit --amendor the standard toolgit-filter-repoto rapidly clean up pushed commit history. - Git Commit / SSH Identity Layer: Use
includeIfandinsteadOfin~/.gitconfigto automatically match commit emails and SSH keys based on project path. - GitHub CLI / AI Agent Layer: Use the
~/bin/ghtransparent wrapper combined with~/.zshenvenvironment interception to physically isolate accounts for both command-line terminals and AI agents like Claude Code and Devin.
Offloading account switching to shell-level mechanics ensures that as long as projects are in the right directory, every manual commit, PR creation, or automated API call from an agent uses the correct identity—with no confusion or accidental cross-account leakage.
Hi, I’m CheerChen.