← Back to Blog

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 tfenv and 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:

  1. Same page in GitHub settings → find the key you just added
  2. Click Configure SSO
  3. Click Authorize next to the org name
  4. 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:

  1. TFENV_TERRAFORM_VERSION environment variable
  2. .terraform-version in the current directory
  3. .terraform-version in any parent directory ← the folder-level pin
  4. ~/.terraform-version (home-level default)
  5. 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:

  1. New folder: ~/Documents/client_b/repos/
  2. New SSH key: ~/.ssh/client_b_github
  3. New SSH config entry: Host github.com-client_b
  4. New .terraform-version if their stack uses Terraform
  5. New includeIf block 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 filename
  • ssh-add --apple-use-keychain ~/.ssh/client_a_github, load the key into ssh-agent and store the passphrase in macOS Keychain
  • chmod 600 ~/.ssh/config, lock down SSH config file permissions
  • pbcopy < file, copy a file's contents to the clipboard
  • pbpaste, print clipboard contents (the reverse of pbcopy)
  • ssh -T git@github.com-client_a, test SSH auth using your config alias
  • git clone git@github.com-client_a:org/repo.git, clone using the SSH alias
  • git config user.email "...", set the email used for commits in this repo
  • git config user.name "...", set the name used for commits in this repo
  • brew install tfenv, install the Terraform version manager
  • tfenv install 1.14.0, download a specific Terraform version
  • tfenv use 1.14.0, select that version as your default
  • echo "1.14.0" > .terraform-version, pin a folder (and everything beneath it) to a version
  • brew install pre-commit, install the pre-commit framework
  • pre-commit install, wire pre-commit into the current repo's git hooks
  • pre-commit run --all-files, run all hooks against every file in the repo
  • python3 -m venv .venv, create a Python virtual environment
  • source .venv/bin/activate, activate the venv
  • pip install <package>, install a package into the active venv
  • deactivate, exit the venv