AetherEchoesEngineering
Engineering#01919 min1,8766 view

破壊的コマンドをローカルで止める dcg を実運用で評価する

coding agent に端末を預けると、rm -rf や git reset --hard は実行後にしか気づけない。Rust 製の dcg は、その一瞬に割り込んで破壊的コマンドを実行前に止めるローカルのガードレールだ。既定パック、逃げ道の設計、fail-open の思想を、実際に導入した手触りから評価する。

SoSoraEndo2026年7月13日 09:069 min1,876

動画で読む

エージェントに端末を渡すと、事故は「実行後」に気づく

結論から書く。coding agent に shell を任せるなら、rm -rfgit reset --hard を実行される前に止める仕組みが要る。事故は、いつも実行後に気づくからだ。dcg(Destructive Command Guard)は、まさにその「実行前の一瞬」に割り込むローカルのガードレールで、Rust 製の単一バイナリとして動く。

私がこの手の道具を真面目に探し始めたのは、Claude Code に git reset --hard を打たせて、未コミットの変更を数時間分飛ばした翌日だった。バックアップは無い。reflog も及ばない。あのときの「あ」という声が、今このセクションを書かせている。人間なら一瞬手が止まる操作でも、エージェントは 0.2 秒で確定させてしまう。速さは長所だが、破壊の速さでもある。

dcg はこの非対称性を埋めにいく。エージェント(Claude Code / Codex CLI / Gemini CLI / GitHub Copilot CLI / Cursor など)が発行するコマンドを、実行前に JSON の hook payload(コマンド情報を載せた受け渡し用データ)として受け取り、危険と判定したら握りつぶす。判定が付くまで、コマンドは走らない。

dcg が実際に止めるもの

dcg が既定で止めるのは「取り返しのつかない」系のコマンドだ。設定ファイルが無くても、core.filesystemrm -rf 保護)と core.git、そして system.diskmkfs やデバイスへの dd)の 3 つのパックが最初から効いている。

具体的なブロック対象を並べると、なかなか生々しい。git reset --hard(未コミットの変更を消す)、git push --forcegit clean -f、temp 以外での rm -rf ./src。データベースなら DROP TABLETRUNCATE。運用系だと docker system prunekubectl delete namespace、AWS の terminate-instancesdelete-db-instance、Windows の format まで入っている。

面白いのは、これらが「pack」という単位でモジュール化されている点だ。database / kubernetes / cloud / CI・CD など 50 を超えるオプションパックがあり、~/.config/dcg/config.toml[packs] セクションで使うものだけを足す。私は普段 Kubernetes を触らないので kubernetes パックは切っている。要らない守りは、誤検知の温床にしかならないからだ。

「止められすぎ」への逃げ道が用意されている

ガードレールの成否は、ブロックの精度より逃げ道の設計で決まる。止めすぎる道具は、結局 DCG_BYPASS=1 を常用させて形骸化する。dcg はこの罠を分かっていて、粒度の違う複数の抜け道を持っている。

一番使うのは一時許可だ。ブロック時に表示される 6 桁コードを使い dcg allow-once 123456 と打つと 24 時間だけの例外ができる。--single-use を付ければその 1 回きり。恒久的に許したいなら dcg allowlist add core.git:reset-hard -r "理由" で、理由付きの allowlist に積む。-r で理由を必須にしている設計が、地味に効く。半年後の自分が「なぜこれを許したのか」を辿れるからだ。

git rebase の途中で走る checkoutrestore のように、危険だが正当な操作もある。ここには dcg rebase-recover という 120 秒限定の一時許可が別に用意されている。本当の緊急時は、環境変数 DCG_BYPASS=1 をコマンドの頭に付けて素通しもできる。守りと逃げの両方を同じ密度で設計してあるのが、この道具の一番信用できるところだ。

fail-open という思想と、その裏返し

dcg の判定は whitelist 優先で、最後は「分からなければ通す」(fail-open) に倒れる。安全パターンに一致すれば即 ALLOW、破壊パターンに一致すれば理由付きで DENY、どちらにも当たらなければ通す。処理には 200ms のハード上限があり、超えたらこれも通す。「気づかないうちに開発を止める」ことを、何より嫌う設計だ。

この思想には裏返しがある。fail-open は「守り切る」ことより「邪魔をしない」ことを優先する。だから dcg は最後の砦ではなく、最初の一枚だと捉えるのが正しい。パースに失敗した危険コマンドは、原理的にすり抜けうる。厳しくしたいなら DCG_FAIL_CLOSED=1 で「読めない入力はブロック」に倒せるが、その瞬間から誤検知と付き合う覚悟が要る。私は開発機では fail-open、CI では fail-closed 寄り、と場所で使い分けている。

誤検知を減らす工夫も入っている。git reset --hard という文字列がコメントや文字列リテラルの中にある(データ)のか、実際に実行される(コマンド)のかを、文脈分類で見分ける。この丁寧さは、.gitignore だけでは守り切れない領域を別の層で塞ぐ発想に近く、無視の設計を .gitignore の外まで広げる話と地続きだと感じる。

導入して分かった、実運用の手触り

入れる前に dcg testdcg explain で挙動を確かめられるのが、実運用ではよく効く。dcg test "rm -rf ./build" は実行せずに判定だけ返し、dcg explain "git reset --hard" は「なぜ止めたか」の決定過程を出す。exit code は許可が 0、ブロックが 1。CI に組むなら dcg scan --staged で staged ファイルを走査でき、--format sarif で GitHub の Code scanning にも流せる。

ブロックされたときの表示も、素っ気なさすぎず、うるさすぎない。実際に止まると、標準エラー側に BLOCKED dcg の枠、その下に Reason(なぜ止めたか)、Command(何を止めたか)、Tip(代替案)の 3 行が出る。git reset --hard を止めたときは「まず git stash で退避しては」と提案までしてくれる。責めるのではなく次の一手を差し出す文面で、ここは思ったより効いた。人は、叱られると逃げ道の DCG_BYPASS=1 を覚えるが、代替案を出されると素直に従うものらしい。

Claude Code への組み込みは、~/.claude/settings.jsonPreToolUseBash matcher に dcg を刺すだけだ。これはエージェントの外側に観測点や制御点を置くという発想の一種で、エージェント側の挙動を SDK から観察した記録とも根は同じだと思っている。中身をいじるのではなく、境界に一枚かませる。

正直に言えば、dcg を入れて一番変わったのは私の心拍数だ。エージェントに Bash を許すときの、あの微妙な緊張が一段下がった。もちろん万能ではない。fail-open である以上、これ一枚に安心を全部預けてはいけない。それでも「実行前に 200ms だけ割り込む」という一点に絞った小さな道具が、端末をエージェントに預ける時代の心理的コストを確かに下げてくれる。守りは薄くていい。ゼロと薄いの差が、いちばん大きいからだ。

この記事は AI が下書きを書き、運営者である私が公開前に内容を確認・編集して公開している。

参考文献

  1. Destructive Command Guard (dcg) — GitHub
  2. Claude Code hooks リファレンス(PreToolUse / Bash matcher)

Reaction

Share

X (Twitter)