0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのフック5個で「うっかり事故」を本気で潰した話

0
Posted at

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個

  1. stdin から JSON 取り損ねた — 最初は $1 で引数を読もうとしてた。違う、フックは stdin に JSON が来る。これに30分溶かした。
  2. 正規表現で git push を全部弾いてしまった — 仕事ができない状態になる。-f--force を含む場合だけに絞った。
  3. block しても Claude が再試行するdecision: block を返したのに、次のターンで形を変えて出してきた。reason をしっかり書いて、Claude 側に「なぜダメか」が伝わるようにしたら止まった。
  4. PostToolUse の lint で勝手にファイルが書き換わる — Claude が混乱する。lint は別ターミナルで動かして、フックでは検知だけにした。
  5. Stop フックの通知音が深夜に鳴る — 家族に怒られた。date +%H で時間帯チェックを入れた。これ、めっちゃ詰まった話というより、めっちゃ怒られた話。
  6. jq が無い環境で全部死ぬ — 別マシンに移ったとき、フックがサイレントに通っちゃってた。怖い。brew install jq を README に書いた。
  7. フックの 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を回す

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?