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に全部任せたら本番DB吹き飛ばしかけた話 ― フック機能で身を守る安全運用ガイド

0
Posted at

結論:Claude Codeは便利だが「何でもできる状態」で使ってはいけない

深夜2時、Claude Codeが生成したマイグレーションスクリプトのDROP TABLEがステージングを超えて本番に向かおうとしていました。

この記事では、私が実際に体験しかけたインシデントの経緯と、Claude Codeのフック機能(PreToolUse / PostToolUse) を使って同じ事故を二度と起こさないための安全運用ガイドをお届けします。フック設定のテンプレートとチェックリスト付きです。


事件の経緯:なぜClaude Codeが本番DBに手を伸ばせる状態だったのか

油断の連鎖

原因は単純で、環境変数の管理不備Claude Codeへの過信が重なった結果でした。

  1. .envファイルに本番の接続情報が残っていた — ステージング検証後に切り戻し忘れ
  2. Claude Codeに「DBマイグレーションを実行して」と曖昧に指示した — 対象環境を明示しなかった
  3. Claude Codeはシェルコマンドを自由に実行できる状態だった — 権限制限なし

Claude Codeは指示に忠実です。.envを読み、接続先が本番であることに疑問を持たず、DROP TABLEを含むマイグレーションSQLを生成し、実行しようとしました。

幸い、実行直前にターミナルの出力で本番ホスト名に気づき、手動で停止できました。しかし、あと数秒遅ければ本番データが消えていました

根本原因

この事故の本質は「AIツールに無制限のシェルアクセスを与えていたこと」です。人間の開発者にも本番DBへの直接アクセス権限は制限するのに、AIツールには何のガードレールも設けていませんでした。


PreToolUse / PostToolUse フックの仕組み

Claude Codeには、ツール(コマンド)実行の前後にカスタムスクリプトを挟めるフック機能があります。これが今回の救世主になります。

ツール実行フローの全体像

フックの基本構造

フックは .claude/settings.json に定義します。各フックはシェルコマンドまたはスクリプトを実行し、JSON形式の標準出力で判定結果を返します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/pre-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/post-log.sh"
          }
        ]
      }
    ]
  }
}

重要なポイント:

項目 説明
matcher 対象ツール名。Bash, Write, Edit など。空文字で全ツール対象
フックの入力 標準入力にJSON形式でツール名・パラメータが渡される
フックの出力 {"decision": "allow"}, {"decision": "block", "reason": "..."}, {"decision": "ask", "message": "..."} のいずれか
出力なし 何も出力しなければ暗黙的に allow と同じ扱い

実装例:危険コマンドの自動ブロック・Slack通知・実行ログ

実装例1:危険コマンドの自動ブロック(PreToolUseフック)

#!/bin/bash
# pre-check.sh — 危険なコマンドを自動ブロック

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Bashツール以外はそのまま許可
if [ "$TOOL_NAME" != "Bash" ]; then
  exit 0
fi

# 危険パターンの定義
DANGEROUS_PATTERNS=(
  "DROP\s+(TABLE|DATABASE)"
  "TRUNCATE\s+TABLE"
  "DELETE\s+FROM\s+\S+\s*(;|$)"  # WHERE句なしのDELETE
  "rm\s+-rf\s+/"
  "mkfs\."
  ":(){ :|:& };:"
  "> /dev/sd"
  "dd\s+if=.+of=/dev/"
  "curl.*\|\s*(bash|sh)"
  "production|prod-db|本番"
)

for pattern in "${DANGEROUS_PATTERNS[@]}"; do
  if echo "$COMMAND" | grep -qiP "$pattern"; then
    echo "{\"decision\": \"block\", \"reason\": \"🚨 危険なコマンドを検出しました: パターン '$pattern' にマッチ。このコマンドは実行できません。\"}"
    exit 0
  fi
done

# 危険パターンに該当しなければ許可
exit 0

実装例2:Slack通知つき実行ログ(PostToolUseフック)

#!/bin/bash
# post-log.sh — 実行ログ記録 & Slack通知

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "N/A"')
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

LOG_DIR="$HOME/.claude/logs"
mkdir -p "$LOG_DIR"

# ログファイルに記録
echo "[$TIMESTAMP] tool=$TOOL_NAME command=$COMMAND" >> "$LOG_DIR/claude-code-audit.log"

# 特定キーワードを含む場合のみSlack通知
ALERT_PATTERNS="(DROP|DELETE|TRUNCATE|ALTER|migrate|deploy|rm\s+-rf)"

if echo "$COMMAND" | grep -qiP "$ALERT_PATTERNS"; then
  SLACK_WEBHOOK="${CLAUDE_SLACK_WEBHOOK_URL}"
  if [ -n "$SLACK_WEBHOOK" ]; then
    PAYLOAD=$(jq -n \
      --arg text "⚠️ *Claude Code Alert*\n• Tool: $TOOL_NAME\n• Command: \`$COMMAND\`\n• Time: $TIMESTAMP\n• User: $(whoami)@$(hostname)" \
      '{text: $text}')
    curl -s -X POST -H 'Content-type: application/json' \
      --data "$PAYLOAD" "$SLACK_WEBHOOK" > /dev/null 2>&1
  fi
fi

実装例3:特定ディレクトリへの書き込み防止

#!/bin/bash
# pre-write-check.sh — 保護対象ディレクトリへの書き込みをブロック

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ]; then
  exit 0
fi

# 保護対象パス
PROTECTED_PATHS=(
  "/etc/"
  "/usr/"
  "$HOME/.ssh/"
  "$HOME/.aws/"
  ".env.production"
  "docker-compose.prod"
)

for protected in "${PROTECTED_PATHS[@]}"; do
  if echo "$FILE_PATH" | grep -q "$protected"; then
    echo "{\"decision\": \"ask\", \"message\": \"⚠️ 保護対象パス '$protected' への書き込みです。本当に実行しますか?\"}"
    exit 0
  fi
done

exit 0

allowlist / denylist パターンで守る「最小権限の原則」

フックによる個別チェックに加え、Claude Codeのpermissions設定でallowlist / denylistを定義することで、多層防御を実現できます。

多層防御の設計思想

permissions設定の実例

.claude/settings.json にて、permissions で allowlist / denylist を定義します。

{
  "permissions": {
    "allow": [
      "Bash(git *)",
      "Bash(npm run lint*)",
      "Bash(npm run test*)",
      "Bash(npx tsc*)",
      "Bash(cat *)",
      "Bash(ls *)",
      "Bash(head *)",
      "Bash(tail *)",
      "Bash(grep *)",
      "Bash(find *)",
      "Read",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf /*)",
      "Bash(*DROP TABLE*)",
      "Bash(*DROP DATABASE*)",
      "Bash(sudo *)",
      "Bash(curl * | bash*)",
      "Bash(*prod*migrate*)",
      "Bash(docker rm -f *)",
      "Bash(kubectl delete *)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/pre-check.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/pre-write-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/post-log.sh"
          }
        ]
      }
    ]
  }
}

allowlist / denylist 設計のポイント

原則 実践
デフォルト拒否 allowlistに明示したコマンドのみ自動許可。それ以外はユーザー確認を挟む
最小権限 git, cat, ls など読み取り系・安全なコマンドのみ自動許可
破壊的操作は二重防御 denylistで即ブロック+フックでパターン検出の二重チェック
環境ごとに分離 プロジェクト単位の .claude/settings.json で設定を分けて管理

フック設定テンプレート公開と導入チェックリスト

プロジェクトのディレクトリ構成

your-project/
├── .claude/
│   ├── settings.json          # permissions + hooks 設定
│   ├── hooks/
│   │   ├── pre-check.sh       # 危険コマンドブロック
│   │   ├── pre-write-check.sh # 保護パス書き込みガード
│   │   └── post-log.sh        # ログ記録 & Slack通知
│   └── logs/
│       └── claude-code-audit.log
└── ...

セットアップ手順

# 1. ディレクトリ作成
mkdir -p .claude/hooks .claude/logs

# 2. フックスクリプトを配置(上記の実装例を保存)
# 3. 実行権限を付与
chmod +x .claude/hooks/*.sh

# 4. settings.json を配置(上記テンプレートを保存)
# 5. 動作確認 — わざと危険コマンドを試す
echo '{"tool_name":"Bash","tool_input":{"command":"DROP TABLE users;"}}' | .claude/hooks/pre-check.sh
# → {"decision": "block", "reason": "🚨 危険なコマンドを検出しました: ..."} が返ればOK

導入チェックリスト

導入前に必ず確認する項目:

  • .env.production がプロジェクトルートに存在しない(または .gitignore に含まれている)
  • settings.jsondenyDROP, TRUNCATE, rm -rf が含まれている
  • allow は読み取り系コマンドと開発用コマンドのみに限定している
  • PreToolUseフックが正しく block のJSONを返すことをテスト済み
  • PostToolUseフックのログ出力先にチームがアクセスできる
  • Slack Webhook URLが環境変数に設定済み(通知を使う場合)
  • フックスクリプトに実行権限(chmod +x)が付与されている
  • チームメンバー全員が同じ settings.json を使っている(Git管理推奨)

まとめ

  • Claude Codeは「何でもできる状態」で使わない。 permissions の allowlist/denylist と フック機能で多層防御を構築し、最小権限の原則を徹底しましょう
  • PreToolUseフックは最後の砦。 危険なコマンドパターンを検出して自動ブロックし、人間が確認する機会を確保できます
  • PostToolUseフックで監査証跡を残す。 すべてのツール実行をログに記録し、必要に応じてSlack通知を飛ばすことで、チーム全体の安全意識が高まります

AIツールは強力ですが、権限管理は人間の責任です。設定に30分かけるだけで、深夜2時の冷や汗を防げます。


参考リンク

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?