Claude Cowork を社内AXに使っている私の実践ログです。社内固有名・個人名は伏せています。
rm -rf / を Claude が機嫌よく投げてきた瞬間、心臓が止まりかけた。
寸前で止めたから無事だったんですが、あれ以来、私は「Claude を信じきらない仕組み」を本気で作っている。結論から言うと、Claude Code のフックを5個ちゃんと書くだけで、9割の事故は止まる。今日はその5個を全部置きます。コピペでいい。
なぜプロンプトでお願いするのは無駄なのか
Claude Code は賢い。ただ、賢いというのは「私の意図を読んでくれる」じゃなくて「もっともらしいコマンドを生成する」だけ。意図と一致してるかは、別問題。
私はこれを3回くらいやられて学習しました。
- main に直 push しかけた
-
.envを読み出してログに転記しようとした -
npm install <よく分からんパッケージ>を実行寸前まで行った
「main にpushしないでね」と CLAUDE.md に書く——あれ、効きません。正確には、5回に1回は突破される。同じチームの先輩は「5回に1回はガチャ」って言ってた。私もそう思う。
私の判断:文章でお願いするのをやめてフックに任せた
これは賛否あると思いますが、私の結論はこう。
「お願い」は人間相手にする行為で、ツールに対してはガードを置くのが筋。
フックは「Claude が手を動かす直前」に割り込めるレイヤー。プロンプトで懇願するのとは、立ち位置がそもそも違う。物理的な壁です。
うん、書いてみると当たり前なんですが、最初の数週間は「丁寧にお願いすれば大丈夫だろう」と本気で思ってた。自分でも何やってんだろうって今では思います。
コピペで使える設定 5個
.claude/settings.local.json にこれを置く:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }
]
},
{
"matcher": "Read|Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/guard-secrets.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/lint-after.sh" }
]
}
],
"Stop": [
{ "hooks": [ { "type": "command", "command": ".claude/hooks/notify.sh" } ] }
]
}
}
肝心の guard-bash.sh がこれ。短い。
#!/usr/bin/env bash
set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')
deny='(rm[[:space:]]+-rf[[:space:]]+/|git[[:space:]]+push[[:space:]]+(-f|--force).*main|sudo[[:space:]]+rm|chmod[[:space:]]+777|curl[^|]*\|[[:space:]]*sh)'
if echo "$cmd" | grep -Eq "$deny"; then
printf '{"decision":"block","reason":"危険コマンドを検知: %s"}\n' "$cmd"
fi
exit 0
guard-secrets.sh はもっと短い。これがまた地味に効く。
#!/usr/bin/env bash
path=$(jq -r '.tool_input.file_path // .tool_input.path // ""')
if echo "$path" | grep -Eq '\.env$|\.pem$|secrets/|credentials'; then
echo '{"decision":"block","reason":"機密ファイルへのアクセスは人間に確認させて"}'
fi
exit 0
仕上げに notify.sh で Stop フック。地味だけど超大事。
#!/usr/bin/env bash
hour=$(date +%H)
if [ "$hour" -ge 9 ] && [ "$hour" -lt 22 ]; then
osascript -e 'display notification "Claude が手を止めました" with title "Claude Code"'
fi
exit 0
ハマったところ7個
-
stdin から JSON 取り損ねた — 最初は
$1で引数を読もうとしてた。違う、フックは stdin に JSON が来る。これに30分溶かした。 -
正規表現で
git pushを全部弾いてしまった — 仕事ができない状態になる。-fか--forceを含む場合だけに絞った。 -
block しても Claude が再試行する —
decision: blockを返したのに、次のターンで形を変えて出してきた。reason をしっかり書いて、Claude 側に「なぜダメか」が伝わるようにしたら止まった。 - PostToolUse の lint で勝手にファイルが書き換わる — Claude が混乱する。lint は別ターミナルで動かして、フックでは検知だけにした。
-
Stop フックの通知音が深夜に鳴る — 家族に怒られた。
date +%Hで時間帯チェックを入れた。これ、めっちゃ詰まった話というより、めっちゃ怒られた話。 -
jqが無い環境で全部死ぬ — 別マシンに移ったとき、フックがサイレントに通っちゃってた。怖い。brew install jqを README に書いた。 -
フックの exit code を間違えた — 1 を返すと Claude 側にうまく伝わらない。0 で
decision:blockの JSON を返すのが正解。これ、ドキュメント読み直すまで気付けなかった。
真似できるチェックリスト
導入する前に、これだけは確認してほしい。
-
.claude/hooks/以下のスクリプトに実行権限がある (chmod +x) - フック内で stdin から JSON を読んでいる
-
block するときは
{"decision":"block","reason":"..."}を返す - 正規表現は「弾きすぎ」を一旦受け入れて、運用で締めていく
-
jqを依存に入れていることを README に書く - 通知系フックには時間帯ガードを入れる(深夜に鳴るとマジで怒られる)
まとめ
- Claude を信じすぎないでフックで物理ガードを置く
- お願いベースの「main pushしないで」はガチャ。効かない前提で設計する
- 危険コマンドと秘密ファイルの検知は正規表現で十分。雑に始めて締めていく
- block するときは reason を書く。Claude にも伝わる
- 自作フックは地味に詰まる。jq、stdin 読み、exit 0 — この3点は早めに身体に入れる
ここまで偉そうに書いておいてなんですが、私もまだフック運用2か月で、粒度は試行錯誤中です。みなさんはどんなフック書いてますか? 「これも入れとけ」っていうのがあればコメントで教えてください。
Claude Cowork を社内AXの相棒として毎日使っているエンジニアの実践ログです。
シリーズ: Claude Cowork で社内AXを回す