Per‑repo SSH wrappers: the tiny habit that stopped me pushing with the wrong key (and the submodule that broke it)

I started using a per-repo SSH wrapper so each project uses the right key. It cut accidental pushes and access headaches — but submodules and CI forced ugly tradeoffs.

Written by: Arjun Malhotra

A laptop on a wooden desk with a coffee cup and a notepad
Photo by Christin Hume on Unsplash

It was 9:12am and I’d already done two dumb things: push a half-baked feature to a client repo with my personal SSH key, and then realize Jenkins’ CI didn’t have permission to fetch a private submodule so the build died in prod. The client pinged. My inbox lit up. I sat there, forehead on the laptop, thinking: this should not be this fragile.

I have three Git identities, two GitHub orgs, and one legacy client that still uses a private GitLab server reachable only over a bastion. On a startup laptop, with SSH agent forwarding, .ssh/config globs, and a habit of copying keys between machines, the wrong key walking into the wrong push was the predictable kind of disaster that repeats until you harden something.

Global ssh config was convenient until convenience became the problem. I needed something small, repo-local, and impossible to ignore.

Why global SSH config failed me (again)

What I actually built (and how it looks) I started with a one-file approach in each repo I care about: a tiny ssh wrapper plus a one-line git config. No global edits, no extra tools, zero rupees.

Repo layout (kept minimal):

The wrapper is tiny and explicit (illustrative, not exact):

Why this works for me:

A real example of my wrapper (conceptual)

The day it failed (the honest tradeoff) Two months after I rolled this out, we hit a morning outage. A release job pulled a private submodule hosted in a different company account. Locally, my wrapper worked because I had a symlink ~/.ssh/client_key -> /secure/wherever. In CI, the job ran on our self-hosted runner which didn’t have that key and didn’t pick up the repo wrapper because submodule fetches in our pipeline ran in a separate step that reset environment variables.

Result: the deploy staged, then died during the asset build. Our rollback was messy and cost a few late‑night coffees. The root cause: I’d assumed the wrapper would be used end-to-end. It wasn’t. CI, submodules, and cross-repo fetches need explicit configuration.

What I changed after the outage

Practical rules I actually follow now

Context for Indian teams This pattern helped me while working with satellite contractors in Pune and a legacy client who forces traffic through a bastion. It also fits remote-work realities: my home internet in a Chennai flat and a coworker on a Mumbai ISP both behaved the same because the repo told them which key to expect. For startups with locked corporate laptops and MDM, the wrapper often ends up as documentation — but documentation that reduces tribal knowledge is still valuable.

Final takeaway: small, visible constraints beat global magic The per-repo SSH wrapper isn’t sexy. It won’t replace proper access controls or short-lived credentials. But it stopped my wrong-key pushes overnight because the project started explicitly stating its expectations.

If you try it, expect one ugly edge: CI and submodules. Don’t assume your wrapper is an automatic fix. Make your CI the source of truth for repo access and let the wrapper be the human‑facing contract. That tiny change — explicit, local, and noisy when misconfigured — is the habit that saved my mornings.