Claude CodeでBigQueryのdry runを使い上限超のクエリをhookで止める
bq queryをdry runで見積もり、スキャン量が上限を超えたらPreToolUse hookがexit 2で止めます。スクリプトと設定、止めきれないケースをまとめました。
Claude CodeにBigQueryを触らせると、SELECT *を巨大なテーブルに投げて、オンデマンド課金のスキャン量が一気に膨らむことがあります。PreToolUse hookでbq queryの実行前にdry runを走らせ、見積もりが上限を超えたらexit 2で止めれば、課金が発生する前に防げます。
dry runはクエリの検証と処理バイト数の見積もりだけを返し、スロットを使わず課金もされません。この性質を、ツール実行前に割り込むhookと組み合わせるのが本記事の構成です。
接続そのものの手順はClaude CodeにBigQueryプラグインを入れて接続・分析するに、PreToolUseの入出力の仕様はPreToolUse hookでツール実行前に許可・拒否・改変するにあります。
dry runで何が分かり、何が分からないか
bq queryに--dry_runを付けると、クエリは実行されず、次の形の1行が返ります。
Query successfully validated. Assuming the tables are not modified,
running this query will process 10918 bytes of data.この「process N bytes」の値を、hookが上限と比べる材料にします。BigQueryのドキュメントでは、dry runは次の3点を提供するとされています。
- オンデマンドモードでの料金の見積もり
- クエリの検証
- キャパシティモードでの処理バイト数の概算
見積もりに頼れない場合もあります。外部データソースを使うフェデレーテッドクエリのdry runは、実際には行が返っても下限として0バイトを報告することがあります。行レベルセキュリティでマスクされたテーブルに対するdry runは、サイドチャネル攻撃を避けるため常に0バイトを返します。この2つでは、見積もりが0でも安全とは言えません。
dry runの性質
スロット消費
なし
dry runはクエリスロットを使わない
課金
なし
dry runの実行自体は課金されない
見積もりが0になる例
2種類
外部データソースのフェデレーテッドクエリ、行レベルセキュリティのマスク対象
hookの流れ
hookが行うことは4段階です。
bq queryの実行前に起きること
- 1
Claudeがbq queryを呼ぶ
PreToolUseがBashツールの入力を受け取り、tool_input.commandにコマンド文字列が入ります。 - 2
hookが--dry_run付きで同じコマンドを走らせる
コマンドの
bq queryの直後に--dry_runを差し込み、課金されない見積もりだけを取ります。 - 3
処理バイト数を上限と比べる
出力から「will process N bytes」のNを取り出し、環境変数の上限と比較します。
- 4
超過ならexit 2
stderrのメッセージがClaudeに「拒否の理由」として渡り、本番の
bq queryは実行されません。
hookスクリプト
.claude/hooks/bq-dryrun-guard.shに置く例です。jqが必要です。既定の上限は10 GiB(10737418240バイト)で、BQ_MAX_SCAN_BYTESで変えられます。
#!/bin/bash
# bq query をドライランに置き換えて見積もり、上限を超えたら exit 2
LIMIT=${BQ_MAX_SCAN_BYTES:-10737418240} # 既定は 10 GiB
input=$(cat)
cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
# bq query 以外、すでにドライランのコマンドは対象外
[[ "$cmd" =~ ^bq([[:space:]]+--[^[:space:]]+)*[[:space:]]+query ]] || exit 0
[[ "$cmd" == *--dry_run* || "$cmd" == *--dry-run* ]] && exit 0
if [[ "$cmd" == *[';|&']* || "$cmd" == *$'\n'* || "$cmd" == *'$('* || "$cmd" == *'`'* ]]; then
echo "bq query は他のコマンドと連結せず単独で実行してください(連結先もドライラン時に実行されてしまいます)" >&2
exit 2
fi
dry=$(sed -E '1s/^(bq([[:space:]]+--[^[:space:]]+)*[[:space:]]+query)/\1 --dry_run/' <<<"$cmd")
if ! out=$(bash -c "$dry" 2>&1); then
echo "ドライランに失敗したため止めました: $out" >&2
exit 2
fi
bytes=$(sed -nE 's/.*will process ([0-9]+) bytes.*/\1/p' <<<"$out" | head -1)
if [[ -z "$bytes" ]]; then
echo "スキャン量を読み取れなかったため止めました: $out" >&2
exit 2
fi
if (( bytes > LIMIT )); then
echo "見積もり ${bytes} bytes が上限 ${LIMIT} bytes を超えています。パーティションの絞り込みか列の削減を検討してください" >&2
exit 2
fi
exit 0このhookはコマンド文字列をbash -cでそのまま再実行するため、;・|・&&・改行・$(...)・バッククォートで後続のコマンドが付いていると、dry run確認の時点でそれも走ってしまいます。連結や置換を含むコマンドを先にexit 2で弾いているのはそのためです。
失敗する経路をすべてexit 2にしているのが要点です。hookの終了コードが0でも2でもないときは非ブロッキングのエラー扱いになり、ツール呼び出しはそのまま進みます。慣例どおりexit 1で終わらせると、課金を止める目的のhookが素通りします。
落とし穴
exit 1では止まらない
ポリシーを強制するhookはexit 2で終えます。exit 1は「非ブロッキングのエラー」で、トランスクリプトにhook errorの通知が出るだけでコマンドは実行されます。スクリプトが途中で落ちる場合に備え、設定側でonFailureを"block"にする方法もあります。
settings.jsonへの登録
プロジェクトの.claude/settings.jsonに次のように書きます。matcherでBashに絞り、ifでbqコマンドのときだけ起動させると、gitやlsのたびにプロセスが立ち上がるのを避けられます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(bq *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/bq-dryrun-guard.sh",
"timeout": 60,
"onFailure": "block"
}
]
}
]
}
}timeoutは秒単位で、command型の既定は600秒です。dry runは数秒で返る想定なので、短く切ったほうが、ネットワーク不調のときに長く待たされません。onFailure: "block"を付けると、スクリプトが起動できない・タイムアウトした場合もPreToolUseではツール呼び出しがブロックされます。
hookの通知にFailed with non-blocking status codeと出たら、終了コードが0でも2でもなかったということです。スクリプトのパスが存在しない場合も同じ通知になるので、実行権限(chmod +x)と${CLAUDE_PROJECT_DIR}以降のパスを最初に確かめてください。
実際のBigQueryに繋ぐ前の動作確認
bqの代わりに固定文字列を返すダミーをPATHの先頭に置くと、課金なしで分岐を確かめられます。
mkdir -p /tmp/fakebin && cat > /tmp/fakebin/bq <<'SH'
#!/bin/bash
echo "Query successfully validated. Assuming the tables are not modified, running this query will process ${FAKE_BYTES:-10918} bytes of data."
SH
chmod +x /tmp/fakebin/bq
echo '{"tool_input":{"command":"bq query --use_legacy_sql=false '"'"'SELECT 1'"'"'"}}' \
| FAKE_BYTES=99999999999 PATH=/tmp/fakebin:$PATH ./.claude/hooks/bq-dryrun-guard.sh
echo "exit=$?"上限を超えるFAKE_BYTESではexit 2とstderrのメッセージが返り、FAKE_BYTESを小さくするとexit 0になります。
Claudeには何が見えるか
PreToolUseがexit 2で止めたとき、Claudeはhookのstderrを拒否の理由として受け取ります。permissionDecision: "deny"をJSONで返したときと同じ扱いです。そのため、メッセージは「何が原因で、次に何をすれば通るか」を書いておくと、ClaudeがWHEREでパーティションを絞ったクエリに書き直して再提出しやすくなります。
上のスクリプトのメッセージも、上限と見積もり値を両方出しています。見積もりが分かれば、Claudeは「あとどれだけ減らせばよいか」を判断できます。
止めきれないケースと、maximum bytes billedとの使い分け
hookは万能のガードではありません。次の点は設計時に織り込んでください。
ifは目安のフィルタ: Bashのコマンドを判定できないとき、Claude Codeはifの条件を無視してhookを起動します。判定できないときはhookが動くので取りこぼしは起きにくい一方、if自体は厳密な許可・拒否の仕組みではありませんbq以外の経路は対象外:Bashにマッチさせたhookは、PythonのクライアントライブラリやMCPサーバー経由のクエリを見ません。MCPツールにはmcp__<server>__.*形式の別のmatcherが必要です- Windowsでは
Bashに発火しないことがある: Git Bashがない環境ではBashツール自体が登録されないため、Bashだけに合わせたhookは動きません。Bash|PowerShellで両方に合わせ、PowerShell用のスクリプトも用意します。詳しくはPowerShell hookがexit 2で止まらない問題にあります - dry runの見積もりが0のとき: 前述のフェデレーテッドクエリ、行レベルセキュリティのマスク対象では0バイトが返りうるため、上限判定をすり抜けます
BigQuery側にも、課金そのものを止める設定があります。bq queryの--maximum_bytes_billedは、実行前に読み取りバイト数を見積もり、上限を超えるとクエリを課金なしで失敗させます。
hookのdry run確認とmaximum bytes billedの違い
PreToolUse hook + dry run
止めた理由をClaudeに返せるので、書き直しまで自動で回せます。一方、bq以外の経路は別途ガードが必要で、見積もりが0になる例外もあります。
--maximum_bytes_billed
クエリを投げる経路を問わず効きます。ただし、クラスタ化テーブルの見積もりは上限側の値になるため、実際の課金が上限内でも失敗することがあります。
実運用では両方を併用する形が現実的です。hookでClaudeに見積もりを見せて自己修正させ、--maximum_bytes_billedを最後の安全網にします。BigQueryの課金コスト管理のガイドでも、オンデマンド課金でのコスト制限策として最大請求バイト数の設定が挙げられています。
hookの失敗時の動作設定はClaude Code hooksのonFailureで失敗時に操作を止める設定で詳しく扱っています。イベント全体の一覧はClaude Code Hooks完全ガイドで確認できます。
上限値の決め方
10 GiBという既定値はあくまで例です。決め方の目安は、よく使うクエリの見積もりをbq query --dry_runで数回取り、その最大値の数倍に置くことです。上限が低すぎると、正当な分析までClaudeが書き直しを繰り返し、会話が無駄に長くなります。
dry runの出力は「テーブルが変更されないと仮定すれば、このクエリはNバイトを処理する」という前提付きの見積もりです。実行までの間にテーブルが更新されると、実際の処理量とずれる余地があります。見積もりを料金に直したいときは、料金計算ツールに入れます。計算ツールの最小単位はMBなので、小さなクエリではKBからMBへの換算が要ります。
プロジェクトごとに変えたいときは、シェルの環境変数としてBQ_MAX_SCAN_BYTESを設定してからClaude Codeを起動します。hookスクリプトのLIMITの行を直接書き換えても構いません。
まとめ
BigQueryの課金は、スキャンされたバイト数に比例します。そのため「実行前に見積もる」ことが、Claude Codeに分析を任せるときの最も手間のかからない歯止めのひとつです。dry runが無料で、PreToolUseが実行前に割り込めるという2つの性質が揃うので、この構成が成立します。
ただし、bqの呼び出しだけを見るhookは、他の経路には効きません。社内で共有する環境では、hookで自己修正の流れを作り、--maximum_bytes_billedで上限を保証する、という分担になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。