記事一覧へ戻る

ソフトウェア開発

GitHubアカウント2つ、切り替えコマンドゼロ

開発用と公開用でGitHubアカウントを分け、指定ディレクトリ以下では名義も認証も自動で切り替わる構成を作る。includeIfとSSHエイリアスとinsteadOfの組み合わせで、切り替え忘れによる誤爆を仕組みで防ぐ。

private用とpublic用でGitHubアカウントを使い分ける — 自動切り替えと誤爆防止の設計

開発用のprivateリポジトリと、公開用のpublicリポジトリを、別々のGitHubアカウントで運用したい。よくある構成だが、素朴にやると2つの事故が起きる。

  1. 名義の誤爆 — 公開リポジトリのコミットに、開発用アカウントの名前・メールアドレスが刻まれる
  2. 認証の誤爆 — 間違ったアカウントの認証でpushしてしまう(あるいは、どちらの認証で通信しているのか把握できていない)

この記事では「ディレクトリに置いた時点で、名義も認証も自動的に切り替わり、間違った操作は失敗する」構成を作る。切り替えコマンドを覚える必要も、切り替え忘れを心配する必要もない状態がゴールだ。

以下、登場するアカウントは架空の例とする。

用途アカウント名メールアドレス認証方式
開発用(private)alex-devdev@example.comHTTPS + macOSキーチェーン(既存のまま)
公開用(public)alexcarteralex@example.comSSH(今回新規構築)

環境はmacOSを想定しているが、キーチェーン関連以外はLinuxでも同じ。

現状と背景

これまでは開発用のGitHubアカウント1つで運用してきた。認証はHTTPS方式で、初回pushのときにブラウザが開いてGitHubにログインし、以後はmacOSのキーチェーンに保存されたトークンで自動認証される。push先は自分のprivateリポジトリだけなので、これで困っていなかった。

今回、これとは別に公開用のGitHubアカウントを作り、一般公開するリポジトリはそちらで管理することにした。アカウントごと分ける理由は管理のしやすさだ。privateリポジトリだらけのところにpublicがあれば目立つし気づく。逆も同様。ただし、それは誤爆しなければの話。

そこで「指定したフォルダ以下のリポジトリは、自動的に公開用アカウントでpushされる」ようにする方法をAIに相談して、この構成に落ち着いた。

TL;DR — 設定の全体像

先に完成形を貼っておく。理屈は後述。

鍵の生成と登録:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_public -C "alexcarter"
cat ~/.ssh/id_ed25519_public.pub   # → 公開用アカウントのGitHubに登録

~/.ssh/config:

Host public-github
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_public
    IdentitiesOnly yes

~/.gitconfig(既存の設定に末尾2行を追加するだけ):

[credential]
    helper = osxkeychain
[user]
    name = alex-dev
    email = dev@example.com
 
[includeIf "gitdir:~/dev/git_public/"]
    path = ~/.gitconfig-public

~/.gitconfig-public(新規作成):

[user]
    name = alexcarter
    email = alex@example.com
 
[url "git@public-github:"]
    insteadOf = git@github.com:
    insteadOf = https://github.com/

動作確認:

ssh -T git@public-github                          # → "Hi alexcarter!"
cd ~/dev/git_public/リポジトリ名 && git config user.email   # → alex@example.com

これで ~/dev/git_public/ 以下に置いたリポジトリは、コミット名義も認証もすべて自動で公開用に切り替わる。remote URLは普通の github.com のままでいい。間違ってprivateリポジトリを指定した操作は権限エラーで必ず失敗する。

以下、この構成の設計意図と各設定の意味。

設計方針: 状態を持たない

アカウント切り替えには gh auth switch のような「今どちらのアカウントか」というグローバル状態を切り替える方式もある。しかしこの方式は、切り替え忘れという人為ミスが構造的に排除できない。

代わりに、すべてを場所(ディレクトリとURL)に紐づけることにした。

  • ~/dev/git_public/ 以下にあるリポジトリ → 自動的に公開用の名義・認証
  • それ以外 → 既存の開発用設定のまま(一切手を加えない)

「今どっちだっけ」という状態が存在しないので、確認も切り替えも不要になる。

2つの設定ファイルの役割分担

混同しやすいが、SSHとGitの設定は守備範囲が違う。両方必要だ。

  • ~/.ssh/config通信時にどの鍵で認証するか(=どのGitHubアカウントとして通信するか)を決める
  • ~/.gitconfig — **コミットに刻まれる名義(user.name / user.email)**を決める

SSHをいくら正しく設定しても、gitconfigを放置すれば公開リポジトリに開発用の名義が刻まれる。逆もまた然り。

手順1: 公開用のSSH鍵を作る

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_public -C "alexcarter"

オプションの意味:

  • -t ed25519 — 鍵の暗号方式。現在の推奨で、GitHubも第一に案内している
  • -f ~/.ssh/id_ed25519_public — 保存先ファイル名。名前は自由だが、id_<方式>_<用途> の形にしておくと後で分かる
  • -C "alexcarter" — コメント。GitHub上で鍵を識別するためのラベルで、機能には影響しない

実行するとパスフレーズを聞かれる。ここは判断ポイント。

  • なしにする(空のままEnter) — 秘密鍵ファイルを読めた者は誰でもこの鍵を使えるようになる。ただしこの鍵に紐づくのは公開用アカウントだけなので、漏洩時の被害範囲は限定的。手軽さ優先ならこれでいい

  • 付ける — macOSならキーチェーンに覚えさせることで、入力の手間は実質初回だけにできる。パスフレーズを設定したうえで、生成後に次を実行:

    ssh-add --apple-use-keychain ~/.ssh/id_ed25519_public

    さらに手順2で作る ~/.ssh/configHost public-github ブロックに、以下の2行を追記しておく:

        UseKeychain yes
        AddKeysToAgent yes
    

生成が終わると、秘密鍵 id_ed25519_public と公開鍵 id_ed25519_public.pub の2ファイルができている。GitHubに登録するのは .pub の方

cat ~/.ssh/id_ed25519_public.pub

この中身を、公開用アカウント(alexcarter)の Settings → SSH and GPG keys → New SSH key に貼り付ける。

なお、別のマシンからも公開用アカウントを使いたい場合、秘密鍵をコピーして持ち回るのではなく、マシンごとに鍵を新規生成して同じアカウントに追加登録するのが正攻法。GitHubは1アカウントに複数の鍵を登録できる。マシンを手放すときはその鍵だけ削除すれば失効が完了する。

手順2: SSHのホストエイリアスを定義する

~/.ssh/config(なければ新規作成):

Host public-github
    HostName github.com
    IdentityFile ~/.ssh/id_ed25519_public
    IdentitiesOnly yes

新規作成した場合はパーミッションを設定しておく。

chmod 600 ~/.ssh/config

public-github は実在しないホスト名で、SSHの中だけで通用するエイリアスだ。git@public-github:... 宛の通信は、実際には github.com に、指定した鍵で接続される。つまり「どの鍵を使うか」がURLに焼き付く。

接続テスト:

ssh -T git@public-github
# → "Hi alexcarter! You've successfully authenticated..." と出ればOK

ポイント: 開発用(alex-dev)はHTTPS認証のままなので、Host github.com のエントリを書く必要はない。HTTPSなら開発用、SSHなら公開用、と認証経路そのものが分離される。

手順3: gitconfigをディレクトリで自動切り替える(includeIf)

~/.gitconfig は普段の開発用設定のまま、末尾に2行足すだけ。

[credential]
    helper = osxkeychain
[user]
    name = alex-dev
    email = dev@example.com
 
[includeIf "gitdir:~/dev/git_public/"]
    path = ~/.gitconfig-public

includeIf "gitdir:..." は「このディレクトリ以下のリポジトリでは、指定したファイルを追加で読み込む」という条件付きインクルード。末尾のスラッシュは必須(サブディレクトリ全体にマッチさせる記法)。

そして ~/.gitconfig-public を新規作成:

[user]
    name = alexcarter
    email = alex@example.com
 
# github.com宛のremote URLを、通信時に自動でpublic-github経由に書き換える
[url "git@public-github:"]
    insteadOf = git@github.com:
    insteadOf = https://github.com/

前半で名義が切り替わり、後半の insteadOf がもう一つの仕掛けになる。

insteadOfの効果: URLの書き分けすら不要になる

insteadOf は、remote URLを通信の瞬間に書き換える設定だ。これにより ~/dev/git_public/ 以下では、

git clone https://github.com/alexcarter/my-tool.git

と、GitHubのページからコピーした普通のURLをそのまま使っても、実際の通信は git@public-github:alexcarter/my-tool.git、つまり公開用のSSH鍵で行われる。public-github というエイリアスを覚えてURLを書き分ける必要すらなくなる。

この構成が誤爆を防ぐ理由

ここまでの設定で、事故のパターンがそれぞれ潰れていることを確認しておく。

名義の誤爆 → 起きない。 ~/dev/git_public/ 以下では includeIf により常に公開用の名義。手動で git config user.email を打つ儀式も、その打ち忘れも存在しない。

認証の誤爆 → 失敗する(静かに成功しない)。 ~/dev/git_public/ 以下ではすべてのGitHub通信が公開用アカウントとして認証される。公開用アカウントは開発用のprivateリポジトリへのアクセス権を持たないので、間違ってprivateリポジトリをremoteに指定すると:

ERROR: Repository not found.

必ず権限エラーで止まる。逆に、ディレクトリの外では今まで通り開発用のHTTPS認証なので、そちらも影響を受けない。

「間違えないように気をつける」のではなく「間違えたら失敗するようにしておく」。誤爆防止はこの方向で設計するのが正しい。

さらに固くする: pre-pushフックで明示ブロック

権限エラー頼みでは不安、という場合はもう一層足せる。~/.gitconfig-public に追記:

[core]
    hooksPath = ~/dev/git_public/.githooks

~/dev/git_public/.githooks/pre-push を作成:

#!/bin/bash
# 公開用アカウント宛以外へのpushを拒否
url="$2"
case "$url" in
  git@public-github:alexcarter/*) exit 0 ;;
  *)
    echo "🚫 BLOCKED: push先が公開用アカウントではありません: $url"
    exit 1 ;;
esac
chmod +x ~/dev/git_public/.githooks/pre-push

これで ~/dev/git_public/ 以下の全リポジトリで、push先が公開用アカウントのパターンに一致しない限り、push自体が拒否される。認証の成否とは独立に、git側で「意図した宛先か」を検査する層になる。

動作確認

設定が効いているかは、テスト用リポジトリで確認できる。

mkdir -p ~/dev/git_public/test && cd ~/dev/git_public/test
git init
 
git config user.email
# → alex@example.com が出ればincludeIfが効いている
 
git config --get-regexp url
# → url.git@public-github:.insteadof の行が見えればOK
 
cd ~ && git config user.email
# → dev@example.com(ディレクトリの外では通常設定)

実際にpushまで通したら、名義の最終確認:

git log -1 --format='%an %ae'

運用: privateのコードをpublicに公開する手順

この構成の上での、実際の公開作業フロー。

⚠️ 必ず ~/dev/git_public/ の「外」でcloneすること。 ~/dev/git_public/ の中でcloneしようとすると、insteadOfの書き換えにより公開用アカウントのSSHでprivateリポジトリを読みに行くことになる。公開用アカウントにはprivateリポジトリへのアクセス権がないため、ERROR: Repository not found. でcloneに失敗する。誤爆防止の仕組みが正しく働いている結果だが、初見だと「リポジトリが見つからない?」と混乱しやすいので注意。

# 1. privateリポジトリをクローン(公開ディレクトリの外で、開発用認証)
cd ~/dev
git clone https://github.com/alex-dev/my-tool.git my-tool-release
cd my-tool-release
 
# 2. 履歴を消す(private時代のコミット履歴を持ち出さない)
rm -rf .git
 
# 3. 公開用ディレクトリに移動してから初期化
cd .. && mv my-tool-release ~/dev/git_public/my-tool
cd ~/dev/git_public/my-tool
git init          # ← この時点で名義は自動的に公開用
git add .
git commit -m "Initial commit"
 
# 4. 公開用アカウントでGitHub上に空リポジトリを作成(README等なしで)
 
# 5. remote追加とpush(普通のURLでOK。insteadOfが書き換えてくれる)
git remote add origin https://github.com/alexcarter/my-tool.git
git branch -M main
git push -u origin main

注意点は2つ。

.git を消しても、ファイルの中身に秘密が残っていないかは別問題。 .env、クラウドのクレデンシャル、ハードコードされたAPIキーなどはpush前にgrepで確認する。

grep -rn -iE "(api[_-]?key|secret|password|token|AKIA)" . --exclude-dir=node_modules

万一、名義を間違えてcommitしてしまったら、push前なら1コマンドで直せる。

git commit --amend --reset-author --no-edit

push後だと履歴の書き換えになって面倒なので、push前の git log -1 --format='%an %ae' の確認を習慣にしたい。

細かい注意点

  • includeIfはclone時にも効くか? Git 2.36以降なら、クローン先ディレクトリに基づいてincludeIfが適用されるため、~/dev/git_public/ 内での git clone も最初から公開用設定で動く
  • insteadOfの副作用: 公開ディレクトリ以下では、他人の公開リポジトリをHTTPSでcloneする場合もSSH(公開用アカウント)経由になる。公開リポジトリの読み取りは誰の認証でも通るので実害はないが、「このディレクトリのGitHub通信はすべて公開用アカウントとして行われる」という理解は持っておく

まとめ

仕組み防ぐ事故
~/.ssh/config のホストエイリアスURLに認証先を焼き付けるどの鍵で通信しているか不明になる事故
includeIfディレクトリで名義を自動切り替えコミット名義の誤爆
insteadOfremote URLを通信時に自動書き換えURL書き分けの手間とミス
権限の非対称性公開用鍵はprivateに届かない誤ったリポジトリへの操作(必ず失敗する)
pre-pushフックpush先URLをパターン検査想定外の宛先へのpush

一度組んでしまえば、日常の操作は「公開するものは ~/dev/git_public/ に置く」だけ。切り替えコマンドはゼロ、覚えておくべき状態もゼロで済む。