Claude Code を使っていると、こういう確認が出ます。
Bash command
cp .env.example .env
Do you want to proceed?
❯ 1. Yes
2. No
rm も delete も出てきません。押しますか。
押すと、手元の .env は消えます。 cp が確認なしで上書きするからです。
気になったので、使い捨てのディレクトリで試しました。.env に REAL_KEY=abc123 と書いておいて、上のコマンドを実行する。結果は REAL_KEY=。確認は一度も出ませんでした。
この記事は「承認プロンプトを見るときに、何を見ればいいのか」の話です。
危険なのは rm だけではない
rm を警戒する人は多いのですが、破壊はもっと地味な顔でやってきます。
| コマンド | 見た目 | 起きること |
|---|---|---|
cp a b |
コピー | b が上書きされる。確認なし |
mv a b |
移動 | 同上 |
> file |
リダイレクト | ファイルが空になる |
git checkout -- . |
巻き戻し | コミットしていない変更が消える |
git reset --hard |
巻き戻し | 同上 |
npm ci |
インストール |
node_modules を消して作り直す |
docker system prune |
掃除 | ボリュームごと消えることがある |
どれも rm を含みません。 承認プロンプトを「rm が無いか」で見ていると、全部通します。
見るべきは「戻せるかどうか」
自分は、承認プロンプトを見るときの基準を1つに絞りました。
これを実行して、間違いだったとき、元に戻せるか。
戻せるなら Yes。戻せないなら、いったん止まる。
| 操作 | 戻せるか |
|---|---|
| ファイルを作る | 戻せる(消せばいい) |
| ファイルを読む | 戻せる(何も変わらない) |
| テストを走らせる | だいたい戻せる |
| 上書き・削除 | 戻せない |
| git の巻き戻し | コミット前は戻せない |
| 外に出す(push / deploy / publish) | 取り消せないことがある |
この基準にすると、cp も > も「戻せない側」に入ります。rm を含むかどうかは関係なくなります。
ただ、毎回これを判断するのは無理
ここが本題です。
1日に何十回も出る確認を、毎回この基準で見るのは続きません。 疲れると Yes を押します。人間なので。
なので、判断そのものを仕組みに預けました。
仕組みに預ける:2段構え
1段目:許可の設定(permissions.deny)
.claude/settings.json に書きます。ここに書いたものは、確認プロンプトすら出ずに弾かれます。
{
"permissions": {
"deny": [
"Bash(echo DENYTEST *)",
"Bash(git push *)",
"Bash(rm -rf *)",
"Bash(rm -r *)",
"Bash(rm -f *)",
"Bash(git reset --hard *)",
"Bash(git checkout -- *)",
"Bash(sudo *)"
]
}
}
1行目は動作確認用のおとりです。echo は文字を出すだけなので何も壊れません。危険なコマンドを実行せずに、設定が効いているか確かめられます。
貼ったら Claude Code を一度終了して開き直してください。 設定は起動時に読み込まれるので、動いているセッションには反映されません。自分はこれで2回ハマりました。
確認方法:
echo DENYTEST hello をターミナルで実行してください
止まれば読み込まれています。
1段目には穴がある
ここは正直に書きます。deny は丸かっこで囲むと素通りします。
( git push )
これは手元で確認しました。Bash(git * push *) も Bash(* git push *) も追加しましたが、どれにも一致しませんでした。書き方を増やす方向では塞げません。
同じことが sh -c "..."、eval "..."、git -C /path push でも起きます。
(検証の詳細は別記事に書きました。この記事では対処だけ書きます)
2段目:実行直前のスクリプト(PreToolUse hook)
deny はコマンドの構造を見ています。だから形が変わると別物になる。
文字の並びそのものを見れば、丸かっこもクォートも関係なくなります。
Claude Code には、コマンドを実行する直前に自分のスクリプトを呼び出す仕組みがあります。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/deny-dangerous.sh",
"timeout": 10
}
]
}
]
}
}
止めるときは、この JSON を標準出力に出して exit 0 します。
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"理由"}}
肝は「境界」の書き方
素朴に grep -q "push" とやると、git commit -m "add push button" のような無害なコマンドまで止まります。コマンド名の位置を見ずに単語だけ探しているためです。
かといって厳しくすると ( git push ) を取りこぼす。
自分は境界を3つに分けました。
# コマンドとして実行される位置とみなす、直前にありうる並び
# かっこを入れているのが ( git push ) への対処
BEFORE='(^|[;&|(){}`\\]|["'"'"'])[[:space:]]*((sudo|env)[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*'
# コマンド名の直後にありうる文字
# 空白と行末だけにすると sh -c "git push" を取りこぼす
AFTER='([[:space:]]|$|["'"'"'`;&|)}\\])'
# コマンド名とサブコマンドの間に挟まるオプション
# これで git -C /path push が拾える
SUBCMD='([[:space:]]+-[^[:space:]]*([[:space:]]+[^-][^[:space:]]*)?)*'
あとは1行ずつ足すだけです。
block() {
printf '%s\n' "$cmd" | grep -Eq "${BEFORE}$1${AFTER}" || return 0
printf '{"hookSpecificOutput":{...,"permissionDecisionReason":"%s"}}\n' "$2"
exit 0
}
block "git${SUBCMD}[[:space:]]+push" 'git push を止めました。'
block 'cp([[:space:]]+-[^[:space:]]*)*[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+' 'cp の上書きを止めました。'
冒頭の cp .env.example .env は、この2行目で止まります。
足したら必ず流して確かめる
ここで2回踏みました。
# ダメ。AFTER と二重になって gcloud run で外れる
block '(gcloud|aws|heroku)[[:space:]]' '...'
# 正しい。境界は AFTER に任せる
block '(gcloud|aws|heroku)' '...'
# ダメ。db: の直後に migrate が来るので AFTER が外れる
block 'rails[[:space:]]+db:' '...'
# 正しい
block 'rails[[:space:]]+db:[a-z_:]*' '...'
どちらも「止めたつもりで止まっていない」状態になります。足したら流して確認してください。
printf '{"tool_input":{"command":"COMMAND"}}' | bash deny-dangerous.sh
"deny" を含む JSON が返れば止まります。危険なコマンドを実行せずに確認できます。
自分は50項目の自己テストを書いて、止めたいものと通したいものを両方流すようにしました。npm run build や git status が通ることも毎回見ています。止めすぎは、止まらないのと同じくらい困るので。
防げないこと
正直に書きます。
hook が異常終了・タイムアウトすると、判断を出さずに操作はそのまま進みます。 安全側に倒れません。公式ドキュメントに書いてある挙動です。deny と両方置く前提で考えたほうが安全です。
スクリプトファイルに書いてから実行する形は止まりません。 実行される文字列は「そのファイルを動かす」だけになるので、中身までは見ていません。
誤検知します。 文字列を見る仕組みなので、echo "git push は禁止" >> README.md のような、危険な単語を含むだけのコマンドも止まります。取りこぼすより誤検知する側に倒しました。止まったら Write / Edit ツールで書かせれば通ります。
Windows では確認していません。 deny は動くはずですが、シェルスクリプトは動きません。
Claude Code の外で自分が打つコマンドには関係ありません。
まとめ
- 承認プロンプトを
rmの有無で見ない。cpも>も戻せない - 基準は「間違いだったとき、元に戻せるか」の1つでいい
- ただし毎回判断するのは続かない。 仕組みに預ける
-
denyは構造を見ている。丸かっこで抜ける -
hookは文字の並びを見る。境界の書き方が肝 - 足したら流して確かめる。止めすぎも困る
自分は文系の大学生で、プログラミングは得意ではありません。得意でないぶん Claude に任せる範囲が広くなり、その分だけ「止める場所」を先に決めておく必要がありました。
確かめていないことは、確かめていないと書きました。
ここまでの内容で塞げます。この記事だけで完結するように書きました。
環境に合わせて設定を組んで、自己テストを通した状態で渡す作業はココナラで受けています。macOS / Linux のみ。