Claude Media
Claude Codeでchezmoiのdotfilesを管理し、秘密を読ませない境界の引き方

Claude Codeでchezmoiのdotfilesを管理し、秘密を読ませない境界の引き方

dotfilesをchezmoiに移すと、秘密の置き場所はホームとソースディレクトリの2つに増えます。Read denyの書き方、chezmoi catやdiffの抜け道、サンドボックスの足し方を示します。

dotfilesをchezmoiで管理し直す作業は、Claude Codeに任せやすい部類です。ファイルを集め、テンプレートに直し、差分を確かめる流れは機械的だからです。難しいのは、その過程で~/.sshや認証トークンを含む設定がClaudeの視界に入らないようにする点です。

chezmoiを入れると、同じ秘密が2か所に現れます。ホームにある実ファイルと、~/.local/share/chezmoiにあるソースの写しです。ホーム側だけにdenyを書いても、ソース側が空いたままになります。この記事では、2か所それぞれに線を引き、chezmoiのコマンド経由の漏れも塞ぐ設定を順に作ります。

chezmoiを入れると、秘密の置き場所が2つになる

chezmoiは、ファイルの状態を「ソース状態」としてソースディレクトリに保存します。既定の場所は~/.local/share/chezmoiで、-Sフラグか設定ファイルのsourceDirで変えられます。ディレクトリは普通のディレクトリとして、それ以外はすべて普通のファイルとして置かれ、属性はファイル名の接頭辞や接尾辞で表します。

接頭辞には次のようなものがあります。

接頭辞・接尾辞意味
dot_意味先頭にドットを付ける。dot_fooは.fooになる
private_意味グループと他者の権限を外す
encrypted_意味暗号化して保存する
.tmpl(接尾辞)意味テンプレートとして展開する

つまりソースディレクトリには、~/.zshrcの中身がdot_zshrcという名前で入ります。名前が変わるので、Read(~/.zshrc)のdenyルールはこのファイルに一致しません。

もう一つの落とし穴は、作業ディレクトリです。Claude Codeは起動したディレクトリの中を、承認なしで読めます。chezmoi cdでソースディレクトリに入ってからClaudeを起動すると、ソース全体が作業ディレクトリになります。dotfilesの移行を任せるときの自然な起動位置が、そのまま広い読み取り範囲になる構造です。

どこにどんな名前で入ったかは、chezmoi source-pathで確かめられます。引数なしならソースディレクトリ、ファイルを渡すとそのソース側のパスを返します。

chezmoi source-path
chezmoi source-path ~/.ssh/config

この出力を見てから、後述のdenyルールのパスを書きます。推測で書いたパターンは、一致しなくても何も警告しません。

秘密を平文でソースに置かない3つの手段

境界を引く前に、そもそも秘密をソースへ平文で入れない構成にします。chezmoiの公式ドキュメントにある手段は3つです。

選択肢

秘密をソースに平文で置かない手段

  • パスワードマネージャーのテンプレート関数

    テンプレート内でonepasswordReadのような関数を呼び、適用時に値を取り出して実ファイルに書き込みます。ソースには呼び出しだけが残り、公開リポジトリでも秘密は漏れません。1Passwordを使う場合の準備は、1Passwordプラグインの解説にあります。

  • encrypted_ 属性

    chezmoi add --encryptで追加すると、ファイルはASCIIアーマー形式でソースに保存されます。age、git-crypt、gpg、transcryptに対応しています。

  • .chezmoiignore

    パターンに一致するファイルを管理対象から外します。マシンごとにテンプレートで切り替えることもできます。

3つは排他ではありません。トークンを含む.zshrcならテンプレート、秘密鍵なら暗号化、会社マシンでしか使わないファイルなら.chezmoiignore、と使い分けます。

ここで押さえておきたい点が一つあります。テンプレートで秘密を差し込んだ場合、chezmoi applyの結果として実ファイルには値が入ります。公式ドキュメントの例では、.zshrc.tmplに書いたCloudflareのAPIトークンが、適用後の~/.zshrcにそのまま展開されます。

# ソース: ~/.local/share/chezmoi/dot_zshrc.tmpl
export CF_API_TOKEN='{{ onepasswordRead "op://Personal/cloudflare-api-token/password" }}'
 
# 適用後: ~/.zshrc
export CF_API_TOKEN='(実際のトークン値)'

ソースは安全でも、ホームの~/.zshrcには秘密がある、という状態です。したがってClaudeには、ソースのテンプレート(dot_zshrc.tmpl)を編集させ、展開後の~/.zshrcは読ませない、という分担が基本形になります。

denyルールは配置先とソースの両方に書く

読み取りを止めるのはpermissions.denyのReadルールです。書き方は.envを読ませないRead denyの解説で詳しく扱っています。ここでは、chezmoi向けに効くポイントだけを押さえます。

  • Readのdenyは、同じパスへのEditとWriteも止める。新規作成も止まる
  • パスはgitignoreと同じ構文で、~/はホーム、//は絶対パス、/は設定ファイルの置き場所を基準にする
  • Write(path)やGlob(path)のルールは受け付けられるが、一致しない。EditとReadで書く

ルールの置き場所にも注意が要ります。ユーザー設定の~/.claude/settings.jsonに書いた/secrets/**は、プロジェクトのsecretsではなく~/.claude/secrets/**を指します。ホームやソースディレクトリを指すときは~/か//で始めます。

実際の設定は、次の形になります。秘密を含むファイルを、配置先とソースの両側で塞ぎます。

{
  "permissions": {
    "deny": [
      "Read(~/.ssh/**)",
      "Read(~/.zshrc)",
      "Read(~/.config/chezmoi/**)",
      "Read(~/.local/share/chezmoi/**/encrypted_*)",
      "Read(~/.local/share/chezmoi/**/private_*)",
      "Read(~/.local/share/chezmoi/dot_ssh/**)"
    ]
  }
}

1行ずつ意図を見ます。

  • ~/.ssh/**と~/.zshrc: 配置先の実ファイル。展開後の値が入る側
  • ~/.config/chezmoi/**: chezmoiの設定ファイル。テンプレートに渡すdataの値などを置く場所
  • encrypted_*とprivate_*: ソース側で属性が付いたファイルを、ディレクトリを問わず一括で塞ぐ
  • dot_ssh/**: .sshをソースへ移した側。接頭辞が付かない名前でも漏れないように、明示する

encrypted_*は暗号文なので、読まれても値は分かりません。それでも塞ぐのは、denyがEditも止めるからです。chezmoi editは編集前に復号し、編集後に再暗号化します。暗号化済みファイルの編集は、この経路を通すのが筋です。暗号化ファイルの構造だけ見せて値は読ませない運用は、SOPSの境界の切り方でも扱っています。

private_*の扱いは、private_が付いたファイルに秘密が入るとは限らない点に注意してください。権限を絞りたいだけのファイルも含まれるので、広すぎると感じたら、前節のchezmoi source-pathで実在する名前を見て絞ります。

一方、秘密を含まないファイルまで塞ぐ必要はありません。.gitconfigや.vimrcのようなファイルは、Claudeに読ませてテンプレート化を手伝わせるほうが移行は速く進みます。

chezmoiのコマンドが、denyの外から値を運んでくる

Readのdenyは、Claudeの組み込みファイルツールと、catやheadのようにファイルを名指しするBashコマンドに効きます。ファイルを名指ししない経路や、サブプロセスが間接的に読む経路は対象外です。chezmoiのコマンドは、まさにこの外側にいます。

次のコマンドは、展開済みの内容や設定値を標準出力に出します。

コマンド出力される内容
chezmoi cat <target>出力される内容ターゲットの内容(展開後のテンプレートを含む)
chezmoi diff出力される内容ターゲット状態と実ファイルの差分。引数なしなら全ターゲット
chezmoi data出力される内容計算済みのテンプレートデータ
chezmoi execute-template出力される内容渡したテンプレートを実行した結果
chezmoi dump出力される内容ターゲット状態のダンプ。引数なしなら全体

chezmoi cat ~/.zshrcはdenyルールのRead(~/.zshrc)には一致しません。コマンドの引数に~/.zshrcが現れても、それはchezmoiへの引数であり、Claude Codeが認識するファイルコマンドではないからです。公式ドキュメントも、Bashの認識対象をcat、head、tail、sed、teeなどと明記しています。

対策は、Bashのルールで止めることです。denyとaskは、複合コマンドの中のどのサブコマンドに一致しても効きます。

{
  "permissions": {
    "deny": [
      "Bash(chezmoi cat *)",
      "Bash(chezmoi diff *)",
      "Bash(chezmoi data *)",
      "Bash(chezmoi execute-template *)",
      "Bash(chezmoi dump *)"
    ],
    "ask": [
      "Bash(chezmoi apply *)",
      "Bash(chezmoi add *)"
    ]
  }
}

評価の順序は、deny、ask、allowです。最初に一致したものが結果を決め、あとから具体的なallowを足してもdenyは外れません。

末尾の*は、前に空白があれば引数なしの素のコマンドにも一致します。Bash(chezmoi diff *)はchezmoi diffだけの実行も止めます。一方で、サブコマンドの前にフラグを挟む書き方は一致しません。公式ドキュメントはgitの例として、git -C . pushがBash(git push *)に一致しないと説明しています。chezmoi -S <dir> cat <target>のような形も同じ理屈で抜けるはずなので、Bashルールは最後の砦にせず、次節のサンドボックスと併用します。

chezmoi diffを塞ぐと、変更内容の確認が不便になります。この不便は、人間が自分のターミナルでchezmoi diffを実行して見る、という運用で吸収します。Claudeには「テンプレートを書いた。chezmoi diffはあなたが確認してください」と報告させれば足ります。

applyとaddをdenyでなくaskに置くのは、Claudeに実行させたい場面があるからです。applyはホームの実ファイルを書き換えます。addは新しいファイルをソースに取り込みます。どちらも実行前に人が見る価値があります。

サンドボックスで、Bashの子プロセスまで塞ぐ

Readのdenyが及ばない経路として、公式ドキュメントはPythonやNodeのスクリプトがファイルを自分で開くケースを挙げています。この種の経路はサンドボックスで塞ぎます。

サンドボックスのfilesystem.denyReadは、OSレベルで強制されます。Bashコマンドとその子プロセスすべてに効くので、スクリプト経由の読み取りも止まります。

{
  "sandbox": {
    "enabled": true,
    "filesystem": {
      "denyRead": [
        "~/.ssh",
        "~/.zshrc",
        "~/.local/share/chezmoi/dot_ssh"
      ]
    }
  }
}

サンドボックスのパスは、通常のパス規約で書きます。/tmp/buildが絶対パスで、~/.kubeがホーム基準です。Readルールの//pathや/pathとは書き方が違うので、混ぜないでください。

もう一点、denyReadがReadツールを止めないことにも注意します。サンドボックスが守るのはBashとその子プロセスで、組み込みのReadやEditは、前節のパーミッションルールが担当します。両方を書いて初めて、2つの経路が塞がります。

移行をClaudeに任せる手順

境界を決めたあとの、実際の移行を順に見ます。人間が最初にやることと、Claudeに任せることを分けます。

手順

dotfiles移行の分担

  1. 1

    人間が秘密の置き場所を決める

    トークンを含むファイルはテンプレートに、秘密鍵は--encryptに、マシン固有のファイルは.chezmoiignoreに振り分けます。

  2. 2

    denyルールを入れて、ソースを開く

    設定ファイルに前節のルールを入れてから、ソースディレクトリでclaudeを起動します。

  3. 3

    秘密を含まないファイルから取り込ませる

    .gitconfigや.vimrcのようなファイルをchezmoi addで取り込み、テンプレート化が必要な箇所を洗い出させます。

  4. 4

    追加時の秘密検出を、警告からエラーに上げる

    chezmoi addの--secretsは既定がwarningです。errorにすると、秘密が見つかったときに追加が失敗します。

4つ目は、設定ファイルのadd.secretsでも指定できます。ignore、warning、errorのいずれかです。Claudeがchezmoi addを実行する前提なら、errorにしておくと安全側に倒れます。

chezmoi add --secrets error ~/.gitconfig
chezmoi add --template ~/.gitconfig
chezmoi managed

chezmoi managedは、管理対象のエントリを一覧します。取り込んだファイルが意図どおりか、秘密を含むはずのファイルが紛れていないかを、人間が確認する材料になります。

Claudeに渡す指示は、CLAUDE.mdに書いておくと毎回言わずに済みます。次は、境界をそのまま文章にした例です。

# dotfiles(chezmoi)の作業ルール
 
- ソースは ~/.local/share/chezmoi。ホームの実ファイルは直接編集しない
- 秘密を含むファイルは、ソース側のテンプレート(*.tmpl)だけを編集する
- 展開後の値を出すコマンド(chezmoi cat / diff / data / execute-template / dump)は使わない
- chezmoi apply と chezmoi add は実行前にユーザーへ確認する
- テンプレートに秘密の値を直書きしない。パスワードマネージャーの関数を使う

CLAUDE.mdは指示であり、強制力はありません。守らせたいことは、前節までのdenyとaskで機械的に塞ぎ、CLAUDE.mdは意図を伝える補助に回します。

どこから漏れるかの早見表

ここまでの対策を、経路ごとに並べ直します。

漏れる経路塞ぐ手段残る隙
ホームの展開後ファイルをReadで開く塞ぐ手段Read(~/.zshrc)などのdeny残る隙denyに書いていない別ファイル
ソースの写しを開く塞ぐ手段encrypted_*やdot_ssh/**のdeny残る隙接頭辞の付かない名前で置いたファイル
chezmoi catやdiffの出力塞ぐ手段Bash(chezmoi ...)のdeny残る隙別名コマンドやラッパー経由の実行
スクリプトが自分でファイルを開く塞ぐ手段サンドボックスのdenyRead残る隙サンドボックスを外して実行したコマンド
テンプレートへの値の直書き塞ぐ手段add.secretsをerrorにする残る隙検出されない形式の秘密

「残る隙」の列は、設定を足せば減ります。ただしゼロにはなりません。最終的な保険は、そもそも秘密をホームに置かない運用です。パスワードマネージャーのCLIを、必要なときだけ呼ぶ形に寄せるほど、読まれて困る実ファイルが減ります。

まとめ

chezmoiの導入は、秘密の置き場所を増やします。denyはホームの実ファイルとソースの写しの両方に書き、chezmoi catやdiffのような展開済みの値を出すコマンドはBashのルールで止めます。スクリプト経由の読み取りまで塞ぐなら、サンドボックスのdenyReadを足します。

最初に決めるのは、どのファイルがホームに値を持つか、という一点です。テンプレートで差し込んだ値はapply後のホームに残るので、そこを読ませない分担にすると、あとの設定が単純になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/sandboxing
  3. chezmoi.io/reference/source-state-attributes
  4. chezmoi.io/user-guide/password-managers
  5. chezmoi.io/user-guide/encryption
  6. chezmoi.io/reference/special-files/chezmoiignore
この記事を共有:XはてブLinkedIn