Claude Code 2.1.314 で、作業ディレクトリの外へのファイルの書き込みを断る設定 permissions.blockWritesOutsideWorkingDirectories が足されました。変更履歴によると、この設定があれば Write・Edit・NotebookEdit の3つの道具は、どの権限モードでも作業ディレクトリの外のパスを断ります。ただ、変更履歴が挙げているのは道具の名前だけです。Bash で書いたときやシンボリックリンクを通したときにどうなるかは書かれていないので、私たちは 2.1.316 を手元の Mac で動かして技術検証しました。

結論

  • 設定が止めたのはファイルの道具(Write・Edit)だけでした。Bash の echo >・cp・python での書き込みは、設定ありでもそのまま外に書けました。
  • 相対パス(../…)と、作業ディレクトリの中に置いたシンボリックリンクでは抜けられませんでした。リンクは解決した先のパスで判定されました。--add-dir で足した場所は内側として扱われ、書けました。
  • Bash も止めたいなら、サンドボックス(sandbox.enabled)と組み合わせると、ホームの下への書き込みは止まりました。ただし macOS では、/tmp(/private/tmp)の下にはサンドボックスがあっても書けました。
  • 使い分けは、ファイルの道具の歯止めにこの設定を使い、Bash の書き込みはサンドボックスで縛る形になります。macOS では、/tmp に置いた大事な物はサンドボックスでも守られません。

試した環境

  • Mac。Claude Code 2.1.316(公式の darwin-arm64 の配布物を、配布元の manifest.json の checksum で照合して使用)。手元にあった 2.1.308・2.1.312 には、この設定はありませんでした。
  • モデルは Sonnet、権限モードは権限の確認を飛ばす bypassPermissions、ヘッドレス(-p と --output-format stream-json)。
  • どのケースも1回ずつ走らせました。
claude -p "<頼み>" --model sonnet --permission-mode bypassPermissions \
  --output-format stream-json --verbose --settings '<設定の JSON>'

試し方

作業ディレクトリの中に inside.txt と、外の置き場を指すシンボリックリンク link を置きました。外の置き場には、Edit で書き換えさせる target.txt を置いてあります。1回の起動で頼む書き込みは1つだけにし、頼みの最後に「ほかの道具は使わず、失敗しても別の方法を試さず、道具の結果をそのまま返して」と添えました。見たのは、stream-json に出る道具の結果(tool_result)と、外の置き場にファイルが実際にできたか(Edit なら中身が書き換わったか)の2つです。

モデルはどの回も頼んだとおりに道具を呼び、断られても別の方法を試しませんでした。Bash にサンドボックスを外す指定を付けることもありませんでした。

比べたのは次の4つの起こし方です。設定はどれも --settings で渡しました。

  • 設定なし: {}
  • 設定あり: {"permissions": {"blockWritesOutsideWorkingDirectories": true}}
  • 設定あり+--add-dir <外の置き場>
  • 設定あり+サンドボックス: 上の設定に sandbox を足す(後で書きます)

書き方ごとの結果

書き方設定なし設定あり設定あり+--add-dir設定あり+サンドボックス
Write(絶対パス)書けた断られた書けた断られた
Write(相対パス ../outside/…)書けた断られた——
Write(中のリンク link/… 経由)書けた断られた—断られた
Edit(外にある既存のファイル)書けた断られた——
Bash echo hello > 外書けた書けた書けた断られた
Bash cp inside.txt 外—書けた—断られた
Bash python3 -c "open(外,'w')…"—書けた—断られた
Bash で /private/tmp の下に書く書けた書けた—書けた

「—」は試していない組み合わせです。最後の行は外の置き場を /private/tmp の下に置いた回の値で、その上の Bash の行のサンドボックスの列は、置き場をホームの下に移した回の値です(いきさつは後で書きます)。相対パスの行も、最初の /private/tmp の回の値です。

ファイルの道具は、相対パスでもリンク経由でも断られた

設定を入れると、Write で外の絶対パスに書く頼みは、次のような道具の結果で断られました。

<外のパス> is outside <作業ディレクトリ>; the permissions.blockWritesOutsideWorkingDirectories setting blocks writes outside the working directories. Ask the user to add the directory with /add-dir, or to remove that setting.

相対パス ../outside/write_rel.txt を渡しても、結果の文には絶対パスに直した場所が出て、同じように断られました。作業ディレクトリの中のリンクを通した link/write_link.txt への書き込みは、「resolves through a symlink to <外の置き場>/write_link.txt」と、リンクの先のパスを挙げて断られています。中にあるように見える場所でも、解決した先が外なら外として扱われました。

Edit では、外のファイルを Read で読むところまでは通り、Edit で書き換える段で断られました。外の target.txt の中身は変わっていません。この設定が断るのは書き込みで、外を読むことは止めません。

断りの文は、/add-dir で場所を足すか設定を外すよう、ユーザーに頼むことを促しています。実際に --add-dir で外の置き場を足して起こすと、設定ありでも Write はそこへ書けました。足した場所は内側として扱われます。

Bash の書き込みは、設定ありでもそのまま通った

一方、Bash で echo hello > 外、cp inside.txt 外、python3 -c "open(外,'w').write('hello')" を走らせると、設定ありでも3つとも外にファイルができました。道具の結果はどれも「(Bash completed with no output)」で、断られた様子はありません。この設定が断るのはファイルの道具の書き込みだけで、Bash のコマンドが書く先は止まりませんでした。

設定の名前だけを見て「これで外には書けない」と思い込むと、ここを見落とします。権限の確認を飛ばすモードでは確認も出ないので、Bash からの書き込みはそのまま通ります。

柵の内側で止められたロボットと、柵の下をくぐる配管を覆いはじめた透明なドーム。ドームの隅には小さな窓が開いている

サンドボックスと組むと Bash も止まった

Bash の書き込みも縛るために、設定に sandbox を足しました。

{
  "permissions": {"blockWritesOutsideWorkingDirectories": true},
  "sandbox": {"enabled": true, "failIfUnavailable": true}
}

外の置き場をホームの下にした回では、Bash の3つの書き込みがすべて止まりました。echo は operation not permitted、cp は Operation not permitted、python は PermissionError: [Errno 1] Operation not permitted で終わり、外にファイルはできていません。Write と、リンクを通した Write は、これまでどおり設定のほうで断られました。failIfUnavailable: true(サンドボックスが使えなければ止まる)を付けても起動は止まらず、サンドボックスは使えていました。

設定を入れたときに、外への書き込みがどこで止まったかを図にすると次のとおりです。

設定を入れたとき、外への書き込みがどこで止まったか

Write・Edit は道具の段で設定が断り、Bash はサンドボックスがあるときだけ、書き先によって止まりました。

最初は /tmp の下に置いていて、サンドボックスでも書けてしまった

最初の回では、作業用の場所ごと /private/tmp の下に置いていて、外の置き場も /private/tmp の下でした。この回はサンドボックス(このときは "enabled": true だけ)を入れても、Bash の echo・cp・python の書き込みが3つとも外に通りました。Write は設定で断られていたので、サンドボックスだけが効いていないように見え、原因を探しました。

原因を見分けるため、外の置き場をホームの下に移して同じケースを走らせ直すと、上のとおり Bash の書き込みは止まりました。違いは書き先が /tmp の下かどうかで、macOS のサンドボックスは /tmp(/private/tmp)への書き込みを通していました。

公式の文書によると、サンドボックスで書き込める先は、作業ディレクトリ・$TMPDIR・--add-dir で足した場所です。macOS の $TMPDIR は /var/folders/… の下で、/private/tmp とは別の場所ですが、/private/tmp にも書けました。

試しの置き場を /tmp に作ると、サンドボックスが働いていないように見えてしまいます。同じことを試すときは、外の置き場をホームの下など /tmp 以外に置くと、結果を見分けられます。

使い分け

この設定は、ファイルの道具(Write・Edit)が作業ディレクトリの外を書き換えないための歯止めとして使えます。リンクや相対パスでも抜けられず、書かせたい場所は --add-dir で足せます。Bash の書き込みまで縛りたいなら、サンドボックスと組み合わせます。macOS では、/tmp に置いた大事な物はサンドボックスでも守られないので、守りたい物は /tmp の外に置きます。

Bash そのものを使わせない場合の道具の外し方は、Claude Code の --allowedTools は境界ではなかった に書きました。

確かめていないこと

  • NotebookEdit は試していません。
  • 権限モードは bypassPermissions だけで、acceptEdits などほかのモードは試していません(変更履歴は、どの権限モードでも断ると書いています)。
  • Linux でのサンドボックスの書き込み先(/tmp を含むか)は確かめていません。
  • モデルが Bash に、サンドボックスを外して走らせる指定(dangerouslyDisableSandbox)を付けた場合の扱いは確かめていません。sandbox.allowUnsandboxedCommands の既定がどう効くかも同じです。
  • どのケースも1回ずつで、繰り返して同じ結果になるかは確かめていません。

おわりに

blockWritesOutsideWorkingDirectories は、名前から受ける印象より狭く、ファイルの道具の書き込みだけを縛る設定でした。Bash まで含めて外への書き込みを縛るにはサンドボックスと組み、macOS では /tmp の下には書けてしまうことを覚えておくと、同じ所で迷わずに済みます。

※ この記事に書いた Claude Code の振る舞いは執筆時点の情報で、いまは変わっている可能性があります。