Claude CodeにSOPSのシークレットを読ませず編集・デプロイする境界の切り方
SOPSの暗号化ファイルは構造だけ見せて値は読ませたい。平文側のRead deny、sops decryptのBash deny、git diffのtextconvという抜け道、鍵の置き場所まで、線の引き方を示します。
SOPSで暗号化したYAMLは、キーが平文のまま残り、値だけがENC[...]に置き換わります。Claude Codeに読ませて困るのは復号後の値で、暗号文そのものではありません。この性質を使えば、構造は見せて値は見せない運用が組めます。
ただし、permissions.denyにRead(secrets/**)と書くだけでは、暗号化ファイルの編集まで止まります。しかもsops decryptの出力やgit diff経由で、復号済みの値がClaudeの目に入る経路が残ります。この記事では、どこに線を引くと何が止まり、何が漏れるかを順に見ます。
前提: SOPS暗号化ファイルは「構造が見える」
SOPSはYAML・JSON・ENV・INI・バイナリを扱うエディタ型のツールです。YAML・JSON・ENV・INIでは、ファイルを木として扱い、キーを平文のまま残して値だけを暗号化します。ドキュメントの例では、次のような形のファイルができます。
myapp1: ENC[AES256_GCM,data:Tr7o=,iv:1=,aad:No=,tag:k=]
app2:
db:
user: ENC[AES256_GCM,data:CwE4O1s=,iv:2k=,aad:o=,tag:w==]
password: ENC[AES256_GCM,data:p673w==,iv:YY=,aad:UQ=,tag:A=]
sops:
kms:
- arn: arn:aws:kms:us-east-1:656532927350:key/920aff2e-...Claudeにこのファイルを読ませると、どんなキーがあるか、どの鍵(KMSやPGPなど)で暗号化されているかは分かります。値は分かりません。構造を相談する相手としては十分で、値だけが閉じた状態です。
守る対象は2種類に分かれます。
- 暗号化済みのファイル:
*.enc.yamlのような名前で、コミットしてよい。Claudeに読ませてよい - 復号後に現れる平文:
.env、*.dec.yaml、secrets/plain/など。Claudeに読ませない
SOPS自体は拡張子を決めていません。どのファイルを暗号化するかは.sops.yamlのpath_regexで指定します。名前の規約は自分で決める部分で、後述のdenyルールもこの規約に依存します。
Read denyで平文側だけを塞ぐ
Claude Codeでファイルを読ませない手段は、permissions.denyのReadルールです。パスの書き方と効く範囲は.envを読ませないRead denyの解説に詳しいので、ここではSOPS向けの要点だけを扱います。
ディレクトリ丸ごとのdenyは暗号化ファイルの編集も止める
Readのdenyは、同じパスに対するEditツールとWriteツールも止めます。新規作成も含みます。そのため、暗号化ファイルを置くディレクトリをRead(secrets/**)で塞ぐと、secrets/app.enc.yamlにキーを足す編集もできなくなります。
File is covered by a Read deny rule in your permission settings and cannot be edited.エラーの文言は上のとおりです。切り分けは「Read deny rule」エラーの対処にまとめてあります。さらに、単一セグメントのディレクトリパターンはdenyだと任意の深さのsecretsにマッチします。サブディレクトリの暗号化ファイルも巻き込まれます。
平文だけをファイル名で塞ぐ
暗号化ファイルを読める状態に保つには、denyの対象を平文のファイル名や置き場所に絞ります。
{
"permissions": {
"deny": [
"Read(.env)",
"Read(.env.*)",
"Read(!.env.example)",
"Read(**/*.dec.yaml)",
"Read(**/*.dec.json)",
"Read(plain/**)"
]
}
}*.enc.yamlはどの行にもマッチしないので、読み取りも編集もできます。!の否定は同じ設定ファイルの、より前に書いたルールにしか効きません。Read(!.env.example)はRead(.env.*)の後ろに置きます。
規約が崩れる点には注意が必要です。誰かが復号結果をsecrets.yamlのような名前で書き出すと、どのルールにもマッチしません。平文をファイルに落とさない運用を前提にし、落とす場合の置き場所をplain/の1か所に決めておくのが、ルールを短く保つ方法です。
Bashからの復号を塞ぐ
Readのdenyは、catやheadのようなファイル名を示すコマンドにも適用されます。一方、sops decrypt secrets/app.enc.yamlはファイル名を示していても、読んでいるのは暗号文で、標準出力に平文が出ます。Readのルールでは止まりません。Bashのdenyで別に塞ぎます。
{
"permissions": {
"deny": [
"Bash(sops decrypt *)",
"Bash(sops -d *)",
"Bash(sops edit *)"
]
}
}sops decryptは標準出力に平文を出すので、そのままClaudeの会話に入ります。-iを付けるとファイルが平文に置き換わり、その後は通常のファイルとして読めてしまいます。sops editはエディタに平文を開く対話型のコマンドで、エージェントの環境で使う場面はありません。
Bashのdenyは境界にならない
Bashのルールは、Claudeが書いたコマンド文字列に対して働きます。同じプログラムを別の書き方で呼ぶ形は止まりません。ドキュメントは/bin/rmやsh -c '...'、git -C . pushを例に挙げ、denyは「普段出てくる呼び方を止めるもの」で、セキュリティ境界ではないと説明しています。
Bash(sops decrypt *)も同じ限界を持ちます。/usr/local/bin/sops decrypt ...やbash -c 'sops decrypt ...'は別の文字列です。したがって、Bashのdenyは事故を減らす層として置き、本命の境界は次節以降の「復号できる鍵をClaudeの手元に置かない」側で作ります。
git diffが復号して見せてしまう
SOPSのドキュメントには、暗号化ファイルの差分をgitで平文表示する設定があります。
# .gitattributes
*.yaml diff=sopsdiffergit config diff.sopsdiffer.textconv "sops decrypt"この設定を入れたリポジトリでgit diffを実行すると、前後のバージョンが復号されてから差分として表示されます。人間のレビューには便利です。
Claudeが同じリポジトリでgit diffを実行すると、復号された値がそのまま出力に出ます。git diffは読み取り専用のgitコマンドとして扱われ、Bash(sops decrypt *)のdenyには引っかかりません。sops decryptを呼んでいるのはgitのサブプロセスです。権限のドキュメントは、Read・Editのdenyが及ばない経路として「任意のサブプロセスが間接的にファイルを読む場合」を挙げています。
このため、Claudeを動かすリポジトリでは、textconvの設定を永続化しないほうが安全です。設定が無ければ、--textconvを付けても復号は走りません。差分を見たい人は、必要なときだけ一時指定で見ます。
git -c diff.sopsdiffer.textconv="sops decrypt" diff人間の作業用に別のcloneを用意し、そこにだけ設定を入れる方法もあります。リポジトリの.gitattributesだけをコミットしても、textconvの設定が無ければ復号は走りません。ただしtextconvは.git/configだけでなく~/.gitconfig(--global)にも書けます。Claudeを動かす環境では、両方を確認します。
git config --show-origin --get diff.sopsdiffer.textconv編集はどこまでClaudeに任せられるか
暗号化ファイルを読めるようにしたところで、Claudeがテキストとして直接書き換えてよい部分は限られます。
値を書き換えない
ENC[...]の部分をEditツールで書き換えても、正しい暗号文にはなりません。SOPSはファイル全体にMAC(改ざん検知用の値)を持ち、値の追加や削除を検出します。値の更新はSOPSのコマンドを経由します。
ドキュメントには、パスと値を指定して更新するsops setがあります。
sops set secrets/app.enc.yaml '["app2"]["key"]' '"newvalue"'値はJSON形式で渡します。ファイルや標準入力から渡す--value-fileと--value-stdinもあります。ただし、Claudeがコマンドに値を書く時点で、その値はClaudeの文脈に入っています。「Claudeに値を知らせずに値を更新する」ことは、コマンド引数に直接書かせる方式では成立しません。
人間が値を用意し、--value-fileで渡す形ならClaudeは値を見ません。値を入れたファイルは、先に挙げたRead(plain/**)のような平文用のdenyの下に置きます。ファイルを読むのはsops setのプロセスなので、Readのdenyには引っかかりません。ここでも「値の入ったファイルが残る」ことに変わりはなく、投入後すぐ削除する運用が前提です。
構造の編集は任せやすい
Claudeに頼みやすい作業は、値を伴わない構造の変更です。
.sops.yamlのcreation_rulesの追記(どのパスをどの鍵で暗号化するか)- Kubernetesのシークレットなど、暗号化する範囲の指定の見直し
- 暗号化ファイルを使うデプロイスクリプトの作成
暗号化する範囲はencrypted_regexなどで決まります。ドキュメントの例では、Kubernetesのシークレットに--encrypted-regex '^(data|stringData)$'を使い、dataとstringDataの値だけを暗号化して、シークレット名を含むmetadataは平文のまま残します。ファイルを開いたときに、どのシークレットが何を持つかが見える利点があります。
平文部分の編集とMACの関係
平文のまま残した部分にも落とし穴があります。ドキュメントによると、暗号化されていない内容もファイルのチェックサムに含まれるため、SOPSの外で書き換えると整合性チェックが壊れます。この挙動は--mac-only-encryptedフラグか、.sops.yamlのmac_only_encrypted: trueで変えられます。この設定では、暗号化した値だけがMACの対象になります。
Claudeにmetadataのラベルやレプリカ数のような平文部分を編集させたいなら、この設定が必要になります。ただし、設定を有効にすると平文部分の改ざんは検出されなくなります。何を許すかのトレードオフです。
creation_rules:
- path_regex: secrets/.*\.enc\.yaml$
age: age1s3cqcks5genc6ru8chl0hkkd04zmxvczsvdxq99ekffe4gmvjpzsedk23c
encrypted_regex: '^(data|stringData)$'
mac_only_encrypted: trueageの値はリファレンスの例にある公開鍵です。自分のプロジェクトでは自分の公開鍵に置き換えます。
デプロイは誰が復号するかで3通りに分かれる
デプロイでは、どこかで復号が必要です。復号する主体で、守れる強さが変わります。
| 復号する主体 | Claudeに見える範囲 | 備考 |
|---|---|---|
| CI(KMSのロールなどで復号) | Claudeに見える範囲暗号化ファイルとCIの実行結果 | 備考Claudeはコミットまで。鍵がClaudeの環境に存在しない |
| 人間がターミナルで実行 | Claudeに見える範囲実行結果をClaudeに貼れば見える | 備考貼る内容を人間が選ぶ |
| Claudeがラッパースクリプトを実行 | Claudeに見える範囲スクリプトの標準出力と標準エラー | 備考鍵がClaudeの環境にあるため、迂回の余地が残る |
最も強いのは、鍵がClaudeの実行環境にない構成です。SOPSのセキュリティの説明は、KMSで鍵の配布をアクセス制御の問題に置き換えるという設計の考え方を述べています。Claudeの環境にKMSの権限を与えない構成は、この考え方と同じ向きです。
Claudeにデプロイを実行させる場合は、復号とデプロイを1つのスクリプトにまとめ、そのスクリプトだけを許可します。
{
"permissions": {
"allow": ["Bash(./scripts/deploy-secrets.sh *)"],
"deny": [
"Bash(sops decrypt *)",
"Bash(sops -d *)",
"Edit(scripts/deploy-secrets.sh)"
]
}
}最後のEditのdenyは、スクリプトをClaudeに書き換えさせないためのものです。許可したスクリプトの中身をcatやechoに差し替えられると、平文を標準出力へ出せてしまい、「平文を出さない」という前提が崩れます。スクリプトの修正は人間が行います。
スクリプトの中ではsops decryptの出力をパイプでデプロイコマンドに渡し、平文をファイルにも標準出力にも出しません。スクリプト内部のsops呼び出しは、Claudeが書いたコマンド文字列ではないため、denyに引っかかりません。デプロイ後に値を読み戻すコマンド(Kubernetesならシークレットの内容を表示するgetなど)は出力に値が出るので、同じように許可しない扱いにします。
terraformやtofuのデプロイで権限を絞る考え方は、OpenTofuのvalidateとplanだけ許可する権限設計と同じ型です。「許可するのは副作用の小さい段階だけ」という線の引き方は、シークレットのデプロイにも使えます。
鍵の置き場所でdenyの限界を補う
denyルールは、Claude Codeが認識する経路に効きます。任意のサブプロセスが間接的に読む経路は対象外で、OS側で遮断するにはサンドボックスを有効にします。ドキュメントは、サンドボックスの設定と権限ルールのパス・ドメインが最終的なサンドボックス構成に統合されると説明しています。設定の書き方はsandbox.filesystem.denyReadの解説にあります。
SOPSの運用に当てはめると、次のように考えられます。
- ageの秘密鍵ファイルやクラウドの認証情報を、Claudeの実行環境から読めない場所に置く。
Read(~/...)で書くホーム相対のdenyと、サンドボックスの読み取り禁止を重ねる - 復号が必要なデプロイは、鍵を持つ別の実行環境(CIや人間のシェル)に切り出す
- 鍵が読める環境で動かすなら、サンドボックスのネットワーク制限で、復号した値の外部送信を絞る
秘密鍵の保存先はSOPSの鍵の種類(age、PGP、KMSなど)で変わるので、自分の構成で実際にどのファイルや環境変数が使われているかを先に洗い出します。
そもそも平文の.envをなくす方向なら、1Passwordプラグインで.envから秘密情報を外す方法が別の選択肢になります。コミット時の混入はgitleaksのhookで止められます。SOPSで暗号化したつもりのファイルが平文のままコミットされる事故を拾えます。
設定を入れたあとの確認手順
denyは、書いた時点では効いているか分かりません。実際にClaudeへ頼んで確認します。
設定後の動作確認
- 1
平文を読ませる
plain/に空のダミーファイルを置き、読むよう頼みます。拒否されることを見ます。 - 2
復号コマンドを実行させる
sops decrypt secrets/app.enc.yamlの実行を頼みます。Bashのdenyで拒否されることを見ます。 - 3
暗号化ファイルを読ませる
secrets/app.enc.yamlを読むよう頼みます。キー名とENC[...]が見えることを確認します。 - 4
git diffを実行させる
暗号化ファイルを変更した状態で
git diffを頼みます。出力がENC[...]のままか、復号済みの値かを見ます。
4つ目で復号済みの値が出た場合は、textconvの設定が有効になっています。git config --show-origin --get diff.sopsdiffer.textconvで設定元を調べ、ローカルの.git/configか~/.gitconfigのどちらにあるかに応じて外します。
よくある質問
SOPS暗号化ファイルを読ませること自体は安全か
暗号文は、鍵がなければ復号できません。ドキュメントの脅威モデルでは、攻撃の可能性が高い順にKMSの認証情報の侵害やPGP鍵の侵害を挙げています。ただし、AESの脆弱性のような将来の可能性に備えて、暗号化ファイルを「適度に非公開に保つ」ことを勧めています。公開リポジトリに置く前提の使い方にはなっていません。
CLAUDE.mdに「復号しない」と書くだけではだめか
CLAUDE.mdの指示はClaudeの振る舞いを寄せますが、権限は変えません。権限を強制するのはClaude Code本体で、強制したい禁止事項はdenyに書きます。
まとめ
SOPSの値は読ませず構造は見せる運用は、平文側のdeny、sops decryptのBash deny、git diffのtextconv設定の3点を揃えて初めて成り立ちます。Bashのdenyは迂回できるため、最終的な境界は鍵をClaudeの環境に置かないことで作ります。デプロイをCIに寄せられるなら、それが最も単純な構成です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。