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を頼んだのに止まらないときの切り分け
- Q/hooksでPreToolUseのhookが登録されているか?答えがいいえのとき答えがはい
settings.jsonの場所と、JSONの構文を見直します。 - Qスクリプトに実行権限があるか?答えがいいえのとき答えがはい
chmod +x .claude/hooks/gcloud-prod-guard.shを実行します。onFailureが未設定だと、起動に失敗してもコマンドは通ります。 - 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を開発用に固定し、本番の認証を持ち込まない構成にすれば、取り違えそのものが起きにくくなります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。