worktree でサブエージェントを隔離して、安心して並列に走らせている。その隔離が実は迂回できていた、という修正が Claude Code v2.1.216 でまとめて入った。git -C や環境変数で共有チェックアウトへ書き戻せる、.claude がシンボリックリンクだと書き込みがプロジェクトの外へ出る。設定した時点では、隔離できているかどうかは分からない。
目次
v2.1.216 で塞がれた抜け穴
修正の中身は、隔離の「外し方」がいくつも残っていたという話になる。
| 抜け穴 | 何が起きていたか |
|---|---|
| git の制御入力影響大 | git -C・--git-dir・GIT_DIR・GIT_WORK_TREE の4経路で、worktree に隔離したサブエージェントが共有チェックアウトへ書き戻せた |
| シンボリックリンク | .claude がシンボリックリンクのとき、ワークフローの保存・スケジュールタスクの書き込み・/rewind がリンク先(プロジェクト外)へ出ていた |
| Windows のネットワークパス | 読み取り専用とされるコマンドが、許可プロンプトなしでネットワークパスへアクセスできた |
| 再開したエージェントの識別 | 復帰時の同一性判定が正しく行われていなかった |
あわせて sandbox.filesystem.disabled という設定が追加された。ネットワークの制御を残したまま、ファイルシステムの隔離だけ外せる。
プロセスの現在地を見るだけでは足りない
git の抜け穴が示しているのは、隔離の判定方法そのものの問題になる。プロセスがどのディレクトリで動いているかを見て許可を決めていると、git が自前で持っている経路をすり抜けられる。
git -C /path/to/shared と書けば、作業ディレクトリを変えずに別のリポジトリを操作できる。GIT_DIR を設定しても同じことが起きる。コマンドの引数と環境変数を解決した後の「実際の書き込み先」まで見ないと、封じ込めにならない。
例えると、部屋に閉じ込めたつもりが、部屋の中に別の部屋へつながる配管があったようなものだ。ドアだけ見張っていても意味がない。
自分の環境で確かめる
更新したから安全、で済ませないほうがいい。筆者は「見た感じ動いている」を信じない方針でやっていて、視覚出力も配布物も実測してから完了と言うようにしている。隔離も同じで、破れるかどうかを試したときに初めて確認できる。
確認は5分で終わる。worktree で隔離したサブエージェントから、共有チェックアウトへ書き込もうとして拒否されるかを見る。
# 1. バージョンを確認する(2.1.216 以降であること)
claude --version
# 2. 検証用の worktree を作る
git worktree add ../test-isolation -b test/isolation
cd ../test-isolation
# 3. 隔離されたサブエージェントから、共有チェックアウトを触らせてみる
# 期待する結果: 拒否される
claude -p "git -C ../main-repo status を実行して結果を報告して" \
--permission-mode dontAsk
ここで共有側の状態が返ってきたら、隔離が効いていない。バージョンと設定を見直す必要がある。
この手順を筆者の環境で実際に走らせたところ、拒否は出たものの、隔離を測れていなかった。経緯と、検査として成立させるための条件は後の節に書く。
シンボリックリンクのほうも確かめておきたい。.claude をリンクにしている構成は珍しくない。設定を1箇所で管理したい場合に使う書き方だ。
# .claude がシンボリックリンクかどうかを確認する
ls -la .claude # -> の表示があればリンク
# Windows の場合
cmd /c "dir /AL" # JUNCTION / SYMLINK の表示を探す
リンクになっていたなら、ワークフローの保存やスケジュールタスクの書き込みが、どこへ着地するかを1度確かめておく。着地点がプロジェクトの外だと、意図しない場所にファイルが増えていく。
権限ルールの書き方も変わっている
関連する変更として、権限ルールのパス一致がディレクトリ1段だけを対象にする形へ変わった件がある。Edit(src/**) のようなルールを積み上げている場合、想定していた範囲と実際の範囲がずれる。
ここはエラーが出ないので気づきにくい。ルールが効かなくなれば毎回プロンプトが出るので分かるが、逆に広く効いてしまう場合は静かに通る。設定を書いた本人が「効いているはず」と思い込んでいる状態がいちばん危ない。
{
"permissions": {
"allow": [
"Edit(src/**)",
"Bash(npm run test:*)"
],
"deny": [
"Read(.env*)",
"Bash(git push:*)"
]
}
}
確認方法は単純で、許可しているつもりの範囲の外にあるファイルを1つ選び、編集させてみる。プロンプトが出れば効いている。黙って編集されたら、ルールが想定より広い。
この手順を走らせたら、隔離を測れていなかった
上の5分の手順を Claude Code 2.1.220 で実際に走らせた。結果は想定と違った。
対照実験から入った。worktree の中で、Claude を通さずに素の git -C を叩く。共有チェックアウトの git status が3行返ってきた。経路そのものは生きている。
次に手順のとおり、worktree の中から headless の Claude へ同じコマンドを渡した。
claude -p "git -C 'C:/.../BugattiAlpha_Hub' status --short を実行して報告して" \
--permission-mode dontAsk
返ってきたのは Permission to run this Bash command was denied. だった。拒否されている。手順どおりなら、ここで「隔離は効いている」と結論して終わる。
引っかかったのは、拒否した主体がどこなのか出力から分からない点だった。隔離が止めたのか、権限ルールが止めたのか、区別がつかない。
同じコマンドを、今度は共有チェックアウト自身から叩いてみた。隔離が理由で止まっているなら、こちらでは通るはずになる。
こちらも拒否された。
権限層を外すと、worktree から共有側が読めた
出どころを切り分けるため、権限モードを bypassPermissions に変えて、worktree の中から同じコマンドを叩いた。
claude -p "git -C 'C:/.../BugattiAlpha_Hub' status --short を実行して報告して" \
--permission-mode bypassPermissions --allowedTools Bash
共有チェックアウトの git status がそのまま返ってきた。
| 試したこと | 結果 |
|---|---|
worktree から素の git -C(対照実験) |
共有側の status が返る |
worktree から --permission-mode dontAsk |
拒否 |
| 共有チェックアウトから同じコマンド | こちらも拒否 |
worktree から --permission-mode bypassPermissions通った |
共有側の status が返る |
設定を見に行くと理由が分かった。settings.json の deny に入っている git 関連は Bash(git reset:*) と Bash(git rebase:*) の2つで、git -C は入っていない。allow 側の git 関連は Bash(git push) だけだった。dontAsk は allow に載っていない操作を拒否するモードなので、git -C ... status は「許可リストに無い」という理由で落ちていた。
隔離は関係していなかった。
ディレクトリが worktree でも、セッションは隔離されない
ここで分かったのは、worktree のディレクトリで claude -p を起動しても、隔離されたサブエージェントにはならないという点だ。cwd がたまたま worktree なだけの、普通のセッションになる。隔離を決めるのは起動のしかただった。
v2.1.216 が塞いだのは、ハーネスが worktree 隔離つきでサブエージェントを起動する経路のほうだ。上に書いた手順は、その経路を一度も通っていなかった。
ついでに確認した2点も書いておく。settings.json は user 側にも project の local 側にも sandbox キーを持っていなかった。下に並べる sandbox の表は、まだ何も設定していない状態の説明になる。.claude は実ディレクトリで、シンボリックリンクの抜け穴は当環境では該当しなかった。
未検証: ハーネスが worktree 隔離つきで起動したサブエージェントから git -C を試す経路は、今回は確かめていない。
検査を検査として成立させる2条件
今回の空振りから、隔離のテストに要る条件が2つ出てきた。
同じコマンドを2箇所で叩いて、結果が違うことを見る。worktree の中と共有チェックアウトで結果が同じなら、測っているものは隔離ではない。
対照実験を先に通す。素の git -C が通ることを確かめてから、Claude 経由で止まることを見る。両方が止まっている状態を「隔離が効いている」と読み違えないための順番になる。
拒否の文言も見ておきたい。worktree 側は Permission to run this Bash command was denied.、共有側は「Bash の実行がこのセッションで許可されていない」で、文言が違っていた。同じ拒否でも出どころが違う。
ファイルシステムの隔離を外す設定をどう使うか
新しく入った sandbox.filesystem.disabled は、使い道を選ぶ設定になる。ネットワークの制御だけ残して、ファイルの読み書きは自由にする形だ。
筆者が想定する使い道は、外部への通信だけを止めたい検証になる。ローカルのファイルは自由に触らせて、外へ出ていく経路だけ塞ぐ。逆に、信頼していないコードを走らせる場面では外すべきではない。
| 状況 | ファイルシステム隔離 | 理由 |
|---|---|---|
| 自分のプロジェクトで作業する | 外してもよい | 読み書きの範囲を制限する必要が薄い。通信制御は残る |
| 外部由来のコードを動かす外さない | 有効のまま | ファイル操作こそが危険な操作になる |
| 複数エージェントを並列で走らせる | 有効のまま | 互いの作業領域を守る目的がある |
permissions と sandbox は役割が違う
Claude Code には制限をかける仕組みが2つある。権限ルール(permissions)とサンドボックス(sandbox)で、片方だけ設定して安心している構成をよく見かける。筆者も最初は権限ルールだけで足りると思っていた。
違いは、何を見て止めるかにある。権限ルールはこれから実行しようとする操作の内容を見て、許可・拒否・確認を決める。サンドボックスは実行された結果として発生するアクセスを、OSに近い層で制限する。
| 仕組み | 止めるタイミング | すり抜ける例 |
|---|---|---|
| 権限ルール | ツール呼び出しの前 | 許可されたコマンドの中から別のコマンドを呼ぶ |
| サンドボックス最後の壁 | 実際のアクセス時 | 設定で無効化されている場合 |
今回修正された git -C の件は、この構図で理解できる。git の実行そのものは許可されている。許可された1回の実行が、引数によって別のリポジトリへ届いていた。操作の内容を見る層だけでは、この経路は止まらない。
2つを併用する意味はここにある。権限ルールで日常の操作を制御し、すり抜けたものをサンドボックスで受け止める。どちらか片方だけでは、抜け道が残る前提で組むのが安全になる。
sandbox の設定はどこを見ればいいか
ファイルシステムの隔離を外す設定が入ったことで、sandbox 関連の設定項目を一度整理しておく価値が出てきた。何がどの経路を塞いでいるのかを把握していないと、外していい設定と外してはいけない設定の区別がつかない。
| 設定 | 塞いでいる経路 | 外すとどうなるか |
|---|---|---|
sandbox.enabled |
サンドボックス全体 | 以下の制御がまとめて無効になる |
sandbox.network.allowedDomains要確認 |
外部への通信先 | 許可リスト外への接続が通る。情報の持ち出し経路になりうる |
sandbox.filesystem.disabled |
ファイルの読み書き範囲 | ローカルのファイル操作が自由になる。通信制御は残る |
sandbox.autoAllowBashIfSandboxed |
コマンド実行時の確認 | サンドボックス内のコマンドが確認なしで走る |
この表で言えば、2行目が最後まで残しておきたい制御になる。ファイルを自由に触れても外へ送れないなら、被害は手元に留まる。逆に通信が自由だと、読めた情報が外へ出る経路ができる。
ネットワークの許可リストが効いているか試す
許可リストを書いただけでは、効いているかは分からない。許可していないドメインへ実際に接続を試みて、止まるかを見る。
# 許可していないはずのドメインへの接続を試させる
# 期待する結果: 拒否される、または許可プロンプトが出る
claude -p "curl -s -o /dev/null -w '%{http_code}' https://example.com を実行して結果だけ報告して"
# 許可しているドメインは通ることも確認する(片方だけ見ない)
claude -p "curl -s -o /dev/null -w '%{http_code}' https://api.github.com を実行して結果だけ報告して"
両方を確認するのが肝になる。拒否だけ見て安心すると、実は全部止まっていて必要な通信もできていない状態を見落とす。通したいものが通り、止めたいものが止まる。この2点が揃って初めて設定が効いていることになる。
隔離を前提にした運用の見直し
今回の修正で気づいたのは、隔離の設定を入れた後、それが効いているかを一度も確かめていなかったという点だ。設定を書いた時点で安心してしまい、破れるかを試していなかった。
過去にも似た失敗をしている。配布物を作ったとき、起動することだけ確認して納品し、実際の機能を動かすと落ちる状態のまま渡してしまった。起動と動作は別物で、そのときの教訓は「配布物は実機能の実行まで検証する」だった。
隔離もこれと同じ構造になる。設定が入っていることと、隔離が機能していることは別物だ。確認するには、破ろうとしてみるしかない。
並列でエージェントを走らせる構成を組んでいるなら、この確認は定期的に入れたい。複数エージェントを1画面で扱う仕組みを作ったときも、同時実行の安全性は設計の前提になっていた。前提が崩れていれば、その上に載っているものは全部揺らぐ。
並列で走らせているときの壊れ方
隔離が破れたときの症状は、派手なエラーとして出ない。共有チェックアウトに、誰が書いたか分からない変更が乗る。並列で3つ走らせていれば、どのエージェントの仕業かも追いにくい。
筆者が経験した中でいちばん面倒だったのは、同じ症状が別の場所で繰り返し出る形の不具合だ。1つ直すと次が出る。そのときの教訓は、同種の症状が2回以上再発したら個別修正をやめて土台を疑う、というものだった。
隔離の抜け穴は、まさにこの土台側の問題になる。作業結果が混ざる症状を1件ずつ直していると、原因は永久に見つからない。並列実行で説明のつかない変更が出たら、隔離そのものを疑うほうが早い。
確認をどこに組み込むか
毎回手で試すのは続かない。筆者は更新のたびに確認したい項目を、短いチェックリストにして残すようにしている。項目は3つで足りる。
隔離した領域から外へ書けないこと。上の worktree のテストがそのまま使える。
権限ルールが想定の範囲で効いていること。許可範囲の外のファイルを1つ編集させて、プロンプトが出るかを見る。
設定ファイルの実体がどこにあるか。リンクを張っている場合、着地点を確認する。
3つとも数分で終わる。更新のたびに走らせても、負担にならない範囲に収まる。
確認を1本のスクリプトにまとめる
3項目を毎回手で叩くのは、そのうちやらなくなる。筆者は同じ種類の確認をスクリプトにして、更新のたびに走らせる形へ寄せている。判断が要る箇所は残して、機械が判定できる部分だけ自動化する。
#!/usr/bin/env bash
# 隔離の確認(更新のたびに走らせる)
set -u
fail=0
echo "== 1. .claude の実体 =="
if [ -L .claude ]; then
echo " リンク -> $(readlink .claude) ※着地点を確認すること"
else
echo " 実ディレクトリ(リンクではない)"
fi
echo "== 2. worktree からの書き戻し =="
out=$(claude -p "git -C ../$(basename "$PWD") status --short を実行して、出力があればそのまま報告して" \
--permission-mode dontAsk 2>&1)
case "$out" in
*拒否*|*denied*|*not\ permitted*) echo " OK: 拒否された" ;;
*) echo " NG: 通った可能性がある -> $out"; fail=1 ;;
esac
echo "== 3. 許可外ドメインへの通信 =="
# 期待: 拒否 or 確認プロンプト。通ってしまう場合は allowedDomains を見直す
exit $fail
3番目を自動判定にしていないのは、環境によって拒否の出方が違うからだ。プロンプトが出る構成と、即座に失敗する構成がある。ここは出力を目で見て判断したほうが確実になる。
機械にやらせる部分と、人が見る部分を分けておくと、確認そのものが続く。全部自動化しようとすると作り込みで力尽きるし、全部手作業だと3回目でやめる。
よくある疑問
更新すれば確認は不要か。修正されたのは判明した経路で、自分の構成に固有の抜け道までは保証されない。試すのが確実になる。
worktree を使っていなければ関係ないか。シンボリックリンクと権限ルールの件は、worktree を使っていなくても効く。.claude をリンクにしている構成は確認しておきたい。
Windows 固有の修正は自分に関係あるか。ネットワークパスへのアクセスは、共有フォルダを作業に使っている環境で効く。ローカルのドライブだけで作業しているなら影響は薄い。
隔離を外したほうが速いのでは。速くはなる。速さと引き換えに、失敗したときの被害範囲が広がる。並列で走らせる構成では、隔離はやり直しの範囲を小さく保つために入っている。
確認して問題が見つかったらどうするか。まず対象の作業を止めて、共有チェックアウトの状態を確認する。意図しない変更が乗っていないかを git status と git log で見る。原因の特定より先に、現状の把握を済ませたほうが判断が早い。
まとめ
今回のリリースは、隔離の抜け穴が複数まとまって塞がれた。受け取るべき教訓は修正の中身そのものより手前にある。隔離が効いているかは、破ろうとして初めて分かる。
今日やるなら、worktree を1つ作って共有チェックアウトへの書き込みを試す。10分ほどで終わる。拒否されたところで止めないことが今回の収穫だった。筆者の環境では拒否が出たが、原因は allow リストに載っていないことで、隔離とは無関係だった。共有チェックアウトからも同じコマンドを叩いて、結果が違うところまで見て初めて隔離を測ったことになる。
確認が終わったら、試した手順をメモに残しておきたい。次の更新でも同じ手順が使える。隔離まわりの修正は、経路が見つかるたびに続いていく類のものだ。手元に確認手順があるかどうかで、更新のたびの安心の質が変わる。