Skip to main content
これは技術ガイドの 4 ページ目です。アーキテクチャで説明したガードパイプラインを前提に、破壊的コマンド分類器の判定方法とエッジケースに絞って説明します。explain の出力を読む場合や、意図どおりに動くカスタムルールを書く場合に必要な情報です。 分類器はガードの最後の段階です。その前に、上限付きのツール入力抽出、パーサー予算、ポリシーファイルと Git メタデータの保護、ポリシースナップショットの読み込み、シークレット保護がすでに完了しています。これらは順序付きガードステージで規定されています。上限付き解析と常時有効なポリシーファイルおよび Git メタデータのガードは、すべての安全レベルで fail closed になります。無効なポリシースナップショットには保護的なフォールバックを適用し、シークレット保護は解決済みポリシーに従います。破壊的コマンド分類器は、前段ですでに行われた判定を緩和できません。 ディスパッチフロー(セグメントへ分割し、環境変数の代入とラッパーを除去し、先頭コマンドを特定して、対応するアナライザーへ渡す)は、アーキテクチャに図示しています。このページでは、その続きとして、各アナライザーが受け取ったセグメントを処理する方法と、安全レベルによって境界が変わる位置を説明します。

安全レベルの境界

3 つの安全レベルは、3 つの機能を組み合わせた preset です。レベルごとの境界を知ると、どの操作がブロックされるかを判断できます。 3 つの preset のいずれにも一致しない機能の組み合わせは、有効レベル custom として報告されます。

Standard

standard は、認識できる破壊的コマンドをブロックします。ただし意図的に、敵対的な入力に耐える水準にはしていません。動的な入力や敵対的な入力に対しては、あくまでベストエフォートです。
  • 安全に見える解析不能なテキストは許可します。 echo 'unterminated は許可されます。
  • 認識できる破壊的テキストは、解析不能でもブロックします。 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 のみの許可を参照してください。
  • 動的な rm -rf の対象は、一律にはブロックしません。 rm -rf "$target" は standard では許可し、fail closed 機能が有効な場合だけブロックします。
  • standard はこのほかにも、動的な実行ファイル、コマンド置換で組み立てられた保護対象のコマンド構造、検証できないその他の再帰削除対象、組み込みの機密パスに対するメタデータのみの単独チェックを、意図的に許可します。
  • 検証できるローカルの生成コマンドに対する eval と source は許可します。 eval "$(ssh-agent -s)" と source <(kubectl completion bash) は、置換の本体が完全にリテラルな単純コマンド 1 つで、リモート取得コマンドでもシェルでもコマンドラッパーでもない場合、standard では通ります。本体は引き続き解析しますが、それが出力するシェルは解析しません。strict と paranoid は、動的なシェルの実行元をすべて拒否します。standard のみの許可を参照してください。
  • 一方 standard でも、機密情報の内容へのアクセス、設定済みの deny path、致命的な操作に対する保護は緩和されません。

Strict

strict モードは fail closed の機能を有効にします。厳しくなるのは、解析できない場合の扱いだけではありません。
  • 解析できないコマンドをブロックします。 echo 'unterminated は、安全に解析できなかったという理由で拒否されます。
  • メタデータのみで機密パスを探る操作をブロックします。 test -f ~/.ssh/id_rsa と find ~/.ssh -type f は standard では許可され、strict ではブロックされます。
  • standard 限定のインラインデータ緩和が無効になります。 standard では、インタープリターのインラインコードに含まれる機密パスのリテラルは、コードにファイルシステム操作、コマンド実行、eval のいずれの痕跡もなければ、実行されないデータのままです。strict はすべてのリテラルを候補として残し、リテラルの中身もスキャンします。
  • heredoc が fail closed になります。 heredoc を含むコマンドは、他のアナライザーがそのセグメントを処理する前に、heredoc の解析で説明する狭い条件を満たさないかぎり拒否されます。条件とは、標準入力に接続された展開されない 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 は通過します。
  • 検証できない破壊的対象をブロックします。 次の 5 つのルールは fail closed 機能によって切り替わるため、standard では許可され、strict ではブロックされます。
strict 相当の各ルールは個別に無効化できます。ただし、破壊的コマンドのルール ID を持たない fail closed の結果(パーサーによる fail closed と機密パスに関する判定)には、strict の動作がそのまま適用されます。

Paranoid

paranoid モードは、strict に 2 つの機能を追加します。
  • paranoid の rmは、カレントディレクトリの内側であっても、一時ディレクトリ以外を対象とする再帰的な強制削除をブロックします。rm -rf ./cache も Remove-Item ./cache -Recurse -Force もブロックされます。一時ディレクトリと設定済みの allow path は引き続き許可されます。
  • 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、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 モードでは、内容にかかわらずすべての 1 行コードをブロックします。 busybox の呼び出しは特別扱いし、サブコマンドをコマンドの位置に移して解析し直します。awk/gawk/mawk のプログラムについては、system() の呼び出しとバッククォートによるコマンド置換をスキャンします。

transparent wrapper

標準的なラッパー(sudo、env、command、builtin)は常に取り除きます。transparent wrapper として宣言したプロキシコマンドも同様です。宣言の方法と制約はカスタムルールを参照してください。ここでは、エンジンがラッパーをどう透過するかを説明します。 透過するときは、ラッパーのフラグと環境変数の代入より後にある最初の保護対象になりうる子コマンド、または明示的な -- の直後のトークンを探します。透過した後は、組み込みの解析とカスタムルールの両方をその子コマンドに適用します。rtk git reset --hard は組み込みのルールでブロックされ、rtk docker system prune は該当するカスタムルールに一致します。保護対象になりうる子コマンドがない場合は、透過しません。 宣言されていないプロキシや、見える形で子コマンドを実行せずに書き換えたり隠したりするプロキシは、透過できません。これらを拾える可能性があるのは、最上位で行う危険な文字列のフォールバックスキャンだけです。 同じコマンド内では、セグメントをまたいでカレントディレクトリを追跡します。rm と find は、この追跡結果を基準に対象を分類します。どの cd が追跡され、どの場合にカレントディレクトリが不明になるかは、作業ディレクトリの追跡を参照してください。
既知の制限:インタープリターの長形式フラグ(--eval、--execute、--require、および =value を付けた形式)は、すべての処理経路で認識されるわけではありません。そのため、コードの引数を取り出せない場合があります。既知の制限を参照してください。

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" と同じ扱いです。引用符付きの代入に対する判定の先送りは、呼び出された関数の本体内にも適用されます。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 ではすべて拒否されます。

heredoc の解析

heredoc の本文は、標準入力に渡されるテキストです。そのテキストをデータとして扱うかプログラムとして扱うかは、読み取り側のコマンドが決めます。そこでエンジンは、他のアナライザーがセグメントを処理する前に、狭く定義した対応条件を確認します。次の条件をすべて満たす場合だけ通過します。
  • コマンドに含まれる heredoc(<< または <<-)が、標準入力(fd 0)に接続されたもの 1 つだけであること。
  • 展開されない heredoc であること。デリミタが引用符で囲まれている(<<'EOF')か、引用符がなくても本文に $、バッククォート、バックスラッシュが含まれないことを指します。どちらの場合もシェルは展開もエスケープ処理も行わないため、本文はバイト単位でそのまま読み取り側に渡ります。
  • 標準入力と競合する他の入力リダイレクト(<、<<、<<-、<<<、<&、<>)がないこと。
  • 読み取り側が、リテラルで書かれた 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 と paranoid ではすべて拒否します。standard では、次の 3 つの経路のいずれかで本文を解析します。
  1. 構文チェックだけを行うシェル(bash -n など)の標準入力に渡される展開されない heredoc は、実行されないデータとして許可します。
  2. インタープリター(python/python2/python3、node、ruby、perl)の標準入力に渡される展開されない heredoc。ただし、コマンドライン上の他のすべての語が - で始まるリテラルで、インタープリターが標準入力からプログラムを読む場合にかぎります。python3 - <<'PY' は条件を満たしますが、python3 tool.py <<'PY' は満たしません。後者では標準入力がスクリプトへのデータになるためです。本文はインタープリターのルールで解析します。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')、エンジンはそのパスと本文を記憶します。同じコマンド内で後から 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 配下のパスは追跡しません。追跡中のパスにその後書き込みやリダイレクトがあった場合、記憶した本文は無効になります。

ブレース展開

パーサーは、リテラルとして解決できた語の中の {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 文字を超える展開です。コマンド位置のブレースは、代わりにパーサーの上限で制限します。こちらを超えた場合は、解析が Structural command analysis limit exceeded. で終わり、すべての安全レベルで拒否になります。

Git ルールエンジン

git のアナライザーは、サブコマンドとオプションを取り出して危険なオプションのパターンと照合し、理由と分類(localDiscard または sharedState)を返します。この分類によって、worktree での緩和の可否が決まります。一時ルートの緩和がこの分類を使うのは、linked worktree の場合だけです。 一時ルートの緩和がこの分類を参照するのは、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 のパターンの全一覧は、ブロックされるコマンドを参照してください。
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 関連の環境変数による上書きを参照してください。
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> はそのまま効きます。
reset --hard/--merge は、-- より前に ref がある場合(ブランチのポインターを動かすため)は sharedState、それ以外の場合(作業ツリーの変更だけを破棄するため)は localDiscard に分類します。

作業ディレクトリの追跡

解析エンジンは、1 つのコマンドのセグメントをまたいで実効カレントディレクトリを保持します。cd の対象がリテラルで解決できる場合、その結果が追跡中のカレントディレクトリになります。解決できない場合はカレントディレクトリを不明にし、rm の解析はカレントディレクトリを基準にできないものとして扱います。どちらの場合も元のカレントディレクトリは基準として残るため、後述の分類では両方を使います。 pushd と popd は、常にカレントディレクトリを不明にします。追跡するのは cd だけです。 追跡できる cd の形式。 オプションは最初の非オプションのトークンまで読み取り、読み取ったオプションはすべて -L/-P の組み合わせでなければなりません。さらに、省略可能な -- の後ろに残るオペランドがちょうど 1 個である必要があります。したがって cd -- /tmp/scratch と cd -P /tmp/scratch は追跡できます。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 はカレントディレクトリが不明になります。 CDPATH。 先頭が .、/、ドライブレターのいずれでもない裸のオペランドは、次のどちらかに当てはまるとカレントディレクトリを不明にします。どこかのセグメントに CDPATH= または CDPATH+= の語がある(先頭の export の有無は問いません)か、hook の環境に CDPATH が設定されているかです。./sub と絶対パスのオペランドは、シェルが CDPATH を参照せずに解決するため、引き続き追跡できます。 本体のスコープ。 then、do、case は本体を開き、fi、done、esac は本体を閉じます。elif は、その直前の分岐を閉じます。本体が開いている間の代入は、その本体の中でだけ有効で、最後の本体が閉じた時点で破棄します。そのため、後続の cd $VAR はカレントディレクトリを不明にします。コマンド語は先頭の do/then/else より後ろで探すため、if true; then unset R; fi は束縛を解除します。 for ループ。 for NAME in のリストがリテラルな 1〜8 語であれば、NAME を束縛した解析状態を語ごとに分岐させ、そのすべてが通過する必要があります。それ以外の for の形式は、リストのない for c やコマンド置換で組み立てたリストも含め、NAME の束縛を破棄します。 追跡中のカレントディレクトリは、次の 4 つの判定に使います。
  • カレントディレクトリ自体かどうかの判定は、追跡中のカレントディレクトリと元のカレントディレクトリのどちらかに解決される対象に一致します。そのため 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 です。
トレースに cwd-change の手順が記録されるのは、カレントディレクトリが不明になった場合だけです。追跡できた cd では何も記録されません。explain トレースを参照してください。

再帰的削除対象の分類

rm の解析では、再帰フラグと強制フラグの組み合わせを検出し、対象を取り出して、カレントディレクトリを基準に分類します。次の順に確認し、最初に一致した分類を採用します。この順序そのものに意味があります。 この順序から、はっきり述べておくべき結果が 2 つあります。
  • ステップ 4 がステップ 7 より前にあるため、リポジトリを含む allow path を設定しても、Git メタデータの保護は緩和されません。
  • ステップ 6 がステップ 7 より前にあるため、allow path は動的な対象や検証できない対象には適用されません。
ステップ 11 には、追跡できた cd が作り出したカレントディレクトリが必要です。実行されるかどうかは作業ディレクトリの追跡で決まります。追跡中のカレントディレクトリが元のカレントディレクトリを含む場合、このステップは適用されません。そのため、/tmp 配下のワークスペースから上のディレクトリへ移動しても、兄弟ディレクトリの削除は拒否されたままです。また、絶対パス、~ で始まるパス、動的な対象、.. を含む対象にも適用されません。 allow path は、絶対パスか ~/ で始まるディレクトリである必要があります。すべての安全レベルで、rm、Remove-Item、find -delete に適用されます。シークレット保護、deny path、root、ホームディレクトリ、保護対象の Git メタデータが緩和されることはありません。$HOME そのものや $HOME を内側に含む上位パスは、検証時と正規化後の両方で拒否します。シンボリックリンクによる境界の抜け道も対象外です。 この分類は、再帰フラグ(-r/-R/--recursive)と強制フラグ(-f/--force)の両方がある場合に実行します。パスの比較には正規化(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 も書き換えるため、これらのルートとコマンドの引数を同じ表記で比較できます。信頼された一時ルートとの比較も、Windows では大文字小文字を区別しません。小文字の MSYS パスが Windows 本来の一時ディレクトリ配下にあれば、カレントディレクトリ外ではなく一時ディレクトリの対象として分類されます。
日常的な利用で効いてくる違いがあります。rm -rf ./subdir(カレントディレクトリの中)は許可されますが、rm -rf .(カレントディレクトリそのもの)はブロックされます。許可されるコマンドを参照してください。

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 の削除ルールを適用しませんが、auto であれば明示的な Remove-Item に加えて Get-Content、Set-Content、Add-Content、Copy-Item、Move-Item を検出し、gc や cp のような別名も引数が PowerShell のパス式で書かれていれば検出します(gc $HOME\.ssh\id_rsa)。どちらの場合も、git.reset-hard のようなシェル共通のルールは引き続き有効です。

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

この 3 つの形式は、解析できないテキストに対するヒューリスティックなスキャンにも含まれます。そのため、echo や rg で始まる場合を除き、解析できないテキストの中にある dd of=/dev/…、mkfs /dev/…、shred <arg> は raw-text.dangerous-command としてブロックされます。いずれも致命的な操作のルールではないため、マスタースイッチとルール単位の override の優先順位に従います。

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

xargs と parallel では、対象が動的な入力(パイプされた標準入力やプレースホルダーの展開)から来るため、カレントディレクトリを基準に検証できません。parallel の SSH リモートモード(-S/--sshlogin)も、worktree での緩和を無効にします。 エンジンが 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 / のような致命的な操作はそれ自身のルールでブロックされます。::: のグループをジョブへ展開する処理は派生トークンの上限に計上され、ジョブの組み合わせで上限を使い切ると fail closed になります。 子コマンドがカスタムルールの対象である場合、その引数のどこかに置換文字列があると動的な入力とみなし、xargs.shell-dynamic または parallel.shell-dynamic として拒否します。どの入力値ならルールに一致するかを逆算することはしません。

Worktree の緩和

worktree モードが有効な場合、検証済みの linked worktree 内では、ローカルの変更を破棄する git コマンドを許可します。緩和されるには、次の条件をすべて満たす必要があります。
  1. 一致したルールが localDiscard に分類されていること(Git ルールエンジンの表を参照)。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)。
検証済みの linked worktree 内でも、次の操作は緩和されません。$、*、?、[ を含む動的な引数。ブランチの強制リセット(git checkout -B/-Bf、または -f や --discard-changes を伴う git switch -C/-Cf)。-f を複数指定した git clean(使い捨ての worktree の境界を越え、入れ子の git リポジトリを削除するために必要な指定)。--recurse-submodules オプション、または submodule を再帰的に扱う設定。
先頭のグローバルオプションを順に処理して、実際の git の作業ディレクトリを解決します。-C <path> と、続けて書いた -C<path> は、ディレクトリの変更として適用します。--git-dir/--work-tree(値を分けて書く形式と = でつなぐ形式のどちらも)は、git のコンテキストを明示的に指定したものとみなし、緩和をすべて無効にします。

一時ルートの緩和

もう 1 つの緩和は、信頼された一時ルート配下の使い捨てリポジトリで git を実行する場合を対象にします。worktree の緩和とは別の仕組みで、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 に該当しないことも必要です。ブランチ、stash、タグの操作は、その worktree が属するリポジトリを変更するため、引き続きブロックします。git reset --hard <ref> は shared state であり、ここでも緩和されません。 git.worktree-remove-force は、リポジトリではなくオペランドを見て判断します。git 自体はワークスペースで動く一方、使い捨ての対象は削除されるパスだからです。このルールが読むのは、remove の後ろにあるリテラルなオペランド 1 個です。 追跡中のシェル変数の代入による置き換えが起きるのは、パーサーがその語を変数展開として認識し、かつその語のリテラル部分に $ とバッククォートが 1 つもない場合だけです。それ以外では、オペランドを書かれたとおりのテキストとして扱います。 そのうえで、オペランドは絶対パスであり、空白、$、バッククォート、glob 文字を含まず、シンボリックリンクではない実在のディレクトリである必要があります。その実パスは、信頼された一時ルートの配下にあり、一時ルートそのものではなく、ワークスペースの祖先でも配下でもない必要があります。そのため、git worktree remove --force "$S/main" と引用符なしの $S/main は緩和の対象になり得ますが、'$S/main'、./linked のような相対パス、存在しないパス、シンボリックリンクには、いずれもルールをそのまま適用します。 トレースには、解除した理由と git の作業ディレクトリを持つ temp-root-relaxation の手順が記録されます。explain トレースを参照してください。

カスタムルール

対応する組み込みのアナライザーがない場合は、フォールバックとしてカスタムルールを実行します。カスタムルールは厳密に「追加のみ」です。ブロックは追加できますが、組み込みのブロックを上書きしたり保護を緩めたりはできません。ルールは <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 の照合を参照してください。 書き方の詳細と照合の仕様は、カスタムルールを参照してください。

分類を検査する

エンジンが特定のコマンドをどのように評価したかを正確に確認するには、explain を実行します。
人が読む形式の出力は、各セグメント、解析のステップ、ルールの評価を順に示します。JSON 出力は構造化されたトレースを返します。そのスキーマは explain トレースを参照してください。 判定がおかしい原因がコマンド側ではなく設定側にあると思われる場合は、ランタイムがフォールバックのポリシーで動いていないか確認してください。npx cc-safety-net status が ready か degraded かを表示します。該当する設定元の修復方法は、設定の復旧を参照してください。

次に読むページ

技術ガイドは、ユーザー向けのライフサイクルの説明から設計の理由へと順に降りていく構成です。このページはその 4 番目にあたります。
  • 戻る:アーキテクチャには、この分類処理が最後を担う順序付きのガードステージがあります。
  • 次へ:設計原則では、分類をパターン照合ではなく意味に基づいて行う理由と、レベルの境界を現在の位置に置いた理由を説明します。
関連ページ:レベルの選び方はモード、判定結果の一覧はブロックされるコマンドと許可されるコマンド、分類処理が認識しない範囲は既知の制限を参照してください。
最終更新日 2026年9月21日