Sets up who-you-are for git: identity, keys, auth, signing, and the config
defaults core git developers actually use. Owns ~/.gitconfig (via
git config --global, inherently idempotent) and ~/.ssh/config's GitHub
block.
When NOT to use
- Git workflows (rebase, merge, commit messages) → native competence
- Installing git/gh binaries →
dev-cli-tools - Repo-side settings (branch protection, secrets) → not a machine-setup task
- Touch ID / FileVault →
macos-security-baseline
Safety rails
- Never overwrite an existing key. Check
ls ~/.ssh/first; creating overid_ed25519destroys access. New purpose → new filename. - Passphrases and tokens never appear in chat, files, or command args; auth
flows are browser-based (
gh auth login) or interactive (ssh-keygenprompts). - Ask for the user's real name/email — never invent or reuse placeholder identity values.
- Existing
user.email/user.namepresent? Show current values and confirm before changing.
Workflow
1. Identity + config defaults
git config --global user.name "<ask the user>"
git config --global user.email "<ask the user>"
git config --global init.defaultBranch main
git config --global fetch.prune true
git config --global rebase.autoStash true
git config --global push.autoSetupRemote true # git >= 2.37; kills --set-upstream dance
git config --global rerere.enabled true
git config --global merge.conflictstyle zdiff3 # git >= 2.35
git config --global diff.colorMoved plain
git config --global branch.sort -committerdate
git config --global column.ui auto
git config --global help.autocorrect prompt
# Editor: ASK which editor the user has, verify the command exists, then e.g.:
command -v code >/dev/null && git config --global core.editor "code --wait"
# Workflow choice, not universal - ask if the team merges on pull:
git config --global pull.rebase true
Rationale per key: references/git-config.md.
2. Global gitignore
Never truncate an existing file — append only missing entries:
[ -f ~/.gitignore_global ] && cp ~/.gitignore_global ~/.gitignore_global.bak.$(date +%Y%m%d%H%M%S)
for e in .DS_Store '*.swp' .env node_modules/ __pycache__/ .venv/ .idea/ .vscode/; do
grep -qxF "$e" ~/.gitignore_global 2>/dev/null || echo "$e" >> ~/.gitignore_global
done
git config --global core.excludesfile ~/.gitignore_global
3. SSH key + Apple Keychain
First inspect what already exists — both keys and effective config:
ls ~/.ssh/id_* 2>/dev/null # existing keys? pick ONE with the user
ssh -G github.com | grep -Ei '^(identityfile|identityagent)' # existing config/1Password agent?
If ssh -G shows a 1Password IdentityAgent or an existing github.com
block pointing at another key, stop and merge deliberately (edit that
block with the user) — appending a second Host github.com block does not
override the first. 1Password users skip key generation entirely (its
agent + op-ssh-sign handle auth and signing; configure per its docs).
Otherwise, settle on ONE key path and use it consistently everywhere below (existing key → reuse; new: only into a free filename):
KEY=~/.ssh/id_ed25519 # the agreed path - substitute throughout
[ -f "$KEY" ] || ssh-keygen -t ed25519 -C "<email>" -f "$KEY" # SET A PASSPHRASE
Wire agent + Keychain so the passphrase is asked once ever
(~/.ssh/config; back the file up first if it exists):
Host github.com
AddKeysToAgent yes
UseKeychain yes
IdentityFile <the agreed KEY path>
/usr/bin/ssh-add --apple-use-keychain "$KEY"
Use Apple's /usr/bin/ssh-add — Homebrew/other OpenSSH builds don't have
--apple-use-keychain. After reboot the agent is empty but the config block
reloads the key transparently on first use.
4. GitHub auth
gh auth login # browser flow; SSH protocol; can create+upload the key for you
gh auth status
ssh -T git@github.com # expect: "Hi <user>! You've successfully authenticated..."
No gh? pbcopy < ~/.ssh/id_ed25519.pub → GitHub → Settings → SSH and GPG
keys → New SSH key (type: Authentication). First ssh -T shows a host
fingerprint prompt — verify against GitHub's published fingerprints page
before typing yes.
5. SSH commit signing (opt-in — Verified badge)
An explicit choice, not a default: global commit.gpgsign makes every
commit fail if the key/agent is unavailable. Confirm the user wants it
(and check nothing is configured already: git config --global --get-regexp '^(gpg|commit\.gpgsign|tag\.gpgsign)').
git config --global gpg.format ssh
git config --global user.signingkey "$KEY.pub" # the agreed key from step 3
git config --global commit.gpgsign true
git config --global tag.gpgsign true
Upload the same public key a second time on GitHub with type
Signing (gh ssh-key add "$KEY.pub" --type signing) — authentication
and signing entries are separate; skipping this leaves commits
"Unverified".
Local verification needs an allowed-signers file (idempotent; use the
identity email from step 1, not directory-dependent git config):
LINE="<email from step 1> $(cat "$KEY.pub")"
grep -qxF "$LINE" ~/.ssh/allowed_signers 2>/dev/null || echo "$LINE" >> ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
Test: commit in a scratch repo, git verify-commit HEAD / git log --show-signature -1. Requires git ≥ 2.34 (SSH signing) — CLT git qualifies.
6. Work/personal identity split (when asked)
# ~/.gitconfig
[includeIf "gitdir:~/work/"]
path = ~/.gitconfig-work
[includeIf "gitdir:~/personal/"]
path = ~/.gitconfig-personal
Each included file sets its own user.email (and optionally signingkey).
The trailing / matches the tree recursively. Remote-based alternative
(git ≥ 2.36): [includeIf "hasconfig:remote.*.url:git@github.com:acme/**"].
Verify from inside each tree: git config user.email.
7. Verify
sh "${CLAUDE_SKILL_DIR}/scripts/check.sh"
Output spec
Done means: the managed keys read back correctly —
git config --global --show-origin --get-regexp \
'^(user\.(name|email|signingkey)|init\.defaultbranch|core\.(editor|excludesfile)|gpg\.|commit\.gpgsign|tag\.gpgsign)'
(targeted query, not --list — a full dump can expose credential-bearing
URLs into the transcript) — ssh -T git@github.com prints the "Hi
!" greeting (exit code 1 is the success case), a signed test
commit verifies locally (if signing was chosen), the passphrase survives
a new shell, and no existing key or config block was touched without
consent.
Gotchas
- "permission denied (publickey)" with a working key usually = key not
loaded (run the ssh-add line) or the repo remote uses HTTPS while auth
was set up for SSH (
git remote -vto check). - Signing on but key not uploaded as Signing type → GitHub shows Unverified; the auth entry does not count.
UseKeychainon a passphrase-less key errors on some setups — omit it then (but keys should have passphrases).- Two GitHub accounts (not just two emails) need more than includeIf:
per-account keys +
Host github-workaliases withIdentitiesOnly yesand matching remote URLs — say so instead of half-configuring it. gh auth login's token normally lands in the macOS keychain but can fall back to a plaintext~/.config/gh/hosts.yml— promise the browser flow (no pasted tokens), not a storage guarantee.- CLT git lags upstream slightly (updates only with Xcode releases). Fine
for everything here;
brew install gitif newer features are needed — brew's PATH position shadows it automatically.