Setting Up a New Client on Your MacBook (Without Wrecking the Others)
If you do consulting work, your laptop is a museum of half-configured tools and "what client was this for again?" git remotes. Every new engagement adds another layer, different GitHub orgs, different email addresses on commits, different Terraform versions, different VPNs, different... well, everything.
This is the simple setup I use to keep clients cleanly separated on a single Mac. Specifically, this post covers:
- One SSH key per client, with a config alias so you never get them mixed up
- Per-repo (or per-folder) git identity so commits show the right email
- Per-client Terraform version pinning with
tfenvand a single config file - Pre-commit hooks so your commits pass CI before you even push
- Per-project Python venvs so dependencies never collide
Once you've done it once, adding the next client takes about ten minutes.
The folder structure I use
~/Documents/
client_a/
repos/
.terraform-version
project-1/
project-2/
client_b/
repos/
.terraform-version
...
Each client gets a folder. Each client's repos/ directory holds all their git repos plus a single .terraform-version file (more on that later). This pattern is the foundation everything else hangs off of.
SSH: one key per client
The mistake people make is using the same SSH key for everything. It works fine until you need to revoke access from one client without nuking your access to the others, or until SAML SSO comes into the picture and refuses to play.
Generate a client-specific key
ssh-keygen -t ed25519 -C "your-client-email@example.com" -f ~/.ssh/client_a_github
The -f flag is the important part. It gives the key a unique filename. Default-named keys (the ones SSH creates if you don't pass -f) collide; named ones don't.
Add it to the agent (and macOS Keychain)
ssh-add --apple-use-keychain ~/.ssh/client_a_github
The Keychain flag stores the passphrase so you don't get prompted every session.
Tell SSH which key to use for which host
Create or open ~/.ssh/config:
touch ~/.ssh/config
chmod 600 ~/.ssh/config
Add an entry:
Host github.com-client_a
HostName github.com
User git
IdentityFile ~/.ssh/client_a_github
IdentitiesOnly yes
IdentitiesOnly yes is not optional. Without it, SSH will offer all your keys to GitHub, the wrong one will get picked, and you'll get error messages that don't actually say "you used the wrong key."
Add the public key to GitHub
Copy it to your clipboard:
pbcopy < ~/.ssh/client_a_github.pub
Then go to github.com/settings/keys, click New SSH key, paste, save.
If the org uses SAML SSO
Most enterprise GitHub orgs enforce SAML SSO. After adding the key, you also need to authorize it for that organization:
- Same page in GitHub settings → find the key you just added
- Click Configure SSO
- Click Authorize next to the org name
- Sign in through their identity provider when prompted
Without this step, you can authenticate to GitHub but every push or clone against the org's repos will fail with a SAML error.
Test it
ssh -T git@github.com-client_a
Note the -client_a suffix on the hostname. That's how SSH knows which key to use. Expected output: Hi <username>! You've successfully authenticated...
Clone using the alias
When the client gives you a clone URL like git@github.com:client-org/repo.git, change github.com to github.com-client_a:
git clone git@github.com-client_a:client-org/repo.git
That's it for SSH. Repeat for each client with a new label.
Heads-up: the alias is SSH-only
This trips a lot of people up. Your github.com-client_a alias lives in ~/.ssh/config. Only the SSH layer knows about it. If you try to use it in an https:// URL like https://github.com-client_a/..., your machine tries to resolve github.com-client_a as a real DNS hostname, fails, and gives you Could not resolve host.
The alias works exclusively with SSH-form URLs:
git@github.com-client_a:org/repo.git
Not HTTPS-form URLs. Ever. If you accidentally grab an HTTPS clone URL from GitHub's UI, swap the whole https://github.com/ prefix for git@github.com-client_a: (and remember the colon, not a slash, after the hostname).
Git identity: don't commit as the wrong person
git config --global user.email is fine for personal projects, but for client work you almost certainly need a different email per client. Setting it per-repo keeps things clean:
cd path/to/cloned/repo
git config user.email "your-client-email@example.com"
git config user.name "Your Name"
Without --global, those settings only apply to that repo. Your other clients are unaffected.
Want it automatic?
Use includeIf in your global gitconfig to set identity based on directory. Add this to ~/.gitconfig:
[includeIf "gitdir:~/Documents/client_a/"]
path = ~/.gitconfig-clienta
Then create ~/.gitconfig-clienta:
[user]
email = your-client-email@example.com
name = Your Name
Now any repo under ~/Documents/client_a/ automatically uses that identity. No more forgetting to set it.
Terraform version pinning with tfenv
Different clients pin different Terraform versions. Some are on 1.5 because they haven't upgraded. Some are on whatever was current six months ago. Switching versions by hand is a nightmare. tfenv solves this.
Install tfenv
brew install tfenv
Important: if you already have terraform installed via Homebrew, uninstall it first. They conflict on the terraform symlink:
brew uninstall terraform
brew link tfenv
(Aside: HashiCorp pulled Terraform from Homebrew core after their 2023 license change, so any brew-installed terraform on your machine is an orphan anyway. Good time to clean it up.)
Install and select a version
tfenv install 1.14.0
tfenv use 1.14.0
terraform version should now report 1.14.0.
The killer feature: folder-level version inheritance
Drop a single file:
echo "1.14.0" > ~/Documents/client_a/repos/.terraform-version
tfenv walks up the directory tree looking for .terraform-version. Any repo under ~/Documents/client_a/repos/, current or future, automatically uses 1.14.0. No per-repo configuration needed.
When you set up the next client, make a new parent folder with its own .terraform-version. Each client's repos automatically pick up the right version when you cd into them.
How tfenv decides which version to use
Highest priority wins:
TFENV_TERRAFORM_VERSIONenvironment variable.terraform-versionin the current directory.terraform-versionin any parent directory ← the folder-level pin~/.terraform-version(home-level default)- The global default set by
tfenv use
One caveat
The parent-folder .terraform-version is local to your machine. It doesn't propagate to teammates. If you want the team to be on the same version, also add a constraint inside the actual repo (versions.tf or wherever the terraform {} block lives):
terraform {
required_version = "~> 1.14.0"
}
~> 1.14.0 allows patch updates (1.14.1, 1.14.2) but blocks 1.15. Strict enough to keep things safe, loose enough to avoid daily bumps.
Pre-commit hooks
Most modern repos, especially Terraform repos, ship with a .pre-commit-config.yaml file at the root. This file lists checks that run automatically every time you git commit: formatting, syntax validation, secret detection, etc. If you don't have pre-commit installed locally, your commits skip these checks, but the same checks run in CI when you open a PR. Result: your PR fails for things you could have caught in three seconds locally.
Install once globally
brew install pre-commit
Wire it up per-repo
After cloning each repo:
cd path/to/cloned/repo
pre-commit install
That's it. The framework adds a script to .git/hooks/pre-commit that fires on every commit. The actual hook tools get downloaded and cached the first time they run, then subsequent commits are fast.
Day-to-day workflow
You don't run pre-commit explicitly. It triggers automatically when you git commit:
git add .
git commit -m "Update ECS task definition"
# ↑ hooks run here against staged files
If everything passes, the commit goes through. If a hook fails (or auto-fixes something like formatting), you'll see output explaining what happened. Review, re-stage with git add, and commit again.
Bulk-checking an entire repo
Useful for first-time setup of an existing repo, or when you want a sanity check before opening a PR:
pre-commit run --all-files
This runs every hook against every file in the repo, not just staged ones. Expect the first run on an established repo to produce a wall of red, pre-existing formatting issues, missing newlines at end of file, etc. Auto-fix what's auto-fixable, fix what isn't manually, commit the cleanup.
Tools the hooks may need
Look at .pre-commit-config.yaml to see what hooks the repo runs. Some rely on local binaries being installed:
| Hook | Tool you need |
|---|---|
terraform_fmt / terraform_validate |
terraform (you have via tfenv) |
terraform_docs |
brew install terraform-docs |
tflint |
brew install tflint |
checkov |
brew install checkov |
tfsec |
brew install tfsec |
Anything from pre-commit/pre-commit-hooks |
nothing, built into the framework |
If a hook fails with a command not found error, that's the missing piece.
Python: a venv per project
Same isolation principle, applied to Python. Don't pip install packages globally. Every project gets its own virtual environment that holds its dependencies, completely separate from every other project. No version conflicts, no "wait, why did upgrading X break Y?" surprises.
cd path/to/project
python3 -m venv .venv # creates .venv/ in the project root
source .venv/bin/activate # activates it for this shell
pip install fastapi # installs into the venv only
While the venv is active, your shell prompt picks up a (.venv) prefix, and python / pip resolve to the venv's binaries, even though those commands don't exist globally on a fresh macOS install. When you're done:
deactivate
Add .venv/ to the project's .gitignore so the folder never gets committed.
A useful alias
In ~/.zshrc:
alias activate='source .venv/bin/activate'
Then in any project folder you can just type activate and it works. (The activation command is the same in every project because the path is relative.)
Adding the next client
The whole setup repeats with one new identifier per client:
- New folder:
~/Documents/client_b/repos/ - New SSH key:
~/.ssh/client_b_github - New SSH config entry:
Host github.com-client_b - New
.terraform-versionif their stack uses Terraform - New
includeIfblock if you want automatic git identity
Maybe ten minutes. Then you can cd into any client's folder and your machine is using the correct client-specific config. Per-repo stuff (pre-commit install, venvs, repo-local git identity if you skipped includeIf) you do once after each git clone.
Why bother
There's a temptation to skip all this and just rely on memory. Don't. The first time you accidentally push code to Client A using the SSH key from Client B (because they're both loaded into the agent and SSH picked the wrong one), or commit with the wrong email and have to rewrite history in front of a client, you'll wish you'd taken twenty minutes up front.
Also: when an engagement ends, cleanup is one folder, one SSH key, one config block. Audit trail is clean. You sleep better.
The trick isn't any one tool. It's the idea of one identifier per client, applied consistently across SSH, git, Terraform, pre-commit, and Python. Once the convention is in place, adding or removing clients is quick and easy.
Commands Used
A quick reference of every command covered above.
ssh-keygen -t ed25519 -C "email" -f ~/.ssh/client_a_github, generate a new SSH key with a custom filenamessh-add --apple-use-keychain ~/.ssh/client_a_github, load the key into ssh-agent and store the passphrase in macOS Keychainchmod 600 ~/.ssh/config, lock down SSH config file permissionspbcopy < file, copy a file's contents to the clipboardpbpaste, print clipboard contents (the reverse ofpbcopy)ssh -T git@github.com-client_a, test SSH auth using your config aliasgit clone git@github.com-client_a:org/repo.git, clone using the SSH aliasgit config user.email "...", set the email used for commits in this repogit config user.name "...", set the name used for commits in this repobrew install tfenv, install the Terraform version managertfenv install 1.14.0, download a specific Terraform versiontfenv use 1.14.0, select that version as your defaultecho "1.14.0" > .terraform-version, pin a folder (and everything beneath it) to a versionbrew install pre-commit, install the pre-commit frameworkpre-commit install, wire pre-commit into the current repo's git hookspre-commit run --all-files, run all hooks against every file in the repopython3 -m venv .venv, create a Python virtual environmentsource .venv/bin/activate, activate the venvpip install <package>, install a package into the active venvdeactivate, exit the venv