Claude Media
Claude Codeでgcloudの本番誤操作を防ぐ — configurationの罠とhook

Claude Codeでgcloudの本番誤操作を防ぐ — configurationの罠とhook

gcloudは対象プロジェクトをコマンドに書かなくても動きます。configurationの取り違えで本番にdeleteが届く事故を、deny rulesとPreToolUse hookで止める設定です。

gcloudのコマンドには、対象のプロジェクトを書かなくても動くものが多くあります。gcloud compute instances delete vm1だけで通り、どのプロジェクトのVMが消えるかは、実行時の設定が決めます。Claude Codeにgcloudを触らせるなら、この「暗黙の対象」が事故の入口です。

この記事では、configurationの取り違えで本番にdeleteが届く事故を、2段で止めます。1段目はdeny rulesで、Claudeが対象を切り替える操作そのものを塞ぎます。2段目はPreToolUse hookで、実行直前に解決先のプロジェクトを調べ、本番ならdeleteを拒否します。

gcloudは対象プロジェクトをどこで決めているか

事故を防ぐには、まず対象がどこで決まるかを知っておく必要があります。gcloudのドキュメントによると、設定の束はconfigurationと呼ばれ、同時に有効になるのは1つだけです。有効なconfigurationのプロパティが、gcloudの動作を決めます。

対象のプロジェクトを動かす経路は、少なくとも次の4つあります。

経路例効く範囲
active configuration例gcloud config configurations activate prod効く範囲以降のコマンドすべて
--configurationフラグ例gcloud ... --configuration=prod効く範囲そのコマンド1回
環境変数CLOUDSDK_ACTIVE_CONFIG_NAME例シェルに設定効く範囲そのターミナルのコマンドすべて
環境変数CLOUDSDK_CORE_PROJECT例プロパティを環境変数で指定効く範囲同上

プロパティの環境変数はgcloud config setで保存した値より優先される、と書かれています。つまり、画面でgcloud config listを見て安心していても、環境変数が別のプロジェクトを指していれば、実際の対象はそちらです。

取り違えは、たいてい次の形で起きます。

  • 午前に本番を調べるためactivateしたconfigurationが、午後もそのまま有効になっている
  • Claudeが別ターミナルの状態を知らず、既定の対象に向けてdeleteを打つ
  • --projectを省いたコマンドを、Claudeが「いつもの形」として生成する

コマンドの文字列だけ見ても、対象は分かりません。ここが、sfの--target-orgのように対象を引数で渡す道具との違いです。

1段目: 切り替えの入口をdenyで塞ぐ

Claudeに対象を勝手に切り替えさせないため、settings.jsonのdenyに次を入れます。

{
  "permissions": {
    "deny": [
      "Bash(gcloud config configurations activate *)",
      "Bash(gcloud config set *)",
      "Bash(gcloud config unset *)",
      "Bash(gcloud projects delete *)"
    ]
  }
}

permissionsはdeny、ask、allowの順に評価され、最初に当たったルールで決まります。denyに入れたgcloud config set *は、allowに具体的なルールを書いても覆りません。

denyが効くのは、Claudeが書いたコマンドの文字列に対してです。ドキュメントは、Bashのdenyやaskのルールがセキュリティの境界ではないと説明しています。Bash(rm *)が/bin/rmやbash -c 'rm ...'を止めない、という例と同じで、/usr/bin/gcloudの絶対パスやsh -c 'gcloud ...'には当たりません。

一方で、次の形は止まります。

  • &&や;でつないだ複合コマンドの中のgcloud(denyはどの部分コマンドに当たっても効く)
  • FOO=bar gcloud ...のように環境変数を前置した形(denyは前置の代入を越えて照合される)

2段目: PreToolUse hookで解決先を調べてから判定する

denyだけでは、すでに本番を向いたactive configurationの上で打たれるgcloud ... deleteが止まりません。そこで、delete系のgcloudが実行される直前に、解決先のプロジェクトを調べるhookを置きます。

hookはexit 0でJSONを返す形にします。PreToolUseのJSONはhookSpecificOutputの中にpermissionDecisionを持ち、denyでツール呼び出しを止め、askで確認を求めます。denyの理由はClaudeに見えるので、次の手を考えさせられます。

#!/bin/bash
# .claude/hooks/gcloud-prod-guard.sh
input=$(cat)
cmd=$(jq -r '.tool_input.command' <<<"$input")
 
# gcloud の delete 系でなければ素通し
grep -qE '(^|[;&|[:space:]])gcloud[[:space:]]' <<<"$cmd" || exit 0
grep -qE '[[:space:]]delete([[:space:]]|$)' <<<"$cmd" || exit 0
 
PROD_RE='-(prod|prd)$'   # 本番プロジェクトIDの命名規則に合わせる
 
pick() { grep -oE -- "$1[= ][^[:space:]]+" <<<"$cmd" \
  | head -1 | sed -E "s/^$1[= ]//"; }
emit() {
  jq -n --arg d "$1" --arg r "$2" '{hookSpecificOutput:{
    hookEventName:"PreToolUse",
    permissionDecision:$d, permissionDecisionReason:$r}}'
  exit 0
}
 
project=$(pick --project)
[ -z "$project" ] && project=$(grep -oE \
  'CLOUDSDK_CORE_PROJECT=[^[:space:]]+' <<<"$cmd" | head -1 | cut -d= -f2)
if [ -z "$project" ]; then
  conf=$(pick --configuration)
  if [ -n "$conf" ]; then
    list=$(gcloud config list --configuration="$conf" 2>/dev/null)
  else
    list=$(gcloud config list 2>/dev/null)
  fi
  project=$(awk -F' = ' '$1=="project"{print $2}' <<<"$list")
fi
 
if [ -z "$project" ]; then
  emit ask "対象プロジェクトを特定できません。確認してください"
fi
if [[ "$project" =~ $PROD_RE ]]; then
  emit deny "本番プロジェクト($project)への delete は禁止です"
fi
exit 0

判定の順序は、コマンドに明示された--project、前置されたCLOUDSDK_CORE_PROJECT、--configurationで指定されたconfiguration、有効なconfigurationの順です。どれでも特定できなければ、通さずにaskへ回します。「分からないときは止める」側に倒すためです。

PROD_REは例です。本番プロジェクトIDが-prodや-prdで終わる命名なら、そのまま使えます。命名に規則がない組織では、本番IDを列挙した配列との完全一致に書き換えます。

登録は.claude/settings.jsonに、次のように書きます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(gcloud *)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/gcloud-prod-guard.sh",
            "onFailure": "block"
          }
        ]
      }
    ]
  }
}

ifにはBashの権限ルール形式でBash(gcloud *)を書けます。複合コマンドやコマンド置換の中のgcloudにも当たるので、cd infra && gcloud ... deleteでもhookが走ります。ただしifは最善努力のフィルターで、硬い許可と拒否にはpermissionsを使うよう案内されています。そのため、1段目のdenyと組み合わせます。

onFailure: "block"も要点です。hookが失敗したり、スクリプトのパスを誤ってもexit 2以外は非ブロックのエラーとして扱われ、コマンドが通ってしまいます。タイムアウトも同じです。onFailureをblockにすると、hookが動かなかったときにコマンドが止まります。

実行して挙動を確かめる

hookは、標準入力にJSONを渡せば単体で試せます。--projectが明示されたdeleteは、gcloudを呼ばずに判定されます。

echo '{"tool_input":{"command":"gcloud compute instances delete vm1 --project=shop-prod"}}' \
  | .claude/hooks/gcloud-prod-guard.sh

このときpermissionDecisionがdenyのJSONが出力され、shop-prodを含む理由が付きます。--project shop-devに替えれば何も出力されず、そのまま通ります。

次に、--projectを省いた場合です。このときだけhookがgcloudを呼び、有効なconfigurationのプロジェクトを見ます。gcloud config listは有効なconfigurationのプロパティを表示するコマンドです。手元でactive configurationを本番に切り替えてから、Claudeにgcloud storage buckets deleteを頼むと、denyが返ります。

切り分け

deleteを頼んだのに止まらないときの切り分け

  1. Q/hooksでPreToolUseのhookが登録されているか?
    答えがいいえのとき

    settings.jsonの場所と、JSONの構文を見直します。

    答えがはい
  2. Qスクリプトに実行権限があるか?
    答えがいいえのとき

    chmod +x .claude/hooks/gcloud-prod-guard.shを実行します。onFailureが未設定だと、起動に失敗してもコマンドは通ります。

    答えがはい
  3. Qコマンドは`gcloud`で始まる形か?
    答えがいいえのとき

    sh -cで包んだ形や、別名の関数は、このhookの文字列照合に掛かりません。次の節の分担を見てください。

    答えがはい

hookは働いている

上の単体テストでdenyが出るなら、残る原因は照合の外にある呼び出し形です。

解決先を調べず、--projectを必須にする方法

hookでgcloudを呼び出すのが重いなら、方針を変えて「delete系には--projectを必ず付ける」と決める手もあります。解決先を調べる代わりに、明示がなければ止めます。

# project が空なら、config を読まずに止める
if [ -z "$project" ]; then
  emit deny "delete には --project を明示してください"
fi

この形なら、スクリプトはgcloud config listを呼びません。active configurationがどこを向いていても結果が変わらず、判定が読みやすくなります。Claudeは拒否の理由を読み、--projectを付けて打ち直します。付ける値が本番IDなら、もう一度denyが返ります。

ただし、このルールが掛かるのはClaudeが書くコマンドだけで、人が自分のターミナルで打つgcloudには効きません。CLAUDE.mdに「gcloudのdeleteには--projectを明示する」と一行書いておけば、拒否の往復は減ります。CLAUDE.mdはClaudeの振る舞いを寄せるだけで、許可の範囲は変えません。止めるのはあくまでhookとdenyです。

2つの方式は両立します。解決先を調べる方式を既定にして、gcloudが見つからない環境では--project必須に倒す、という使い分けもできます。

文字列の照合では防げない呼び出し

denyもhookも、Claudeが書いたコマンドの文字列を見ます。文字列に現れない経路は塞げません。

経路止まるか理由
bash -c 'gcloud ... delete ...'止まるかdenyは止まらない理由文字列の先頭がgcloudではない
/usr/bin/gcloud ... delete止まるかdenyは止まらない理由同上
スクリプトファイルの中のgcloud止まるかどちらも止まらない理由コマンドは./cleanup.shとしか見えない
gcloud ... deleteの--project省略止まるかhookが解決先を見る理由有効なconfigurationを読むため

hookのスクリプトはgrepでgcloudを探すので、bash -c '...'の中身も文字列としては含まれ、hook側では拾える場合があります。ただし、クォートの中身までは解釈しない単純な照合です。シェルスクリプト経由の呼び出しは、スクリプトの中身を読まない限り判定できません。

そのため、文字列の照合を境界にせず、実行環境そのものを分けるほうが確実です。ドキュメントも、コマンド文字列に依存しない強制にはsandboxingを使うよう案内しています。

  • Claudeを動かす端末では、有効なconfigurationを開発用の1つに固定する。gcloudのドキュメントは、ディレクトリごとにconfigurationを自動で切り替える手段として、direnvでCLOUDSDK_ACTIVE_CONFIG_NAMEを設定する方法を挙げている
  • 本番プロジェクトの認証は、Claudeを動かす端末に置かない。ログインは別の端末か別のユーザーで行う。この考え方は、Salesforce CLIで本番deployを止める設定の「本番に認証しない」と同じ
  • 書き込み範囲の制限は、sandboxのallowWriteとdenyWriteで別に掛ける

Google CloudをMCP経由で操作する構成は、権限の切り方が違います。その場合はgcloud MCPサーバーの追加手順を参照してください。

本番を向いたまま作業が続いたとき

hookとdenyを入れても、本番を向いたconfigurationで読み取り系のコマンドは通ります。gcloud ... listやdescribeは止まりません。これは意図した設計です。読み取りまで止めると、調査の作業が回らなくなります。

止めたい動詞がdelete以外にあるなら、スクリプトの正規表現を広げます。update、remove、IAMポリシーの変更などが候補です。広げるほど、本番での正当な保守作業も止まります。その場合は、人が自分の端末で実行する運用に寄せるのが現実的です。

askを返した場合は、Claude Codeが確認のプロンプトを出します。確認に答える人がいない無人実行では、askではなくdenyに倒しておくと、挙動が読めます。

まとめ

gcloudの事故は、コマンドに対象が書かれないことから起きます。そのため、文字列のルールだけでは足りません。切り替えの入口をdenyで塞ぎ、delete系の直前にhookで解決先を調べ、本番なら拒否する。この2段が、Claudeが書くコマンドの範囲で効く線引きです。

その外側にある呼び出し形は、環境の分離で受けます。Claudeを動かす端末のconfigurationを開発用に固定し、本番の認証を持ち込まない構成にすれば、取り違えそのものが起きにくくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. code.claude.com/docs/en/permissions
  3. cloud.google.com/sdk/docs/configurations
  4. cloud.google.com/sdk/docs/properties
この記事を共有:XはてブLinkedIn