DevOps

ディレクトリごとにClaude Codeのアカウントを分ける

  • Claude Code
  • macOS
  • direnv
  • Security

Claude Code の設定ディレクトリは CLAUDE_CONFIG_DIR で切り替えられます。作業ディレクトリごとにこの環境変数を仕込んでおけば、cd するだけでログイン中のアカウントが入れ替わります。

// ~/Workspace/org-b/.sallyport.jsonc
{
  "expand": true,
  "env": {
    "CLAUDE_CONFIG_DIR": "$WORKSPACE_PATH/.claude",
  },
}

CLAUDE_CONFIG_DIR でアカウントを切り替えるやり方そのものについては、既に何本か記事が出ています。この記事で書くのは、macOS でこれが効く理由のほうです。

ところが公式ドキュメントには「macOS は認証情報が Keychain にあるので引き継がれる」と書かれていて、素直に読むとアカウントは分けられないことになります。実際には分けることができます。

設定ディレクトリの下に何が入るのか

CLAUDE_CONFIG_DIR を設定すると、通常 ~/.claude の下に置かれるものがまるごとそちらへ移ります。settings.jsonskills/plugins/projects/ の下のセッション履歴、shell-snapshots/ あたりが該当します。

見落としやすいのが ~/.claude.json です。これは ~/.claude ディレクトリの中ではなく、ホーム直下に置かれる、アプリケーションの状態を持つファイルです。ところが CLAUDE_CONFIG_DIR が設定されていると、参照先は設定ディレクトリの中の .claude.json に移ります。

ログイン中のアカウント情報もここに入っています。実際に手元の2つの設定ディレクトリを覗くと、それぞれ別のアカウントが記録されています。

claude auth status で確認するのが早いです。環境変数を付けるかどうかで出力が変わります。

claude auth status --text
CLAUDE_CONFIG_DIR=~/Workspace/org-b/.claude claude auth status --text
Login method: Claude Max account
Organization: Org A
Email: a@example.com

Login method: Claude Max account
Organization: Org B
Email: b@example.com

2つのアカウントが同時にログインしたままになっています。片方に入り直すともう片方が落ちる、ということは起きません。

リファレンスに項目がないまま他のページから参照されている

CLAUDE_CONFIG_DIREnvironment variables のリファレンスに項目がありません。Claude Code settings にも出てこず、claude --help にも出てきません。

それでいて、他のページからは参照されています。Explore the .claude directory は「これを設定すると、このページに出てくる ~/.claude のパスはすべてそのディレクトリの下になる」と書いていますし、Manage sessions はセッションの保存先を ~/.claude の外へ移す手段として表に載せています。どちらもリンク先は Environment variables で、そこに項目がありません。

Debug your configuration では、設定が壊れているときに何も読み込まないセッションと比較するための手段として紹介されています。

Point CLAUDE_CONFIG_DIR at an empty directory to bypass everything under ~/.claude, and launch from a directory that has no .claude folder, .mcp.json, or CLAUDE.md so project configuration is also skipped.

Debug your configuration

マルチアカウント用途もドキュメントに載せてほしい、という issue #33430 は closed as not planned で閉じています。つまり今のところ、この使い方は公式に想定された用途ではありません。動くけれど、この使い方はドキュメントには無い、という位置付けのまま使うことになります。

macOSのKeychainは設定ディレクトリごとに別のエントリになる

同じ Debug ページには、空の設定ディレクトリで起動したときの挙動がこう書かれています。

  • On Linux and Windows, you’ll be prompted to log in again because credentials are stored under the configuration directory
  • On macOS, credentials are in the Keychain and carry over to the clean session

Debug your configuration

Linux と Windows は認証情報が設定ディレクトリの下にあるので再ログインが要る、macOS は Keychain にあるので引き継がれる、と読めます。これを真に受けると、macOS で CLAUDE_CONFIG_DIR を分けてもアカウントは1つのまま、という結論になります。

空のディレクトリを指して確かめると、そうはなりません。

CLAUDE_CONFIG_DIR=/tmp/claude-clean claude auth status --text
Not logged in. Run claude auth login to authenticate.

理由は Keychain のサービス名の作り方にあります。

手元にインストールされている v2.1.233 のバイナリから、サービス名を組み立てている箇所を引用します。minify されたままなので変数名は読めませんが、やっていることは追えます。bzu には同じ場所で require("crypto") が入っています。

function WZ(e=""){
  let t=process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR,
      r=t!==void 0?!t:!process.env.CLAUDE_CONFIG_DIR,
      n=t!==void 0?t.normalize("NFC"):En(),
      o=r?"":`-${bzu.createHash("sha256").update(n).digest("hex").substring(0,8)}`;
  return `Claude Code${Ua().OAUTH_FILE_SUFFIX}${e}${o}`
}

e に渡ってくるのは -credentials という文字列です。r は末尾に何も付けないかどうかの判定です。CLAUDE_SECURESTORAGE_CONFIG_DIR が定義されていればそれが空文字のときだけ、定義されていなければ CLAUDE_CONFIG_DIR が未設定か空文字のときだけ真になります。真のときは o が空文字になり、サービス名は Claude Code-credentials のままです。

CLAUDE_CONFIG_DIR が設定されていると o- と設定ディレクトリのパスの sha256 の 先頭8桁 が入ります。サービス名が Claude Code-credentials-<8桁> になるので、Keychain 上でも別のエントリとして扱われます。設定ディレクトリが違えばパスが違い、パスが違えばハッシュが違うので、衝突しません。

CLAUDE_CONFIG_DIR の有無で Keychain のサービス名が変わる。未設定なら Claude Code-credentials、設定するとそのパスの sha256 先頭8桁が末尾に付き、同じ login.keychain の中に別のエントリとして並ぶので、2つのアカウントが同時にログインしたままになる

同じ計算は手元でも再現できます。ハッシュを取る対象は CLAUDE_CONFIG_DIR に渡した文字列そのものです。path.resolve() は挟まらないので、相対パスを渡すとその文字列がそのままハッシュに掛かり、同じディレクトリを指していても絶対パスで渡したときとは別のエントリになります。

printf '%s' "/Users/example/Workspace/org-b/.claude" | shasum -a 256 | cut -c1-8
46474663

この8桁をそのままサービス名に埋めて Keychain を引くと、エントリが返ってきます。-w を付けなければパスワード本体は表示されず、メタデータだけが出ます。実際には16進のキーや <NULL> の属性が十数行続くので、主要なものだけ抜き出します。

security find-generic-password -s "Claude Code-credentials-46474663"
keychain: "/Users/example/Library/Keychains/login.keychain-db"
class: "genp"
attributes:
    "acct"<blob>="example"
    "svce"<blob>="Claude Code-credentials-46474663"
    ...

acct に入るのは $USER の値で、Claude Code 側のアカウントとは関係ありません。アカウントを区別しているのは svce の末尾8桁だけです。

なお、ハッシュを取る前に NFC 正規化が挟まります。パスにマルチバイト文字が入っていると、手計算した値と食い違います。

コードからはもうひとつ、CLAUDE_SECURESTORAGE_CONFIG_DIR という別の環境変数を先に見にいっていることも分かります。こちらが設定されていればそちらが優先され、空文字を渡したときだけデフォルト扱いに戻ります。認証情報の置き場所だけを設定ディレクトリと切り離したい場合の逃げ道になっていますが、これもドキュメントには出てきません。

Linux と Windows なら認証情報そのものが設定ディレクトリの下に入るため、ここまでの Keychain の話は関係なく、ディレクトリを分ければそのまま分かれます。macOS だけが一段回りくどい経路を通ります。

ドキュメントの記述と実装が食い違っているので、macOS でアカウントを分ける運用は、いま動いていても将来も動く保証はありません。バージョンを上げたら再ログインを求められる可能性は残ります。

ディレクトリごとに環境変数を設定する

CLAUDE_CONFIG_DIR を毎回手で入力するのは現実的ではありません。エイリアスで claude-personal のようなコマンドを作る方法もありますが、それだと ghgit が別のアカウントを向いたままになります。設定ディレクトリだけでなく、SSH 鍵も 1Password のアカウントも同じ単位で切り替えたいので、ディレクトリに入った時点で環境変数がまとめて入る形にしています。

長らく direnv を使っていました。今は Nix と home-manager でリポジトリごとのセットアップを配っている都合もあって、個人で作っている sallyport に移しています。

// ~/Workspace/org-b/.sallyport.jsonc
{
  // Environment variables applied while inside this workspace.
  // WORKSPACE_PATH is exported automatically.
  "expand": true,
  "env": {
    // Git
    "GIT_ALLOW_PUSH_MAIN_BRANCH": "yes",

    // 1Password
    "OP_ACCOUNT": "my.1password.com",
    "OP_SSH_KEY": "Development/GitHub - org-b",

    // Claude
    "CLAUDE_CONFIG_DIR": "$WORKSPACE_PATH/.claude",
  },
}

承認していない設定を適用しないのは、sallyport に限った話ではありません。direnv も同じで、.envrc を書き換えれば承認は外れます。

承認は sha256(設定の識別子 + 中身) で記録します。Nix の store へのシンボリックリンクとして配られた設定はリンク先ではなく置かれている場所で識別するので、rebuild で store パスが変わっても承認は保たれ、中身を書き換えたときだけ外れます。

sallyport で変えたのは適用する対象のほうです。direnv が実行するのはシェルスクリプトですが、.sallyport.jsonc に書けるのは環境変数の宣言だけで、既定では値がコードとして解釈されることもありません。

同じ狙いのツールは他にもあります。shadowenv は Shadowlisp という宣言的な言語で環境変数の差分を書く形で、任意のシェルコマンドは走りません。安全側に倒れている代わりに、値を $(op read ...) のようなコマンドの出力から取る書き方ができません。

sallyport で expand を opt-in にしてあるのはそのためです。既定では値をシングルクオートでそのまま適用し、expand: true を書いたときだけ zsh の展開を通します。

1Password の SSH エージェントまわりは以前も書きました。

ブログClaude Codeのsandboxが1Password SSHエージェントのUnixソケットを塞ぎ署名が失敗するClaude Codeのsandboxed Bashから1Password SSH鍵で署名付きコミットを実行すると失敗します。原因はUnixソケット接続の遮断で、sandboxを無効化しなくてもallowUnixSockets設定でソケットパスだけを許可すれば解決します。

ここで使うツールは何でも構いません。direnv でも、direnv に暗号化を組み合わせた構成でも、やりたいことは「そのディレクトリにいる間だけ環境変数が入っていて、出たら元に戻る」の一点です。設定ディレクトリを分ける話とツールの選定は独立しているので、既に direnv が入っているならそのまま export CLAUDE_CONFIG_DIR=... を書けば済みます。

共有したいものはプラグインにする

どちらのアカウントでも使いたいスキルやサブエージェントは、設定ディレクトリを分けた時点で別々の実体になります。

ここはプラグインで揃えられます。組織ごとに standards リポジトリを1つ持ってマーケットプレイスにし、それぞれの設定ディレクトリへインストールする形です。ラビーの分は公開しています。

GitHubGitHub - LabeeHive/standards: Shared standards repository for Labee LLC projects — a Claude Code plugin marketplace of skills, agents, and workflowsShared standards repository for Labee LLC projects — a Claude Code plugin marketplace of skills, agents, and workflows - LabeeHive/standards

公式マーケットプレイスのプラグインのように両方で使うものも、同じ要領で両方へ入れます。更新は設定ディレクトリごとに走るので、放っておいて揃うわけではありません。

プラグインに載らないものは残ります。ユーザー全体に効く CLAUDE.md のように home-manager から配っているものもありますが、今のところ多くは手で複製しています。

分ける単位も配り方も、まだ様子を見ながら決めている最中です。

どこで分かれるかは設定ディレクトリのパスで決まる

--resume で拾えるセッションも設定ディレクトリごとに分かれます。別のアカウントで始めた作業の続きを拾いたいときは、そちらに合わせてから起動することになります。

どこで分かれるかを決めているのは、設定ディレクトリのパスの文字列そのものです。ワークスペースを別の場所へ移すとサービス名のハッシュも変わるので、移す前に作られた Keychain のエントリは新しいパスからは引けません。ディレクトリ名を変えるだけでも同じことが起きます。

設定ディレクトリの中身を新しいパスへコピーしても、claude auth status は未ログインと出ます。認証情報はファイル側ではなく、パスから引く Keychain のエントリ側にあるためです。