Claude CodeのコミットにSSH署名を付ける — パスフレーズで止まらない構成
gpg.format=sshでコミットに署名し、鍵をssh-agentに載せておけばClaude Codeの実行中にパスフレーズ入力で止まりません。sandboxで署名が通る条件も示します。
Claude Codeにgit commitを任せるとき、コミットにSSH署名を付けるのはgit側の設定だけで済みます。gpg.formatをsshにして、署名に使う公開鍵を指定し、commit.gpgSignを有効にします。
詰まりやすいのはその先です。署名のたびに鍵のパスフレーズを聞かれるとClaude Codeの実行が止まり、sandboxを有効にしていると、鍵やエージェントに手が届かず署名が失敗します。この記事は、設定の最小形と、止まらない構成にする条件を順に扱います。
最小の設定:3つのキーでSSH署名になる
SSH署名はGit 2.34以降で使えます。GitHubのドキュメントも、SSH署名の検証にはGit 2.34以降が必要としています。設定するのは次の3つです。
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgSign true鍵のファイル名は環境ごとに違います。ここではid_ed25519.pubを例にしています。
gpg.formatは署名の形式です。既定はopenpgpで、x509とsshを選べますuser.signingkeyには、gpg.formatがsshのとき、秘密鍵のパスか、エージェントを使う場合は公開鍵のパスを書けますcommit.gpgSignをtrueにすると、すべてのコミットに署名します。コマンド側で付けるならgit commit -Sで、署名を外すのは--no-gpg-signです
Claude Codeが実行するgit commitも、同じgitの設定を読みます。commit.gpgSignがtrueなら、Claudeが-Sを付けなくても署名されます。署名の有無はgitの設定で決まるので、CLAUDE.mdに「署名すること」と書くより、設定で揃えるほうが確実です。
署名とattributionは別の話
コミット本文のCo-Authored-Byを消したい、という話はここの対象ではありません。あれはsettings.jsonのattributionが制御するトレーラーで、暗号署名ではありません。切り分けはClaude Code attributionの設定で読めます。
パスフレーズで止まらないようにする:鍵はエージェントに載せる
gitの設定は、鍵の指し方で挙動が分かれます。
user.signingkeyの書き方 | 署名に使われる秘密鍵 |
|---|---|
公開鍵のパス、またはkey::で始まる公開鍵 | 署名に使われる秘密鍵ssh-agentにある鍵(エージェントに載っている必要がある) |
| 秘密鍵のパス | 署名に使われる秘密鍵そのファイルの鍵 |
| 未設定 | 署名に使われる秘密鍵gpg.ssh.defaultKeyCommandの出力(例:ssh-add -L)の最初の鍵 |
公開鍵を指定する形は、秘密鍵がエージェントに載っていることが前提です。載せたあとは署名のたびにパスフレーズを聞かれません。Claude Codeの外のシェルで、起動前に済ませます。
ssh-add ~/.ssh/id_ed25519 # ここでパスフレーズを入力する
ssh-add -l # 鍵が載ったことを確認する
claudeClaude Codeの中のBashツールでパスフレーズを聞かれる状況を作ると、入力待ちで止まるか、フルスクリーンの端末が壊れる報告があります。この症状と、BatchMode=yesで対話を禁じる回避策は、Claude Codeのsshでpassphraseを聞かれて固まる原因に詳しく書いてあります。署名の場合も、鍵を先にエージェントへ載せる点は同じです。
エージェントの有無はシェルごとに違います。ターミナルを開き直した直後や、sshで入ったリモートホストでは空のことがあります。署名が通らないときは、まずssh-add -lで鍵が見えているかを確かめます。
署名を確かめる:allowedSignersFileを用意する
署名を付けただけでは、手元のgitは「検証済み」とは扱いません。gpg.ssh.allowedSignersFileに、信頼する公開鍵を書いたファイルを指定します。ファイルは「プリンシパル 公開鍵」の行で構成します。
echo "you@example.com $(cat ~/.ssh/id_ed25519.pub)" \
>> ~/.config/git/allowed_signers
git config --global gpg.ssh.allowedSignersFile \
~/.config/git/allowed_signers
git log --show-signature -1メールアドレスはコミッターのものに置き換えます。ファイルの置き場所は任意です。gitのドキュメントによると、公開鍵がこのファイルにあると署名の信頼度はfullyになり、無いとundefinedになってgit verify-commitが失敗します。リポジトリの中に置く運用もできますが、その場合はトップレベルからの相対パスで指定します。
GitHub上で「Verified」と表示させるには、署名に使う公開鍵をGitHubのアカウントに登録します。GitHubのドキュメントは、既存の認証用の鍵を署名用にも登録できると説明しています。
sandboxを有効にしているときに署名が通る条件
ここからが、Claude Code固有の論点です。sandbox内で動くBashコマンドは、ファイルシステムとネットワークに制限がかかります。署名に関係するのは、次の3つの層です。
署名を止める3つの層
エージェントのソケット
ssh-agentとの通信はUnixソケットです。sandboxingのページは、エージェントのソケットをふさぐことがあると書いています。鍵ファイルの読み取り
denyReadやcredentialsで~/.sshを隠していると、鍵ファイルを指す設定も読めません。gitの設定の書き込み
作業ディレクトリの
.gitconfigと.git内のconfig・hooksへの書き込みは、sandboxが拒否します。
macOS:エージェントのソケットを許可する
macOSでは、sandbox.network.allowUnixSocketsが空だとsandboxがすべてのUnixソケットをふさぎます。エージェントを使う署名では、ソケットのパスを許可リストに足します。設定リファレンスの例は~/.ssh/agent-socketです。
{
"sandbox": {
"network": {
"allowUnixSockets": ["~/.ssh/agent-socket"]
}
}
}実際のパスは、手元でecho $SSH_AUTH_SOCKを実行して確かめます。例の値をそのまま使える環境とは限りません。この設定の詳細は、allowUnixSocketsの解説で扱っています。
Linux・WSL2:許可リストは無視される
Linux・WSL2では、allowUnixSocketsのリストは無視されます。seccompフィルターがソケットのパスを見られないためです。許可するならallowAllUnixSocketsをtrueにしますが、これはすべてのUnixソケットを開く設定です。
seccompフィルターが入っていない環境では、sandboxはUnixソケットの呼び出しをそもそもふさぎません。/sandboxのDependenciesタブで、フィルターの有無を確かめられます。
ソケットを開けたくないときの2つの道
ソケットの許可を広げたくない場合は、次の2つがあります。
- 秘密鍵のパスを
user.signingkeyに指定し、鍵にパスフレーズを付けない専用の署名鍵にする。エージェントのソケットは要りませんが、鍵ファイルがディスクに平文であるため、漏れたときの影響は大きくなります git commit *をexcludedCommandsに入れて、コミットだけをsandboxの外で動かす
2つ目の設定例です。
{
"sandbox": {
"excludedCommands": ["git commit *"]
}
}注意点は、excludedCommandsの解説にある制約がそのまま当てはまることです。
- 末尾の
*が必要です。引数なしのgit commitにしか一致しない書き方では、git commit -m "..."を拾えません cdを含む呼び出しや、$(...)のコマンド置換を含む呼び出しは、sandboxに残ります- 除外したコマンドは、フルアクセスで動きます。
git commitにはフックが付くため、フックのスクリプトも外で動きます
署名のためだけにgit commit全体をsandboxの外に出すのは、境界を広く緩める選択です。エージェントのソケットを1本許可するほうが、緩める範囲は小さく済みます。
| 方法 | 緩める範囲 | 向く状況 |
|---|---|---|
allowUnixSockets(macOS) | 緩める範囲指定したソケット1本 | 向く状況エージェントで署名する、macOSのsandbox |
allowAllUnixSockets(Linux・WSL2) | 緩める範囲すべてのUnixソケット | 向く状況seccompフィルターが入っている環境 |
excludedCommandsにgit commit * | 緩める範囲コミット処理全体 | 向く状況ソケットの許可が使えない、または効かない |
| パスフレーズなしの専用鍵 | 緩める範囲鍵ファイルの扱い | 向く状況署名のためにsandboxを触りたくない |
Claudeに署名を外させない:設定とルールの置き場所
署名が失敗したとき、--no-gpg-signやgit -c commit.gpgSign=false commitで署名を外せば、コミット自体は作れてしまいます。Claudeにその回避をさせないための手は3つあります。
設定は自分のホームの~/.gitconfigに置く
sandbox内のコマンドは、作業ディレクトリの.gitconfigと.git内のconfigに書き込めません。ホームの~/.gitconfigも、sandboxの書き込み先に入っていなければ変更できません。commit.gpgSignをgit config --globalで自分で設定しておけば、sandbox内のClaudeがgit configで設定を書き換えることはできません。ただし、コマンドラインの-cはこの守りの外です。
denyルールで回避の書き方を止める
-cと--no-gpg-signは、settings.jsonのpermissions.denyで止められます。
{
"permissions": {
"deny": [
"Bash(git commit --no-gpg-sign *)",
"Bash(git -c commit.gpgSign=false *)"
]
}
}ただし、Bashの権限ルールは文字列の一致で動くため、引数を絞るルールは崩れやすいと公式のページが警告しています。オプションの順序を変えられたり、別の書き方で同じことをされたりすると、拾えません。確実な防御にはならず、抑止として置く位置づけです。
CLAUDE.mdに失敗時の振る舞いを書く
Claudeの判断に寄せる緩和策です。
## コミット
- コミットは署名付きで作る。署名に失敗したら `--no-gpg-sign` や
`-c commit.gpgSign=false` で回避せず、失敗の内容を報告して止まる
- 失敗の原因が鍵のパスフレーズなら、ユーザーに `ssh-add` を頼む署名が失敗したときの切り分け
署名に失敗するときは、sandboxの外のシェルで同じコミットが通るかを最初に見ます。通れば原因はsandbox側で、通らなければgitの設定か鍵の側です。
署名できないときの切り分け
- Qsandboxの外のシェルで、同じコミットに署名できない?答えができる答えがはい
sandboxが原因です。ソケットの許可、
~/.sshの読み取り拒否、excludedCommandsを見直します。 - Qgpg.formatの値はsshになっている?答えがなっていない答えがはい
gpg.formatが未設定で、OpenPGPとしてgpgが呼ばれています。sshを設定します。 - Qssh-add -lに署名用の鍵が見える?答えが見えない答えがはい
鍵がエージェントにありません。
ssh-addで載せてから、Claude Codeを起動し直します。
鍵とgit側の設定は揃っています
user.signingkeyのパスが公開鍵を指しているか、Gitが2.34以降かを確かめます。
commit.gpgSignを有効にしたままrebaseを回すと、書き換えたコミットすべてに署名が走ります。gitのドキュメントも、大量のコミットに署名される点と、パスフレーズを何度も打たずに済むようエージェントを使う便利さを挙げています。Claudeに履歴の整理を任せるときほど、鍵をエージェントに載せておく価値が出ます。
まとめ
設定の核心は、公開鍵をuser.signingkeyに指し、秘密鍵をエージェントに載せて起動することです。パスフレーズを聞かれる状況さえ作らなければ、Claude Codeの実行は止まりません。
sandboxを有効にしているなら、通る条件はOSで分かれます。macOSはallowUnixSocketsに1本、Linux・WSL2はallowAllUnixSocketsかフィルター無しの環境が前提です。ソケットを開けたくないなら、コミットをexcludedCommandsに回すか、専用鍵に切り替える選択になります。SSHリモートへのpushが落ちる別問題は、sandboxでgit SSHだけ失敗する原因で扱っています。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。