The Quiet Power of insteadOf in .gitconfig

Git has one line you can put in .gitconfig that rewrites every URL it touches before doing anything with it. Clone, fetch, push, submodule init, doesn’t matter. Set url.<base>.insteadOf once and any command that would have used the old string uses the new one instead.

Why bother

Two problems show up on almost every machine that touches more than one Git host or more than one identity. First, multiple SSH keys: a work GitHub org needs one key, personal projects need another, and the usual fix (an SSH Host alias you have to remember to type) fails the moment you type the normal URL out of habit. Second, HTTPS clone URLs are what every host shows you by default, and pushing over HTTPS means typing a token or fighting 2FA on every push. insteadOf fixes both by moving the decision out of your fingers and into a rule you write once.

Different SSH key per org, automatically

Give SSH the alias first:

# ~/.ssh/config
Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_work

Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_personal

Then tell Git to redirect any URL under the work org to that alias:

# ~/.gitconfig
[url "git@github-work:"]
    insteadOf = [email protected]:acme-corp/


[url "git@github-personal:"]
    insteadOf = [email protected]:joe-the-super-dev/

git clone [email protected]:acme-corp/backend.git gets rewritten to git@github-work:backend.git before Git opens a connection. You type the URL you’d type anyway. The org name in the URL decides which key gets used, not your memory.

Force HTTPS to SSH

[url "[email protected]:"]
    insteadOf = https://github.com/

Paste an HTTPS URL from GitHub’s UI, get an SSH connection instead. This also fixes stale HTTPS remotes already sitting in old clones on your machine, no git remote set-url cleanup needed.

Want reads over HTTPS (anonymous, no key management, good for CI) but writes over SSH? Use pushInsteadOf instead of insteadOf:

[url "[email protected]:"]
    pushInsteadOf = https://github.com/

Fetches stay on HTTPS. Pushes get redirected to SSH and use your key. Same repo, two protocols, no manual switching.

Setting it up without touching the file

Every example above edits .gitconfig by hand, but git config writes the same lines for you, which matters the moment you want a script or a hook to set this up instead of a human with a text editor.

git config --global url."git@github-work:".insteadOf "[email protected]:acme-corp/"
git config --global url."[email protected]:".insteadOf "https://github.com/"

Drop --global and the same command writes to the current repo’s .git/config instead of your user-wide one. That’s local only though: .git/config never gets committed, so it won’t reach anyone who clones the repo. If you want the rewrite to happen automatically for every teammate the moment they set up the project, run the command from somewhere that already executes on their machine, like a Composer hook:

{
  "scripts": {
    "post-install-cmd": [
      "git config url.\"[email protected]:\".insteadOf \"https://gitlab.company.com/\""
    ],
    "post-update-cmd": [
      "git config url.\"[email protected]:\".insteadOf \"https://gitlab.company.com/\""
    ]
  }
}

Both hooks matter. Composer treats install and update as separate lifecycles: post-install-cmd only fires after composer install, and post-update-cmd only after composer update. A teammate who happens to run update first would never see the rewrite if you’d only wired the installation hook.

Running the same git config line on every install is safe. It’s not --add, so it overwrites the single existing value instead of appending a new one. Fifty installations in a row still leave exactly one insteadOf line in .git/config. The same trick works from a postinstall npm script or a Makefile setup target, anywhere your project already runs a command on every checkout.

A few more rewrites worth stealing

You can invent your own short-hand prefixes too. Nothing stops a rule from mapping a made-up scheme onto a real host:

[url "https://github.com/"]
    insteadOf = gh:
[url "https://gitlab.com/"]
    insteadOf = gl:

git clone gh:torvalds/linux just works.

The same mechanism cleans up a dead host. If a team moved from git.old-domain.com to git.new-domain.com and forty scripts and READMEs still point at the old one, don’t touch any of them:

[url "https://git.new-domain.com/"]
    insteadOf = https://git.old-domain.com/

Every clone, every submodule, every CI job referencing the old host lands on the new one instead.

Companies running an internal proxy in front of GitHub (for caching, for a firewall, for compliance) use it to make github.com URLs resolve to the mirror without anyone noticing:

[url "https://mirror.internal.corp/github/"]
    insteadOf = https://github.com/

Developers keep writing normal GitHub URLs in scripts and docs. The traffic never leaves the building.

One detail worth knowing before you rely on any of this: the rewrite happens before Git resolves any URL, so a .gitmodules file pointing at https://github.com/... gets redirected the same way a manual clone would. That matters when a project’s submodules were added by someone on HTTPS and you work entirely over SSH.

Going further with includeIf

insteadOf rewrites URLs, but it’s still one flat set of rules for the whole machine. If you want the rules themselves to differ by folder, not just the key or protocol they resolve to, includeIf is the directive that hands you that. It loads a whole separate config file, but only when the current repo’s path matches a pattern.

# ~/.gitconfig
[includeIf "gitdir:~/work/"]
    path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
    path = ~/.gitconfig-personal

Anything under ~/work/ picks up ~/.gitconfig-work, which can carry its own user.email, its own commit signing key, and its own insteadOf block for the work SSH alias. Anything under ~/personal/ picks up the personal file instead. You get the same split as trick one, work identity here, personal identity there, but driven by where the repo lives on disk rather than which org name shows up in the URL.

This is also what finally kills the “wrong commit author” problem. Without it, user.email comes from your global .gitconfig (so every personal repo silently gets your work email unless you remember to override it) or you end up running git config user.email [email protected] by hand in every single personal clone the day after you set it up. Put user.name and user.email in the bucket files instead:

# ~/.gitconfig-work
[user]
    name = Jane Doe
    email = [email protected]

# ~/.gitconfig-personal
[user]
    name = Jane Doe
    email = [email protected]

Every new repo under the matching folder gets the right author automatically the moment you clone it. Nothing to set per repo, nothing to forget.

Nothing stops you from going one level finer and pointing a single high-stakes client repo at its own include file, so that one project gets its own signing key or its own mirror rewrite without touching the rest of ~/work/.

One thing to watch

insteadOf matches on prefix, and Git applies the longest matching prefix when rules overlap. A broad rule (https://github.com/) and a narrow one (https://github.com/acme-corp/) coexist fine, but two rules at the same specificity can rewrite a URL to somewhere you didn’t expect. If a clone ever hits a host you didn’t type, check the config before assuming the network is broken:

git config --get-regexp url\..*insteadof

It’s almost always a rule doing exactly what you told it to, just not what you meant.

Leave a Reply

Your email address will not be published. Required fields are marked *