1Password SSH Agent로 GitHub 다중 계정 관리하기
터미널에서는 되던 git push가 VS Code에서는 읽기 전용 키로 인증되어 실패했다. 1Password에는 필요한 SSH 키가 모두 있었지만, 키가 있다는 사실만으로 Git이 올바른 계정을 선택하지는 않았다.
ERROR: The key you are authenticating with has been marked as read only.
fatal: Could not read from remote repository.
이 문제를 해결하려면 세 가지를 구분해야 한다. SSH Agent는 개인 키로 서명하고, Git 설정은 사용할 계정을 고르며, GitHub CLI 토큰은 API 요청을 인증한다. 이 글은 macOS의 1Password SSH Agent와 OpenSSH를 사용하는 Git을 기준으로, 터미널과 VS Code에 같은 계정 선택 규칙을 적용하는 과정을 정리한다.
Techtaurant에 직접 작성한 Git 다중 계정 관리와 macOS에서 1Password SSH Agent로 여러 GitHub 계정 분리하기를 통합했다. 원문 게시일은 2026-08-02 한국시간이다. 두 글에서 겹치는 설명을 줄이고, 실패 처리와 worktree 사용 시 주의점을 보완했다.
먼저 인증 경로를 나누기
| 설정 | 결정하는 것 | 결정하지 않는 것 |
|---|---|---|
user.name, user.email | 커밋에 기록할 작성자 | GitHub 접속 계정 |
IdentityAgent, SSH_AUTH_SOCK | 서명을 요청할 SSH Agent | 저장소별로 사용할 키 |
IdentityFile, IdentitiesOnly | SSH 인증에 사용할 키 후보 | 커밋 작성자 |
GH_TOKEN | gh의 GitHub API 인증 | SSH remote의 git push 인증 |
커밋 이메일을 바꿔도 다른 SSH 키로 인증할 수 있고, gh auth status가 성공해도 git push는 실패할 수 있다. 확인할 때도 이 경로를 각각 검사해야 한다.
이 글에서는 개인 저장소를 ~/Github/personal/, 업무 저장소를 ~/Github/work/ 아래에 둔다. 계정명·이메일·공개 키 파일명은 예시이므로 실제 환경에 맞게 바꾼다. 기본 Git 인증에는 Nix나 ssh-tresor가 필요하지 않다. 토큰 암호화는 마지막의 선택 사항이다.
1. 현재 프로세스가 어느 Agent를 보는지 확인하기
원문에서 처음 만난 오류는 ssh-tresor list-keys의 No keys found in SSH agent였다. 1Password가 실행 중이어도 현재 도구가 다른 Agent 소켓에 연결되어 있으면 키가 보이지 않는다.
macOS에서 1Password의 SSH Agent를 활성화한 뒤, 현재 셸에 소켓을 지정한다.
export SSH_AUTH_SOCK="$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
ssh-add -l
예상하는 키의 fingerprint가 나오는지 확인한다. Fingerprint는 기기 ID가 아니라 공개 키의 식별자다. 같은 키라면 다른 Mac에서도 같은 fingerprint가 나온다.
이 환경변수는 현재 셸과 이후 실행한 자식 프로세스에 전달된다. 이미 실행된 VS Code나 Dock에서 시작한 앱의 환경까지 바꾸지는 않는다. Git GUI도 OpenSSH를 통해 같은 Agent를 사용하도록 ~/.ssh/config에 다음 설정을 추가한다.
Host github.com
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
기존 파일을 통째로 교체하지 말고, 이미 github.com에 적용되는 설정이 있는지 확인한다. OpenSSH의 대부분의 단일 값 옵션은 먼저 얻은 값을 사용하므로 구체적인 Host 블록은 일반적인 Host *보다 앞에 둔다. IdentityFile처럼 여러 값이 누적되는 옵션은 예외다.
IdentityAgent는 OpenSSH가 읽는 설정이고, SSH_AUTH_SOCK은 ssh-add나 ssh-tresor 같은 도구에도 필요하다. 두 설정의 소비자가 다르다. 다른 Git GUI를 사용한다면 1Password의 클라이언트 호환성 문서를 확인한다.
2. 저장소 경로로 커밋 작성자와 SSH 키 선택하기
1Password에서 개인용·업무용 SSH 키의 공개 키를 내려받아 다음처럼 보관한다. 개인 키를 내보낼 필요는 없다. 각 공개 키는 해당 GitHub 계정에 등록되어 있어야 한다.
~/.config/secrets/ssh/
├── github-personal.pub
└── github-work.pub
~/.config/git/
├── config
└── github/
├── index.inc
├── personal.inc
└── work.inc
아래는 ~/.config/git/config에 추가할 공통 설정이다. XDG_CONFIG_HOME을 별도로 지정했다면 그 경로의 git/config를 사용한다.
[user]
useConfigOnly = true
[include]
path = github/index.inc
user.useConfigOnly는 작성자 정보가 없을 때 Git이 이름이나 이메일을 추측하지 않게 한다. 다만 이미 다른 전역 설정에 작성자 정보가 있다면 그것을 사용할 수 있다. 계정 누락을 오류로 드러내려면 기존 ~/.gitconfig와 저장소 설정에 남은 기본 작성자도 확인해야 한다.
github/index.inc에는 계정별 디렉터리를 지정한다.
[includeIf "gitdir:~/Github/personal/"]
path = personal.inc
[includeIf "gitdir:~/Github/work/"]
path = work.inc
Git의 상대 include.path는 해당 설정을 선언한 파일을 기준으로 해석한다. gitdir: 패턴 끝의 /는 그 아래 경로까지 포함한다.
github/personal.inc:
[user]
name = Personal User
email = personal@example.com
[core]
sshCommand = ssh -o IdentitiesOnly=yes -i \"${HOME}/.config/secrets/ssh/github-personal.pub\"
github/work.inc:
[user]
name = Work User
email = work@example.com
[core]
sshCommand = ssh -o IdentitiesOnly=yes -i \"${HOME}/.config/secrets/ssh/github-work.pub\"
설정의 \"는 Git이 파싱한 뒤 SSH 실행 명령에 따옴표를 남기기 위한 것이다. 홈 경로에 공백이 있어도 공개 키 경로가 하나의 인자로 전달된다.
OpenSSH는 공개 키와 일치하는 개인 키의 서명을 Agent에 요청할 수 있다. IdentitiesOnly=yes는 설정하지 않은 Agent 키까지 무작정 제시하지 않도록 제한한다. 기존 SSH 설정에 다른 IdentityFile이 있다면 그 키도 후보가 될 수 있으므로 최종 설정을 확인한다. 이 방식은 1Password의 키 선택 안내에 설명되어 있다.
원격 주소는 계속 git@github.com:OWNER/REPOSITORY.git 형태를 쓴다. SSH 호스트 별칭으로 개인·업무 계정을 나누는 방법도 있지만, 여기서는 원격 주소를 바꾸지 않고 디렉터리를 계정 선택 기준으로 삼았다.
덮어쓰는 설정과 worktree 확인
Git 설정은 일반적으로 system → global → local → worktree → command 순으로 읽으며, 단일 값은 뒤의 설정이 우선한다. ~/.gitconfig는 기본 XDG 전역 파일보다 뒤에 읽힌다. worktree별 설정은 해당 확장이 활성화됐을 때 적용된다.
GIT_SSH_COMMAND 환경변수는 core.sshCommand보다 우선한다. 이전에 .envrc에 넣어 둔 SSH 덮어쓰기나 GIT_AUTHOR_*, GIT_COMMITTER_*가 있다면 계정별 설정과 충돌하지 않도록 정리한다. .envrc 변경은 검토 후 다시 direnv allow로 허용한다.
또한 gitdir:는 화면에 보이는 작업 폴더가 아니라 Git 디렉터리 위치를 기준으로 한다. Linked worktree에서는 원본 저장소의 .git/worktrees/...가 기준이 될 수 있다. worktree를 업무 폴더에 놓았다는 이유만으로 계정이 바뀐다고 가정하지 말고, 그 안에서 다음 절의 설정 출처를 확인한다. 자세한 매칭 규칙은 Git의 conditional includes 문서를 따른다.
3. 터미널과 VS Code에서 같은 설정인지 검증하기
개인 저장소와 업무 저장소 안에서 각각 실행한다.
git config --show-origin --get user.name
git config --show-origin --get user.email
git config --show-origin --get core.sshCommand
git remote get-url origin
작성자·공개 키·원격 주소가 같은 계정을 가리키는지 확인한다. Git 인증을 진단하는 데 전체 환경변수나 토큰을 출력할 필요는 없다.
개인 키를 지정했을 때의 SSH 설정은 다음처럼 확인한다. 업무 계정은 공개 키 파일명을 바꾼다.
ssh -G -o IdentitiesOnly=yes \
-i "$HOME/.config/secrets/ssh/github-personal.pub" git@github.com |
grep -E '^(user|hostname|identityagent|identityfile|identitiesonly) '
ssh -T -o IdentitiesOnly=yes \
-i "$HOME/.config/secrets/ssh/github-personal.pub" git@github.com
GitHub가 출력하는 Hi USERNAME!이 원하는 계정인지 본다. GitHub는 셸을 제공하지 않아 인증에 성공해도 종료 코드가 1일 수 있다. USERNAME/REPOSITORY가 나오면 계정 키가 아니라 deploy key인지 확인한다. GitHub 연결 테스트, 키 주체 확인
저장소 접근은 읽기 작업으로 먼저 확인한다.
GIT_TRACE=1 git ls-remote origin
Trace에서 의도한 공개 키가 SSH 명령에 들어가는지 확인한다. 읽기 성공이 push 권한까지 보장하지는 않는다. 로그를 공유할 때는 비공개 저장소 주소와 로컬 경로를 가린다.
VS Code는 Cmd+Q로 완전히 종료한 뒤 Dock에서 다시 실행하고 Source Control의 fetch를 확인한다. 기존 인스턴스의 환경을 재사용하는 경우가 있어 창만 닫았다 여는 것으로는 비교가 되지 않을 수 있다. Git 출력에서 실행 파일과 오류를 확인하고, 터미널의 결과와 비교한다. VS Code의 환경 상속 설명
| 증상 | 먼저 확인할 것 |
|---|---|
| Agent에 키가 없다고 나옴 | 현재 SSH_AUTH_SOCK, 1Password Agent 활성화·키 접근 상태 |
| 다른 계정 또는 읽기 전용 키로 인증됨 | 실제 공개 키 경로, 추가 IdentityFile, deploy key 등록 여부 |
| 터미널만 성공함 | VS Code의 Git 실행 파일, OpenSSH 설정, 이전 프로세스 환경 |
| 옮긴 저장소에서 계정 설정이 사라짐 | includeIf 패턴과 실제 Git 디렉터리 |
gh는 성공하지만 Git은 실패함 | API 토큰 인증과 SSH 인증을 분리해서 검사 |
4. 선택 사항인 GitHub CLI 토큰 암호화
여기까지로 Git의 다중 계정 설정은 끝이다. 아래 구성은 GitHub API 토큰을 평문 파일에 저장하고 싶지 않을 때 추가한다. 1Password CLI와 ssh-tresor, direnv가 필요하다.
gh의 API 인증에는 GH_TOKEN을 사용할 수 있다. 이는 저장된 gh 인증정보보다 우선한다. 새 clone에서 SSH 주소를 사용하도록 고르는 설정은 별개다.
gh config set git_protocol ssh --host github.com
이 설정은 기존 저장소의 remote를 바꾸거나 SSH 키를 선택하지 않는다. GitHub CLI 환경변수, config 설정
저장할 토큰과 복호화 키를 명시하기
ssh-tresor는 Agent의 서명을 이용해 비밀값을 암호화한다. 먼저 사용할 키를 확인한다.
ssh-tresor list-keys
ssh-keygen -lf "$HOME/.config/secrets/ssh/github-personal.pub"
두 명령의 fingerprint를 비교한 뒤 아래의 SHA256:REPLACE_WITH_FINGERPRINT를 바꾼다. op://VAULT/ITEM/FIELD도 자신의 1Password 비밀값 참조로 바꾼다. --key를 생략하면 기본 키 순서에 의존하므로 명시한다.
다음 예시는 Bash 스크립트로 저장해 실행한다. 기존 암호문이 있으면 덮어쓰지 않으며, 읽기나 암호화가 실패하면 임시 파일만 지우고 종료한다.
#!/usr/bin/env bash
set -euo pipefail
umask 077
dest="$HOME/.config/secrets/tresor/github-personal-token.tresor"
mkdir -p "${dest%/*}"
if [ -e "$dest" ]; then
printf 'Already exists: %s\n' "$dest" >&2
exit 1
fi
tmp=$(mktemp "${dest%/*}/.ssh-tresor.XXXXXX")
trap 'rm -f "$tmp"' EXIT
if ! op read --no-newline 'op://VAULT/ITEM/FIELD' |
ssh-tresor encrypt --armor --key 'SHA256:REPLACE_WITH_FINGERPRINT' > "$tmp"; then
printf 'Encryption failed; no destination file was created.\n' >&2
exit 1
fi
chmod 600 "$tmp"
mv "$tmp" "$dest"
trap - EXIT
ssh-tresor list-slots "$dest"
pipefail는 파이프라인의 실패를 종료 코드에 반영한다. 이 예제는 그 실패를 if로 처리해서 잘못된 출력이 목적지로 이동하지 않도록 한다. --armor는 암호문을 텍스트로 표현하는 옵션이며 암호화 강도를 높이지 않는다. ssh-tresor 사용법, 1Password CLI read
direnv에서 필요한 계정의 토큰만 불러오기
개인 저장소 상위 디렉터리의 .envrc 예시다. 업무 디렉터리는 별도의 토큰 파일을 지정한다.
unset GH_TOKEN
if ! token=$(ssh-tresor decrypt \
"$HOME/.config/secrets/tresor/github-personal-token.tresor"); then
unset token
return 1
fi
if [ -z "$token" ]; then
unset token
return 1
fi
export GH_TOKEN="$token"
unset token
export GH_TOKEN="$(...)" 한 줄로 처리하면 복호화 명령의 실패를 확인하기 어렵다. 값 할당과 export를 나누고 실패를 반환한다. direnv가 오류를 표시하면 이전 셸 상태가 남아 있을 수 있으므로 인증 작업을 계속하지 말고 원인을 해결한 뒤 계정을 다시 확인한다.
direnv allow
gh auth status
gh api user --jq .login
토큰은 복호화 후 평문 환경변수가 되어 자식 프로세스에 전달된다. 암호문과 Agent 승인은 저장된 비밀값을 보호하지만 실행 중인 환경변수까지 암호화하지는 않는다. 토큰 원문을 확인하려고 echo "$GH_TOKEN"을 실행할 필요는 없다.
키를 교체할 때는 새 키 슬롯을 추가하고 복호화를 확인한 뒤 이전 슬롯을 제거한다. 복호화 가능한 키를 모두 잃으면 암호문만으로 토큰을 되살릴 수 없다.
설정을 유지할 때의 기준
Nix나 Home Manager로 설정을 관리한다면 같은 Git 설정 파일을 두 곳에서 생성하지 않도록 소유자를 정한다. 원문에서는 Git·Git LFS 패키지 설치와 설정 파일 관리를 분리했지만, Home Manager로 includeIf까지 관리해도 된다. 도구를 끄는 것이 필수 조건은 아니다.
1Password의 agent.toml은 Agent가 제공할 키 범위를 좁히는 데 사용한다. 키 순서만 바꿔서 계정 선택 문제를 해결하려고 하지는 않는다. 저장소에 맞는 키 선택은 Git 설정과 OpenSSH 설정이 맡는다.
이 구성의 대가는 경로 규칙을 유지해야 한다는 점이다. 저장소나 worktree를 옮겼다면 작성자 정보와 SSH 키를 다시 확인한다. user.useConfigOnly는 커밋 작성자 추측을 막는 옵션이며, 미분류 저장소의 SSH 접속을 차단하는 장치는 아니다.
터미널과 GUI가 같은 Git·OpenSSH 설정을 읽도록 만들면, 어느 셸에서 앱을 실행했는지에 따라 인증 계정이 바뀌는 문제를 줄일 수 있다. 계정이 맞는지는 이메일 하나가 아니라 설정 출처, 선택된 공개 키, GitHub가 응답한 사용자명으로 확인한다.