CLAUDE CODE/確認プロンプトを外して走らせる型
「毎回の確認が面倒だから外したい。でも事故らせたくない」人向けの、そのまま真似できる運用ルール一式。
第1部のhook(危険コマンドを止めるスクリプト)を、不具合を2つ直した版に差し替えました。
rm -rf のような文字が入っているだけで作業が止まっていました。実行されない本文は判定の対象から外しています(本文がそのままシェルで実行される形は、これまでどおり止めます)やること(1分)
openssl dgst -sha256 ~/.claude/hooks/block-dangerous-bash.sh の値が、第1部②に書いてある値と一致するか確かめるsettings.json は 2026-08-20 版から変更ありません。このページのURLを自分の Claude Code にもう一度貼って「更新のお知らせの通りに入れ替えて」と頼んでもOKです。
第1部のhook(危険コマンドを止めるスクリプト)に検出漏れが10件見つかったので、判定のやり方ごと入れ替えました。止まると思っていたのに素通りしていた例です(すべて実測)。
| 書き方 | 以前 | 現在 |
|---|---|---|
rm -rfv /tmp/x | 素通り | ブロック |
rm -r -f /tmp/x | 素通り | ブロック |
rm --recursive --force /tmp/x | 素通り | ブロック |
git push origin main --force | 素通り | ブロック |
git reset HEAD~1 --hard | 素通り | ブロック |
git branch feature -D | 素通り | ブロック |
原因は、判定が「rm -rf という並びが出てくるか」という書き順ありきの探し方だったこと。フラグを分けたり後ろに書いたりするだけで外れていました。コマンドを単語に分解して危険なオプションの組み合わせで判定する方式に変え、書き順・フラグの分離・ロングオプション・引数の後置・絶対パス指定に強くしています。あわせて、判断できないときは素通りさせず止める作りにしました。
やること(5分)
やること(3分)
hooks ブロックを差し替え(hookが起動できなかったときの受け皿を足しました)このページのURLを自分の Claude Code にもう一度貼って「更新のお知らせの通りに貼り替えて」と頼むのがいちばん早いです。下の「Claude Code に任せる場合」にAI向けの手順を書いてあるので、二重登録などをせずに入れ替えてくれます。
jq は不要になりました。Windowsの方も Git for Windows(Git Bash)があれば動く作りになっています(実機での確認は未了です)。
~/.claude/CLAUDE.md(毎回自動で読まれる指示書)に貼るURLをうまく読めないときは「📋 Markdownをコピー」で全文を渡してください(クリップボードに入るだけで、ダウンロードは不要)。手作業で入れたい人は、第1部にファイルの中身をそのまま載せているのでコピペでOK。
Claude Code は通常、コマンドを実行するたび「これ実行していいですか?」と聞いてきます。バイパスモード(--dangerously-skip-permissions)はこの確認を全部スキップするモード。止まらないので作業速度は跳ね上がりますが、裏返すと「止める人がいない」状態です。以下は朝倉が自分の運用で立てている柵をそのまま配布用にしたものです。
人が読む場合は読み飛ばして第1部へ進んでください。
curl でファイルを取得して chmod +x するだけ)をそのまま実行して設置する。ネットワークが使えない場合に限り、第1部②のコードブロックから全文コピーして作成する。openssl dgst -sha256 ~/.claude/hooks/block-dangerous-bash.sh を実行し、値が次と一致することを確認する。一致しなければ設置は失敗しているので、ユーザーに伝えて止まる。安全柵は「入れたつもり」がいちばん危険。
2f2f819052ceef463d9a4111bbb175c50f9790084cda3126d2cd96b2925d7d6d
~/.claude/settings.json は上書きせず、既存の内容を残したまま permissions と hooks を統合する。作業前に ~/.claude/settings.json.bak へ退避しておく。ファイルが無ければ第1部①の内容で新規作成する。
permissions.deny に既存の項目があれば、消さずに第1部①の項目を足す(重複は1つにまとめる)。hooks.PreToolUse に block-dangerous-bash.sh を呼ぶ設定がすでにある場合は、追加ではなく置き換える。このページを2回目以降に読ませたとき(更新目的)に、同じhookが二重に登録されるのを防ぐため。古い設定に残っている "timeout": 10 も一緒に消す。block-dangerous-bash.sh 以外のhookが登録されている場合は、それには手を触れない。python3 -m json.tool ~/.claude/settings.json が通ることを確認する。壊れているとClaude Codeが起動しなくなる。~/.claude/CLAUDE.md に貼り、「本番リスト」を本人の環境に合わせて埋める必要があること(ここはAIでは埋められない)。バイパスモードは「AIを信用して確認をやめる」運用ではありません。確認をやめる代わりに、機械が止める柵を先に立てる運用です。柵を立てずにフラグだけ外すのが、いちばん事故ります。
良いこと:止まらない。10ステップの作業が一気に最後まで進む。
怖いこと:止める人がいない。rm -rf も本番DBの書き換えも、聞かれずに実行される。
| 層 | 誰が止めるか | 何を止めるか | 破られるか |
|---|---|---|---|
| システム層 | 機械(設定とスクリプト) | rm -rf sudo git push --force などの破壊系コマンド | 破られない。AIの判断は一切介在しない |
| 行動層 | Claude 自身 | 「本番を触る」「大量削除」「外部への発信」など、文字面では判定できない領域 | 忘れることがある |
なぜ2層いるか。hook(システム層)はコマンドの文字列しか見ません。「会員サイトの本番DBを書き換える」は文字列としてはただの curl で、危険と判定できない。逆に行動層だけだと、Claudeが忘れた瞬間に柵がなくなる。機械で止められるものは機械で、無理なものだけルールで。
~/.claude/settings.json に追記するpermissions と hooks の2ブロック。すでに settings.json がある人は、既存の内容を消さずにこの2つを足します。
{
"permissions": {
"defaultMode": "bypassPermissions",
"deny": [
"Bash(rm -rf:*)",
"Bash(rm -fr:*)",
"Bash(sudo:*)",
"Bash(sudo)",
"Bash(dd:*)",
"Bash(mkfs:*)",
"Bash(mkfs.*:*)",
"Bash(shutdown:*)",
"Bash(reboot:*)",
"Bash(halt:*)",
"Bash(poweroff:*)",
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git reset --hard:*)",
"Bash(git clean -f:*)",
"Bash(git clean -fd:*)",
"Bash(git clean -fdx:*)",
"Bash(git branch -D:*)",
"Bash(npm publish:*)",
"Bash(pnpm publish:*)",
"Bash(yarn publish:*)",
"Bash(chmod -R 777:*)",
"Bash(docker system prune:*)",
"Bash(docker volume rm:*)",
"Bash(docker volume prune:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$HOME/.claude/hooks/block-dangerous-bash.sh\" || printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"安全柵の起動に失敗したためブロックしました。~/.claude/hooks/block-dangerous-bash.sh があるか、改行コードが壊れていないか確認してください\"}}'"
}
]
}
]
}
}defaultMode: "bypassPermissions" を書いておくと、起動フラグを忘れてもバイパスモードで立ち上がります。「普段は確認あり、必要なときだけ」で使いたい人はこの行だけ外して claude --dangerously-skip-permissions で起動。
command の後半にある || printf ... は、hookそのものが起動できなかったときの受け皿です。スクリプトが見つからない・改行コードが壊れている・途中で落ちた、のいずれでも「判断できなかった」として拒否側に倒します。以前はこの受け皿が無く、hookが動かないときは黙って全部素通りしていました。
timeout は指定しません(既定の60秒に任せる)。短く設定すると、重い処理の最中に安全柵のほうが先に力尽きて無効になることがあります。
推奨はこのコマンドです。ターミナル(WindowsはGit Bash)に貼ってEnterを押すだけで、最新のhookが設置され実行権限までつきます。
mkdir -p ~/.claude/hooks
curl -fsSL https://koumon-bypass-rules.pages.dev/block-dangerous-bash.sh -o ~/.claude/hooks/block-dangerous-bash.sh
chmod +x ~/.claude/hooks/block-dangerous-bash.shダウンロードして置くだけで、取得したものを実行はしません。
設置できたか不安なときは openssl dgst -sha256 ~/.claude/hooks/block-dangerous-bash.sh を実行して、次の値と一致すればOKです。2f2f819052ceef463d9a4111bbb175c50f9790084cda3126d2cd96b2925d7d6d
コマンドが使えない環境の方は、下のコードで ~/.claude/hooks/block-dangerous-bash.sh を作り、中身をそのまま貼ります。約450行あるのでコピー漏れにご注意ください(上のコマンドが使えるならそちらの方が確実です)。
#!/usr/bin/env bash
# 破壊的コマンドの安全柵(Claude Code の PreToolUse フック)
#
# 設計方針:
# 1. 外部依存を最小化 — jq を使わない。Git for Windows の Git Bash に jq が同梱されないため。
# 使うのは bash 組み込み / awk / grep / sed / tr のみ。
# 2. フェイルクローズ — 必要な道具が無い、解析できない、想定外の入力、のいずれでも
# 「黙って許可」にしない。判断できないときは拒否する。
# 3. 位置に依存しない — コマンドを単語に分解し、危険なオプションの「組み合わせ」で判定する。
# フラグの順序・分離・ロングオプション・引数の後置に強い。
# 4. set -e を使わない — 途中の非ゼロ終了で拒否JSONを出す前に落ちる事故を防ぐ。
# 5. 実行されない本文は見ない — ヒアドキュメントの本文は「データ」であって実行されない。
# cat / tee / python3 等へ渡す本文まで走査すると、危険語を含む
# ファイルやメモを書くだけで作業が止まる(2026-08-25 に4件実測)。
# 本文が bash / sh / zsh 等に渡る形(実行される)は従来どおり走査する。
#
# 入力: stdin に PreToolUse の JSON
# 出力: 拒否時のみ stdout に decision JSON(常に exit 0)
#
# テスト: bash tools/test_hook.sh (変更したら必ず流すこと)
set -uo pipefail
# --- 拒否JSONの出力(外部コマンド不要。printf は bash 組み込み) ---
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
printf '%s' "$s"
}
deny() {
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' \
"$(json_escape "$1")"
exit 0
}
# --- 中身が空白だけかを判定する ---
# ${var//[空白]/} は使わない。macOS 標準の bash 3.2 では入力が数KBを超えると急激に遅くなり
# (5KBで6秒・20KBで321秒を実測・2026-08-25)、フックのタイムアウトを超えて
# 安全柵そのものが素通りする。パターンマッチなら大きさに関係なく一定時間で終わる。
blank() { case "$1" in *[![:space:]]*) return 1 ;; esac; return 0; }
# --- 標準入力を読む(cat に依存しない) ---
input=""
while IFS= read -r _line || [ -n "$_line" ]; do
input="${input}${_line}"$'\n'
done
blank "$input" && exit 0
# --- 必要な道具の存在確認(無ければ判定不能なので拒否する=フェイルクローズ) ---
for _t in awk grep sed tr; do
if ! command -v "$_t" >/dev/null 2>&1; then
deny "安全柵が動作に必要な「${_t}」を見つけられないため、念のためブロックしました。Windowsの方は Git for Windows が入っているかご確認ください。解決しない場合は配布元にご連絡ください"
fi
done
# --- JSONエスケープを戻す ---
# 順序が重要。先に \\ を退避しないと、Windowsパス C:\\tools\\rm.exe の \t が
# タブとして誤変換され、コマンド名が壊れて検出漏れになる。
# 改行は __CCNL__ という目印に置き換える。後段の awk が「行」を復元して
# ヒアドキュメントの範囲を判定し、最後に ; へ畳む。空白に潰すと前後の行が1つの
# コマンドとして繋がり、誤検知と検出漏れの両方が起きる。
unescape() {
sed -e 's/\\\\/__BSLASH__/g' \
-e 's/\\n/__CCNL__/g' -e 's/\\r/__CCNL__/g' \
-e 's/\\u000[aAdD]/__CCNL__/g' \
-e 's/\\t/ /g' \
-e 's/\\"/"/g' \
-e 's|\\/|/|g' \
-e 's/__BSLASH__/\\/g'
}
# --- stdin JSON から .tool_input.command を取り出す ---
extract_command() {
awk '
found { next }
{
idx = index($0, "\"command\"")
if (idx == 0) next
rest = substr($0, idx + 9)
sub(/^[ \t]*:[ \t]*/, "", rest)
if (substr(rest, 1, 1) != "\"") next
rest = substr(rest, 2)
out = ""
i = 1; n = length(rest)
while (i <= n) {
c = substr(rest, i, 1)
if (c == "\\") { out = out c substr(rest, i + 1, 1); i += 2; continue }
if (c == "\"") break
out = out c
i++
}
print out
found = 1
}
END { if (!found) exit 3 }
' 2>/dev/null
}
# ============================================================
# ヒアドキュメント本文の除去
# ============================================================
# 目的: 実行されない本文(cat > file <<EOF の中身など)を走査対象から外す。
# 「文章に書いた危険語」で作業が止まるのを根本から止めるため。
#
# 落とす条件は2つとも満たすときだけ(どちらか欠けたら落とさない=フェイルクローズ):
# ア. その行のコマンドが全て「本文を実行しない受け手」(cat/tee/python3/node/git 等)
# イ. 終端記号が見つかっている(見つからない=解釈できていないので落とさない)
# bash / sh / zsh / ssh / 未知のスクリプトへ渡る本文は、従来どおり走査対象に残す。
strip_heredoc() {
awk '
function bname(t) {
gsub(/\\/, "/", t)
sub(/^.*\//, "", t)
sub(/\.exe$/, "", t)
return tolower(t)
}
# セグメントの先頭語(環境変数代入・修飾子・リダイレクト記号は読み飛ばす)
function seg_head(seg, nt, T, k, tok, b) {
nt = split(seg, T, /[ \t]+/)
for (k = 1; k <= nt; k++) {
tok = T[k]
if (tok == "") continue
if (tok ~ /^[A-Za-z_][A-Za-z0-9_]*=/) continue
if (tok ~ /^[0-9]*[<>]/) continue
if (tok ~ /^-/) continue
b = bname(tok)
if (b == "" || (b in MODIFIER)) continue
return b
}
return ""
}
# 行のコマンドが全て「本文を実行しない受け手」なら 1
function data_only(line, i, n, c, q, cnt, s, head, ok) {
split("", SEGST, " "); split("", SEGEN, " ")
cnt = 1; SEGST[1] = 1
i = 1; n = length(line); q = ""
while (i <= n) {
c = substr(line, i, 1)
if (q != "") {
if (c == "\\" && q == DQ) { i += 2; continue }
if (c == q) q = ""
i++; continue
}
if (c == SQ || c == DQ) { q = c; i++; continue }
if (index(";&|()", c) > 0) { SEGEN[cnt] = i - 1; cnt++; SEGST[cnt] = i + 1; i++; continue }
i++
}
SEGEN[cnt] = n
ok = 0
for (s = 1; s <= cnt; s++) {
if (SEGEN[s] < SEGST[s]) continue
head = seg_head(substr(line, SEGST[s], SEGEN[s] - SEGST[s] + 1))
if (head == "") continue
if (!(head in SINK)) return 0
ok = 1
}
return ok
}
# 行に含まれるヒアドキュメント開始を順に拾う。<<< (ヒアストリング)は本文を持たないので対象外。
# 引用符の中の << は演算子ではないので見ない。
function scan_ops(line, i, n, c, q, j, k, dash, delim, qd, cnt) {
split("", OPS, " ")
cnt = 0; i = 1; n = length(line); q = ""
while (i <= n) {
c = substr(line, i, 1)
if (q != "") {
if (c == "\\" && q == DQ) { i += 2; continue }
if (c == q) q = ""
i++; continue
}
if (c == SQ || c == DQ) { q = c; i++; continue }
if (c == "\\") { i += 2; continue }
if (substr(line, i, 2) == "<<") {
if (substr(line, i, 3) == "<<<") { i += 3; continue }
j = i + 2; dash = 0
if (substr(line, j, 1) == "-") { dash = 1; j++ }
while (substr(line, j, 1) == " " || substr(line, j, 1) == "\t") j++
qd = substr(line, j, 1)
if (qd == SQ || qd == DQ) {
j++; k = j
while (k <= n && substr(line, k, 1) != qd) k++
delim = substr(line, j, k - j)
j = k + 1
} else {
k = j
while (k <= n && index(" \t;&|<>()" DQ SQ, substr(line, k, 1)) == 0) k++
delim = substr(line, j, k - j)
gsub(/\\/, "", delim)
j = k
}
# 算術左シフト $((1 << 3)) 等を誤ってヒアドキュメントと見ないよう、
# 終端記号として妥当な形のときだけ採用する
if (delim ~ /^[A-Za-z_][A-Za-z0-9_]*$/) { cnt++; OPS[cnt] = dash US delim }
i = j; continue
}
i++
}
return cnt
}
BEGIN {
SQ = sprintf("%c", 39)
DQ = sprintf("%c", 34)
US = sprintf("%c", 31)
NL = 0
# 本文を「データ」として受け取る側。ここに載っているものだけ本文を落とす。
# シェル(bash/sh/zsh/ksh…)・ssh・docker・xargs・未知のスクリプトは意図的に載せない。
nn = split("cat tee diff comm sort uniq wc head tail cut paste column tr rev fold expand " \
"base64 md5 shasum sha1sum sha256sum grep egrep fgrep rg ag sed awk gawk mawk nawk " \
"jq yq less more python python2 python3 node deno ruby perl php git " \
"mail mailx sendmail msmtp ffmpeg pandoc sqlite3 psql mysql patch pbcopy", A, " ")
for (ii = 1; ii <= nn; ii++) SINK[A[ii]] = 1
mm = split("command builtin nohup time env exec", B, " ")
for (ii = 1; ii <= mm; ii++) MODIFIER[B[ii]] = 1
}
{
cn = split($0, T, "__CCNL__")
for (ti = 1; ti <= cn; ti++) { NL++; LN[NL] = T[ti] }
}
END {
no = 0; i = 1
while (i <= NL) {
line = LN[i]
no++; OUT[no] = line
i++
nops = scan_ops(line)
if (nops == 0) continue
keep = data_only(line) ? 0 : 1
for (k = 1; k <= nops; k++) {
split(OPS[k], P, US)
dash = (P[1] + 0); delim = P[2]
bn = 0; found = 0; j = i
while (j <= NL) {
t = LN[j]
# <<- はタブ字下げを無視する形。JSONエスケープ復元でタブは空白になっているので、
# 空白・タブのどちらの字下げでも終端と認める
if (dash) sub(/^[ \t]+/, "", t)
if (t == delim) { found = 1; j++; break }
bn++; BODY[bn] = LN[j]
j++
}
if (!(found && !keep)) {
for (m = 1; m <= bn; m++) { no++; OUT[no] = BODY[m] }
}
i = j
}
}
for (k = 1; k <= no; k++) printf "%s%s", (k == 1 ? "" : " ; "), OUT[k]
printf "\n"
}
' 2>/dev/null
}
raw="$(printf '%s' "$input" | extract_command | unescape)"
parse_rc=$?
cmd="$(printf '%s' "$raw" | strip_heredoc)"
# 本文除去そのものが失敗したら(awk の実装差など)、除去前の全文で判定する=フェイルクローズ。
# ここで空のまま進むと、安全柵が黙って素通りする側に倒れる
blank "$cmd" && ! blank "$raw" && cmd="$raw"
if [ $parse_rc -ne 0 ] || blank "$cmd"; then
# 解析できなかった場合は素通りさせず、生の入力を走査対象にする。
# JSONの記号を空白に潰して単語境界を復元しないと、"command":"sudo ...
# のように引用符が直前に来る形で各ルールの境界判定が外れる。
raw="$(printf '%s' "$input" | unescape | tr '":,{}[]' ' ')"
cmd="$(printf '%s' "$raw" | strip_heredoc)"
blank "$cmd" && ! blank "$raw" && cmd="$raw"
blank "$cmd" && exit 0
fi
lc="$(printf '%s' "$cmd" | tr '[:upper:]' '[:lower:]')"
has() { printf '%s' "$lc" | grep -Eq "$1"; }
hasC() { printf '%s' "$cmd" | grep -Eq "$1"; }
# ============================================================
# 単語分解による判定(位置に依存しない中核ロジック)
# ============================================================
# 原文(大文字小文字を保持)を渡す。git branch -D は大文字のDが安全か危険かを分けるため。
tokenscan() {
printf '%s' "$cmd" | awk '
function is_long(t) { return substr(t, 1, 2) == "--" }
function is_short(t) { return substr(t, 1, 1) == "-" && !is_long(t) }
function short_has(t, ch) { return is_short(t) && index(tolower(substr(t, 2)), ch) > 0 }
function base(t) {
gsub(/\\/, "/", t)
sub(/^.*\//, "", t)
sub(/\.exe$/, "", t)
return tolower(t)
}
{
line = $0
gsub(/[;&|()<>`]/, " __SEP__ ", line)
n = split(line, t, /[ \t]+/)
i = 1
while (i <= n) {
tk = t[i]
if (tk == "" || tk == "__SEP__") { i++; continue }
# 環境変数代入・単なる修飾子は読み飛ばして、次を本体コマンドとして扱う
if (tk ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { i++; continue }
b = base(tk)
if (b == "command" || b == "builtin" || b == "nohup" || b == "time" || b == "env") { i++; continue }
# ---- rm: 再帰 + 強制 が揃ったら危険 ----
if (b == "rm") {
rec = 0; frc = 0; i++
while (i <= n && t[i] != "__SEP__") {
a = t[i]
if (substr(a, 1, 1) == "-") {
if (a == "--recursive" || a == "--dir" || short_has(a, "r")) rec = 1
if (a == "--force" || short_has(a, "f")) frc = 1
if (rec && frc) { print "RM"; exit }
}
i++
}
continue
}
# ---- Windows: rd / rmdir /s, del /s ----
if (b == "rd" || b == "rmdir" || b == "del" || b == "erase") {
sflag = 0; i++
while (i <= n && t[i] != "__SEP__") {
if (tolower(t[i]) == "/s") sflag = 1
if (sflag) { print "WINDEL"; exit }
i++
}
continue
}
# ---- git: サブコマンドごとに、区切りまでの全引数を見る ----
if (b == "git") {
i++
while (i <= n && t[i] != "__SEP__" && substr(t[i], 1, 1) == "-") {
if (t[i] == "-C" || t[i] == "-c") i += 2; else i++
}
sc = (i <= n && t[i] != "__SEP__") ? tolower(t[i]) : ""
args = ""; j = i + 1
while (j <= n && t[j] != "__SEP__") { args = args " " t[j]; j++ }
args = args " "
largs = tolower(args)
if (sc == "push") {
if (largs ~ / --force / && largs !~ /--force-with-lease/) { print "GITPUSH"; exit }
if (largs ~ / -[a-z]*f[a-z]* /) { print "GITPUSH"; exit }
if (args ~ / \+[^ ]+:/ ) { print "GITPUSH"; exit }
}
if (sc == "reset" && largs ~ / --hard /) { print "GITRESET"; exit }
if (sc == "clean") {
if (largs ~ / --force / || largs ~ / -[a-z]*f[a-z]* /) { print "GITCLEAN"; exit }
}
if (sc == "branch") {
if (args ~ / -[a-zA-Z]*D[a-zA-Z]* /) { print "GITBRANCH"; exit }
if (largs ~ / --delete / && largs ~ / --force /) { print "GITBRANCH"; exit }
}
if (sc == "commit" && (largs ~ /--no-verify/ || largs ~ /--no-gpg-sign/)) { print "GITCOMMIT"; exit }
continue
}
i++
}
}
' 2>/dev/null
}
TOK="$(tokenscan)"
# 環境に trash があれば、復元可能な代替として案内に含める
if command -v trash >/dev/null 2>&1; then
RM_ALT="代替: 新しい作業フォルダは mktemp -d / 既存物の削除は trash <パス>(復元可能) / zipのキャッシュ除去は -x の除外指定"
else
RM_ALT="代替: 新しい作業フォルダは mktemp -d、既存ファイルは1つずつ確認しながら削除。Windowsではエクスプローラーからゴミ箱へ移動してください"
fi
case "$TOK" in
RM)
deny "フォルダの再帰的な強制削除はブロックしました(安全柵)。同じコマンドの再送はしないでください。${RM_ALT}" ;;
WINDEL)
deny "フォルダの再帰削除(rd /s、del /s)はブロックしました(安全柵)。エクスプローラーからゴミ箱へ移動してください" ;;
GITPUSH)
deny "git push --force はブロックしました(安全柵)。共有先の作業履歴が失われます。--force-with-lease を使ってください" ;;
GITRESET)
deny "git reset --hard はブロックしました(安全柵)。未コミットの変更が消えます" ;;
GITCLEAN)
deny "git clean -f 系はブロックしました(安全柵)。追跡外のファイルが消えます" ;;
GITBRANCH)
deny "git branch -D(強制削除)はブロックしました(安全柵)。統合済みのみ削除する -d を使ってください" ;;
GITCOMMIT)
deny "git commit --no-verify / --no-gpg-sign はブロックしました(安全柵)。チェック機構はスキップしないでください" ;;
esac
# ============================================================
# 文字列パターンによる判定(単語分解に馴染まないもの)
# ============================================================
# PowerShell の再帰強制削除
if has 'remove-item[^;|&]*-recurse' && has 'remove-item[^;|&]*-force'; then
deny "Remove-Item -Recurse -Force はブロックしました(安全柵)"
fi
# 管理者権限昇格
if has '(^|[[:space:]]|;|&|\||\()(sudo|doas|pkexec)([[:space:]]|$)'; then
deny "管理者権限での実行(sudo)はブロックしました(安全柵)。必要な場合は内容を確認したうえでご自身で実行してください"
fi
# ディスク破壊
if has '(^|[[:space:]]|;|&|\|)(dd|mkfs(\.[a-z0-9]+)?)[[:space:]]'; then
deny "dd / mkfs はブロックしました(安全柵)。ディスクを破壊する可能性があります"
fi
if has '(^|[[:space:]]|;|&|\|)(format|diskpart|format-volume|clear-disk)([[:space:]]|$)' \
|| has 'cipher[[:space:]]+/w'; then
deny "ディスクの初期化・消去コマンドはブロックしました(安全柵)"
fi
# システム停止
if has '(^|[[:space:]]|;|&|\|)(shutdown|reboot|halt|poweroff|stop-computer|restart-computer)([[:space:]]|$)'; then
deny "システムの停止・再起動はブロックしました(安全柵)"
fi
# ネット経由スクリプトの即時実行
if has '(curl|wget)[[:space:]][^;|&]*\|[[:space:]]*(sudo[[:space:]]+)?(sh|bash|zsh|ksh)([[:space:]]|$)'; then
deny "ダウンロードしたスクリプトをそのままシェルに流す実行はブロックしました(安全柵)。中身を確認してから実行してください"
fi
if has '(irm|invoke-restmethod|iwr|invoke-webrequest)[[:space:]][^;|&]*\|[[:space:]]*(iex|invoke-expression)'; then
deny "ネットから取得したスクリプトの即時実行(irm | iex)はブロックしました(安全柵)"
fi
# 公開操作(取り消せない)
if has '(^|[[:space:]]|;|&|\|)(npm|pnpm|yarn)[[:space:]]+publish([[:space:]]|$)'; then
deny "npm/pnpm/yarn publish はブロックしました(安全柵)。公開は取り消せません"
fi
# 権限の全開放
if has 'chmod[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+777'; then
deny "chmod -R 777 はブロックしました(安全柵)"
fi
# docker 破壊系
if has 'docker[[:space:]]+(system[[:space:]]+prune|volume[[:space:]]+rm|volume[[:space:]]+prune)'; then
deny "docker の破壊的な prune / volume rm はブロックしました(安全柵)"
fi
# fork bomb
if hasC ':\(\)[[:space:]]*\{[^}]*\|[[:space:]]*:[[:space:]]*&'; then
deny "fork bomb を検出したためブロックしました(安全柵)"
fi
# 生ディスク書き込み
if has '>[[:space:]]*/dev/(disk|sd|nvme|hd)'; then
deny "生ディスクへの書き込みはブロックしました(安全柵)"
fi
exit 0長いので枠の中でスクロールします。全文は右上の「コピー」で丸ごとクリップボードに入ります。
手で作った場合は実行権限をつけます。
chmod +x ~/.claude/hooks/block-dangerous-bash.sh追加のインストールは不要です。以前のひな形は jq というツールに依存していましたが、なくても動くように作り替えました。Windowsの Git Bash には jq が同梱されておらず、そこで安全柵が丸ごと無効になっていたためです。
保存するときは改行コードをLFにしてください。Windowsのエディタで開いて保存し直すとCRLFに変わることがあり、そうなるとスクリプトが構文エラーで動きません(①の受け皿が拒否側に倒すので、素通りにはなりません)。
Claude Code を再起動してから、次の2つを頼みます。どちらも存在しないパスなので、万一実行されても何も消えません。
①「~/存在しないフォルダ_test を rm -rf で消して」
②「rm -rfv ~/存在しないフォルダ_test を実行して」
②が今回ふさいだ穴です。2つとも「ブロックしました(安全柵)」と出れば成功。①だけ止まって②が実行された場合は、hookの貼り替えができていません(古いままになっています)。
実行されてしまう場合は、hookのパス・実行権限(chmod +x)・Claude Code を再起動したか・settings.json のJSONが壊れていないか、を順に確認します。
cd foo && rm -rf bar のように途中に混ざった形は素通りしうる。それをコマンド文字列全体の検査で拾うのが hook の役目。片方だけでは穴が残るので両方入れるgrep の検索語や echo の文字列にたまたま該当パターンが入っただけでも止まる。止まったら書き方を変えるCLAUDE.md に貼る)ここを ~/.claude/CLAUDE.md にそのまま貼ります。「本番リスト」だけ自分の環境に置き換えてください。
## バイパスモード運用ルール
確認プロンプトが出ないモードで動いている。破壊系コマンドはシステム層(hook + permissions.deny)で
強制ブロック済み。以下はhookで拾えない領域なので必ず守る。
大原則: 取り消せる操作は自律で進める。取り消せない操作は必ず一度止まって確認する。
1. `.env` / APIキー / 認証情報をコミット・外部送信しない
2. `--no-verify` `--force` `--no-gpg-sign` など安全機構のスキップは禁止(明示指示でも再確認する)
3. 本番環境への変更は事前確認(下の「本番リスト」参照)
4. 顧客データ・記録の削除やアーカイブは事前確認(Notion DB / スプレッドシート / 顧客管理ツール)
5. 10ファイルを超える一括削除は事前確認
6. 未コミット変更を上書きしない(`git stash drop` / `git checkout --` で消さない)
7. 外部への発信(公式LINE配信 / SNS投稿 / メール送信 / 決済)は、送信直前に
「宛先・件数・本文」を提示して承認を取る。「進めて」だけを送信承認とみなさない
8. 10分を超えるタスクは、開始時と完了時に報告する
### 本番リスト(これを触るときは必ず止まる)
- (記入)例: 会員サイト https://〜 のコンテンツ・会員データ
- (記入)例: 販売中LPのページ
- (記入)例: 顧客管理のNotion DB「〇〇」
- (記入)例: 公式LINEの配信(テスト送信を除く)| ルール | なぜ必要か |
|---|---|
| 1. 認証情報 | 一度公開リポジトリに載ると、消してもコミット履歴に残る。取り消せない |
| 2. 安全機構スキップ | --force 系は「事故を防ぐ仕組みを外す」操作。外す判断は人間が握る |
| 3. 本番 | 壊れると売上と信用に直結する。ローカルのやり直しとは損害の桁が違う |
| 4. 顧客データ | 削除は取り消せない。アーカイブは戻せてもリンクや自動連携は戻らない |
| 5. 大量削除 | 1〜2ファイルの消し間違いは復旧できる。50ファイルは復旧できない |
| 6. 未コミット変更 | まだ保存されていない作業=この世に1つしかないデータ。消えたら終わり |
| 7. 外部発信 | 送ったものは取り消せない。バイパスモードで唯一「常に人が承認する」領域 |
| 8. 長時間タスク | 黙って10分動かれると、暴走か進行か判断できない |
いきなり本番業務で使わない。危険度の低いものから順に上げて、誤ブロックとAIの脱線を観察します。
| 段階 | 試すこと | 見るポイント |
|---|---|---|
| 1. 読み取り | ファイルを読む/一覧を出す/Notion検索 | 普通の作業が誤ブロックされないか |
| 2. ローカル編集 | ファイル作成・編集、スクリプト実行 | 摩擦ゼロを体感できるか。余計な編集をしないか |
| 3. git | add / commit / 通常の push | コミットは通り、force が要る場面で止まるか |
| 4. 外部ツール(本番以外) | テスト用Notionページの更新、下書き作成 | 自分から動けているか |
| 5. 本番系 | 会員サイト・LP・本番DBに関わる更新 | 事前確認ルールを守るか(ここが本試験) |
| 6. 外部発信 | LINE配信・SNS投稿 | 承認を取ってから送るか |
1〜3は数セッションで素通りするはず。4以降を本気で観察する。5で確認なしに本番を触ったら、その時点でルールの書き方を直します。
| 症状 | 対応 |
|---|---|
| 日常作業で誤ブロックされる | 該当パターンを緩める/deny から削る |
| 「これは止めてほしかった」が素通りした | hookにパターンを追加する |
| 同じコマンドが毎回すり抜ける | hookのロジックを見直す |
閾値:同じパターンで誤ブロックが2回以上出たら即修正(1回は偶発の可能性)。
| 症状 | 対応 |
|---|---|
| 事前確認せずに本番を触った | ルールを具体化する(曖昧さが原因) |
| 「10ファイル超」で毎回止まるが実害がない | 閾値を上げる(20/50)か撤廃する |
| ルールに書いていない領域で事故った | ルールを足す |
| ルールは守られているのに摩擦を感じる | その項目の撤廃を検討する |
閾値:同じルール違反が2回起きたら、AIではなくルールの書き方が悪い(抽象的すぎる/長すぎる)と考える。
黄金律:ルールは増やすより減らす
「このルールは、過去1ヶ月で1度でも自分を救ったか?」で判断します。救っていなければ削る。一度も発動しないルールは認知負荷を増やすだけで、やがて守られなくなります。ルールが長いほど、AIの中での優先度も薄まります。
そして事故ログを1つ持っておくこと。事故った日・誤ブロックされた日を追記していくと、「どのルールが実際に効いたか」があとで判断できます。
settings.json/hookスクリプト → 再起動で反映(今動いているセッションには反映されないことがある)CLAUDE.md → 次のセッションから反映(今のセッションは起動時に読み込み済み)「直したのに効かない」の9割はこれ。まず再起動する。
| 症状 | 原因 |
|---|---|
| hookが全く効かない | 実行権限がない(chmod +x)/パスの書き間違い/Claude Code を再起動していない |
| 一部のコマンドだけ素通りする | hookが古い(2026-10-10より前のもの)。第1部の②のコードで丸ごと上書きする |
| 貼り替えたのに構文エラーで動かない | 改行コードがCRLFになっている。エディタでLFに直して保存する |
| 起動しなくなった | settings.json のJSONが壊れている(カンマ・括弧)。エディタで開くとすぐ分かる |
| 毎回ブロックされて作業にならない | grep や echo の文字列にパターンが入っている。書き方を変える |
| 起動のたびの警告確認が煩わしい | 慣れたら "skipDangerousModePermissionPrompt": true を追加。柵を立て終わってから |
--dangerously-skip-permissions または settings.json の bypassPermissionsPreToolUse は「実行される直前」に割り込むものrm -rfv rm -r -f git push origin main --force git reset HEAD~1 --hard git branch feature -D などが素通りしていた。判定を「書き順を探す」方式から「コマンドを単語に分解して危険なオプションの組み合わせを見る」方式へ変更"timeout": 10 を削除(重い処理の最中に安全柵のほうが先に力尽きて無効になるため)jq を不要化。Windowsの Git Bash には jq が同梱されておらず、そこで安全柵が丸ごと無効になっていたバイパスモードの本質は「AIを信用するかどうか」ではなく、取り返しがつく操作と、つかない操作を先に仕分けておくことです。
取り返しがつく操作は全部任せてスピードを取る。取り返しがつかない操作だけ、機械の柵と人の承認を通す。この線引きさえ作っておけば、確認プロンプトを消しても運用は壊れません。