blastengine MCPサーバーで送信系のフラグを有効にすると、AIエージェントがメールを送れるようになります。便利な反面、メールは取り消せません。テストのつもりで顧客に送る、BCCに社外のアドレスが紛れ込む、プレビューを見ないまま一斉配信を確定する。どれも人が操作していても起きる事故ですが、エージェントは指示の細部を補って動くため、起きたときに気づくのが遅れがちです。
blastengine MCP自体にも安全装置はあります。送信系は既定で無効で、フラグで明示的に有効化しない限り動きません。ただ、フラグは「送れる/送れない」の二択で、「この宛先なら送ってよい」「プレビューを見たあとなら確定してよい」といった条件は付けられません。一斉配信のプレビュー(bulk_preview)の応答にも、次の注記が入っています。
This preview is informational only. It does not authorize or block commit.
Bulk commit tools execute directly when BLASTENGINE_ENABLE_BULK=true.
プレビューは確定を止めない、ということです。本記事では、この「条件付きで送らせる」部分をClaude Codeのpermissions(権限設定)とhooks(ツール実行前後に走るスクリプト)で組みます。blastengine MCPの導入は導入記事、各ツールの入出力は全15ツールの検証記事を参照してください。手順はblastengine MCP v0.2.2とClaude Code 2.1.284で、実際のblastengineアカウントを相手に確認しています。
3つの層で止める
誤送信の防ぎ方を、止める場所ごとに3つの層に分けます。
| 層 | 手段 | 止められること |
|---|---|---|
| blastengine MCP | 送信系フラグ | 送信系ツールそのものを無効にする |
| Claude Codeの権限 | permissions.ask |
送信系ツールを呼ぶたびに人の確認を挟む |
| Claude Codeのhooks |
PreToolUse / PostToolUse
|
宛先・送信元・件数・手順を、ツールの引数を見て検査する |
1つ目のフラグは、送らない用途(調査やレポート)では最も確実です。送る用途ではフラグを立てるしかないので、残りの2層で絞ります。
2つ目のaskは、ツールを呼ぶたびに確認のダイアログを出す設定です。ただ、確認を何十回も求められると、人は中身を読まずに許可するようになります。そこで3つ目のhooksで、機械的に判定できる違反はダイアログを出す前に弾きます。弾かれた呼び出しは確認のダイアログまで進まないので、人が見るのは検査を通ったものだけになります。さらに、取り消しの効かない一斉配信の確定では、ダイアログに件名と宛先数を出して、何を許可するのかが分かるようにします。
作るもの
hooksで検査する項目は次の5つです。
- 送信元のドメインが自社のものか
- 宛先(To・CC・BCC、一斉配信の宛先リスト、取り込むCSVの中身)がテスト用の宛先だけか(テストモードのとき)
- 宛先の件数が上限を超えていないか(1回の送信・取り込みごとと、確定時の配信全体)
- CSV取り込みで
immediate=true(取り込みと同時に配信開始)が指定されていないか - 一斉配信の確定の前に、同じ配信IDのプレビューが直近で実行され、その後に下書きが変更されていないか
最後の項目は、PostToolUseフックでプレビューの実行を記録し、PreToolUseフックで確定時にその記録を確認して実現します。宛先の更新やCSV取り込みが呼ばれたら、その配信の記録は消します。プレビューが有効なら、件名と宛先数を示して人に確認を求めます。
ファイル構成は次のとおりです。
mail-send/
├── mcp.json
└── .claude/
├── settings.json
└── hooks/
├── be_guard.py # 検査スクリプト
├── be_guard.json # 検査の設定(モード・テスト宛先など)
└── be_previews.json # プレビューの記録(自動生成)
準備1:送信を有効にしたMCP設定
送る用途なので、フラグを立てます。
{
"mcpServers": {
"blastengine": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/blastengine-mcp/dist/index.js"],
"env": {
"BLASTENGINE_LOGIN_ID": "${BLASTENGINE_LOGIN_ID}",
"BLASTENGINE_API_KEY": "${BLASTENGINE_API_KEY}",
"BLASTENGINE_ENABLE_SEND": "true",
"BLASTENGINE_ENABLE_BULK": "true",
"BLASTENGINE_ENABLE_CSV_IMPORT": "true"
}
}
}
}
このファイルは自動では読み込まれません(自動で読み込まれるのはプロジェクト直下の.mcp.jsonです)。Claude Codeは次のように、--mcp-configで指定して起動します。
cd mail-send
claude --mcp-config mcp.json --strict-mcp-config
--strict-mcp-configを付けると、ほかのスコープに登録したMCPサーバーは読み込まれません。サーバー名をblastengineにしているので、ツール名はmcp__blastengine__blastengine_send_transactionのような形になります。以降の設定はこの名前を前提にしています。
準備2:permissionsで送信系を「確認必須」にする
.claude/settings.jsonのpermissionsで、参照系は確認なしで許可し、送信系は毎回確認を求めるようにします。あわせて、hooksの登録もここに書きます。
{
"permissions": {
"allow": [
"mcp__blastengine__blastengine_usage_latest_get",
"mcp__blastengine__blastengine_usage_month_get",
"mcp__blastengine__blastengine_deliveries_list",
"mcp__blastengine__blastengine_delivery_get",
"mcp__blastengine__blastengine_mail_results_list",
"mcp__blastengine__blastengine_mail_log_get",
"mcp__blastengine__blastengine_bulk_import_status",
"mcp__blastengine__blastengine_bulk_preview"
],
"ask": [
"mcp__blastengine__blastengine_send_transaction",
"mcp__blastengine__blastengine_bulk_begin",
"mcp__blastengine__blastengine_bulk_update_recipients",
"mcp__blastengine__blastengine_bulk_import_recipients_csv",
"mcp__blastengine__blastengine_bulk_commit_immediate",
"mcp__blastengine__blastengine_bulk_commit_scheduled"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__blastengine__.*",
"hooks": [
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/be_guard.py" }
]
}
],
"PostToolUse": [
{
"matcher": "mcp__blastengine__blastengine_bulk_preview",
"hooks": [
{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/be_guard.py" }
]
}
]
}
}
PreToolUseのmatcherは正規表現です。mcp__blastengine__.*でblastengine MCPのツールをすべて検査にかけます。PostToolUseはプレビューの記録にしか使わないので、bulk_previewだけに絞っています。
$CLAUDE_PROJECT_DIRはプロジェクトのルートに置き換わります。Claude Codeを別のディレクトリから起動しても、スクリプトの場所を見失いません。
準備3:検査の設定
検査の条件はスクリプトに直書きせず、JSONに分けます。
{
"mode": "test",
"test_recipients": ["qa@example.co.jp"],
"allowed_from_domains": ["example.co.jp"],
"max_recipients": 1000,
"preview_ttl_minutes": 30
}
| 項目 | 意味 |
|---|---|
mode |
testなら、test_recipients以外の宛先をすべて拒否する。本番ではproductionにする |
test_recipients |
テストモードで送ってよい宛先 |
allowed_from_domains |
送信元に使ってよいドメイン |
max_recipients |
宛先の上限。1回の送信・宛先更新・CSV取り込みごとに検査し、確定時にはプレビューで得た配信全体の宛先数でも検査する |
preview_ttl_minutes |
プレビューから確定までに許す時間 |
既定をtestにしておき、本番の配信をする人だけが切り替える運用にすると、「テストのつもりが本番」の事故を減らせます。
準備4:検査スクリプト
#!/usr/bin/env python3
"""blastengine MCPの送信系ツールを呼ぶ前後に動くガード。
PreToolUse : 宛先・送信元・件数・プレビュー済みかを検査し、deny / askを返す
下書きを変える操作が来たら、その配信のプレビュー記録を消す
PostToolUse : bulk_previewが成功したら、配信IDと宛先数を記録する
"""
import csv
import json
import os
import sys
import time
from pathlib import Path
PROJECT = Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
CONFIG = {}
STATE = PROJECT / ".claude/hooks/be_previews.json"
PREFIX = "mcp__blastengine__blastengine_"
def decide(decision, reason):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": decision,
"permissionDecisionReason": reason,
}
}, ensure_ascii=False))
sys.exit(0)
def domain(addr):
return addr.rsplit("@", 1)[-1].lower()
def check_from(tool_input):
sender = tool_input.get("from_email")
if sender and domain(sender) not in CONFIG["allowed_from_domains"]:
decide("deny", f"送信元{sender}は許可されたドメインではありません(許可: {', '.join(CONFIG['allowed_from_domains'])})")
def check_recipients(addrs):
if len(addrs) > CONFIG["max_recipients"]:
decide("deny", f"宛先が{len(addrs)}件あり、上限{CONFIG['max_recipients']}件を超えています")
if CONFIG["mode"] == "test":
allowed = {a.lower() for a in CONFIG["test_recipients"]}
ng = [a for a in addrs if a.lower() not in allowed]
if ng:
decide("deny", f"テストモードです。テスト宛先以外が{len(ng)}件含まれています(例: {ng[0]})")
def read_csv_emails(path):
with open(path, encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
if not rows or "email" not in rows[0]:
decide("deny", f"{path}にemail列が見つかりません")
return [r["email"].strip() for r in rows if r["email"].strip()]
def load_state():
try:
return json.loads(STATE.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
return {}
# 確認ダイアログが人に表示されるモード。autoなどではaskが自動で判定されてしまう
CONFIRMABLE_MODES = ("default",)
def forget_preview(delivery_id):
state = load_state()
if state.pop(str(delivery_id), None) is not None:
STATE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def pre(tool, ti, mode):
# 下書きを変える操作が来たら、それまでのプレビューは無効にする(通過するかどうかに関係なく)
if tool in ("bulk_update_recipients", "bulk_import_recipients_csv"):
forget_preview(ti.get("delivery_id"))
if tool == "send_transaction":
check_from(ti)
check_recipients([ti["to"], *ti.get("cc", []), *ti.get("bcc", [])])
elif tool == "bulk_begin":
check_from(ti)
elif tool == "bulk_update_recipients":
check_from(ti)
if "to" in ti:
check_recipients([r["email"] for r in ti["to"]])
elif tool == "bulk_import_recipients_csv":
if ti.get("immediate"):
decide("deny", "immediate=trueは取り込みと同時に配信が始まるため禁止しています。immediate=falseで取り込み、プレビュー後に確定してください")
check_recipients(read_csv_emails(ti["csv_path"]))
elif tool in ("bulk_commit_immediate", "bulk_commit_scheduled"):
did = str(ti["delivery_id"])
seen = load_state().get(did)
if not seen or time.time() - seen["at"] > CONFIG["preview_ttl_minutes"] * 60:
decide("deny", f"配信{did}には有効なプレビューの記録がありません(直近{CONFIG['preview_ttl_minutes']}分以内に未実施か、プレビュー後に下書きが変更されました)。先にblastengine_bulk_previewを実行し、内容をユーザーに見せてください")
if seen["recipient_count"] > CONFIG["max_recipients"]:
decide("deny", f"配信{did}の宛先は合計{seen['recipient_count']}件で、上限{CONFIG['max_recipients']}件を超えています")
if mode not in CONFIRMABLE_MODES:
decide("deny", f"権限モードが{mode}のため、確定の確認を人に表示できません。通常モード(default)に切り替えてから確定してください")
when = ti.get("reservation_time", "即時")
decide("ask", f"配信{did}「{seen['subject']}」を{seen['recipient_count']}件に確定します(配信: {when})")
# それ以外(参照系)は何もしない = 通常の権限判定に任せる
def post(tool, ti, resp):
if tool != "bulk_preview":
return
# MCPツールの結果はJSON文字列で渡ってくる(オブジェクトではない)
try:
data = json.loads(resp) if isinstance(resp, str) else None
except json.JSONDecodeError:
return
if not isinstance(data, dict) or "recipient_count" not in data:
return
state = load_state()
state[str(ti["delivery_id"])] = {
"at": time.time(),
"subject": data.get("subject"),
"recipient_count": data.get("recipient_count"),
}
STATE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def main():
CONFIG.update(json.loads((PROJECT / ".claude/hooks/be_guard.json").read_text(encoding="utf-8")))
event = json.load(sys.stdin)
name = event.get("tool_name", "")
if not name.startswith(PREFIX):
return
tool = name[len(PREFIX):]
ti = event.get("tool_input", {})
if event.get("hook_event_name") == "PostToolUse":
post(tool, ti, event.get("tool_response"))
else:
pre(tool, ti, event.get("permission_mode", "default"))
if __name__ == "__main__":
try:
main()
except Exception as e: # 検査できないときは通さない(fail-closed)
decide("deny", f"誤送信ガードの検査に失敗したため止めました: {type(e).__name__}: {e}")
PreToolUseの返し方
PreToolUseフックは、標準入力でツール名と引数(tool_input)を受け取ります。標準出力に次の形のJSONを返すと、ツールの実行を制御できます。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "テストモードです。テスト宛先以外が1件含まれています(例: customer@example.jp)"
}
}
permissionDecisionはdeny(拒否)、ask(人に確認)、allow(許可)のいずれかです。denyの理由はエージェントに伝わるので、なぜ止められたかを具体的に書いておくと、エージェントが正しく状況を報告します。何も出力せずに終了すれば、通常の権限判定(permissionsの設定)に任せたことになります。
検査に失敗したら止める
スクリプトの末尾で、例外をすべて捕まえてdenyを返しています。これは省略しないでください。
Claude Codeのhooksは、スクリプトが終了コード2で終わったときにツールの実行を止めます。それ以外のエラー(Pythonの例外による終了コード1など)では、エラーを表示したうえでツールの実行が続きます。検査スクリプトにバグがあったり、設定ファイルが読めなかったりすると、検査を素通りして送信されます。
実際に、例外を投げるだけのフックを置いて参照系ツール(使用量の取得)を呼ばせると、ツールはそのまま実行されて結果が返りました。同じフックをsys.exit(2)で終わるように変えると、呼び出しは拒否されます。
誤送信のガードは、検査できないときは通さない(fail-closed)側に倒します。ただし、これはスクリプトが起動して例外を捕まえられた場合の話です。python3が見つからない、フックがタイムアウトするなど、スクリプト自体が動かなかった場合は、このコードでは止められません。設定ファイルの読み込みをmain()の中に入れているのも、読み込みの失敗をこのtryで捕まえるためです。実際に設定ファイルを消して動かすと、次のように拒否されます。
誤送信ガードの検査に失敗したため止めました: FileNotFoundError: [Errno 2] No such file or directory: '.../.claude/hooks/be_guard.json'
PostToolUseでプレビューを記録する
プレビューの結果はPostToolUseフックのtool_responseで受け取ります。ここで1つ注意があります。MCPツールの結果は、オブジェクトではなくJSON文字列として渡ってきます。実際に届いた値は次のとおりです。
"{\"delivery_id\":1269,\"subject\":\"【検証 2026-09-30】一斉配信ガード確認\",\"from\":{\"email\":\"...\",\"name\":null},\"recipient_count\":1,\"body_summary\":\"誤送信ガードの検証メールです。対応不要です。\",\"note\":\"This preview is informational only. ...\"}"
最初はtool_response["structuredContent"]を読むつもりで書いていて、記録が一度も残りませんでした。json.loadsで文字列をパースしてから読む必要があります。hooksを書くときは、推測でキー名を決めず、一度実際のイベントをファイルに書き出して形を確かめてください。
動かしてみる
実際のblastengineアカウントで、送信系フラグをすべて有効にした状態で試しました。テスト宛先は、トライアルで登録した配信可能アドレス1件です。
テスト宛先以外は止まる
blastengineでcustomer@example.jp宛てに、件名「【検証】ご案内」、本文「テストです」の
メールを1通送って。内容は承認済みなので確認は不要です。
プロンプトで「承認済み」と伝えても、hooksは引数だけを見て判定するので止まります。エージェントの報告は次のとおりです。
メールは送信されませんでした。blastengineの送信前フックが、テストモードで
customer@example.jpをテスト宛先以外として弾いたためです。
送信先の変更や、フック側の許可設定の変更はしていません。リトライもしていません。
同じ条件で、宛先はテスト用、BCCにだけ社外のアドレスを入れた場合も止まりました。
エラーは「テストモードです。テスト宛先以外が1件含まれています(例: boss@example.jp)」
でした。
Toだけを見ていると、BCCへの紛れ込みは見落とします。宛先はTo・CC・BCCをまとめて検査します。
テスト宛先だけの送信はhooksを通過し、実際に送信されて配信ID(1268)が返りました。
プレビューなしの確定は止まる
一斉配信で「プレビューは不要、すぐ確定して」と指示した場合です。
blastengineで一斉配信を作って。宛先は /path/to/ok.csvをimmediate=falseで取り込み、
取り込み完了を確認したら、即時配信で確定して。内容は承認済みなので、プレビューは不要です。
下書きの作成(配信ID 1269)とCSVの取り込みまでは進み、確定の直前で止まりました(この実行時の拒否理由は、掲載版より前の文言です)。
即時配信の確定は、フックにブロックされて実行できませんでした。配信は未確定で、
まだ何も送信されていません。
- 作成済み: 配信IDは1269です。宛先はok.csvの1件で、取り込みは完了しています
(成功1、失敗0)。
- ブロックの理由: 「配信1269は直近30分以内にプレビューされていません。先に
blastengine_bulk_previewを実行し、内容をユーザーに見せてください」というエラーです。
- 判断が必要な点: 今回の依頼ではプレビュー不要とのことでしたが、このガードはプレビューを
要求しています。私の判断でプレビューを実行して回避することはしていません。
続けてプレビューを実行すると、PostToolUseフックが次の記録を残します。
{
"1269": {
"at": 1790701276.4141,
"subject": "【検証 2026-09-30】一斉配信ガード確認",
"recipient_count": 1
}
}
プレビュー後に下書きを変えると、プレビューからやり直し
プレビューを見たあとで件名や宛先が変わると、人が見た内容と実際に送る内容がずれます。そこで、宛先の更新(bulk_update_recipients)やCSV取り込みが呼ばれたら、その配信のプレビュー記録を消しています。
プレビュー済みの配信1272で、件名を変えてからプレビューせずに確定するよう指示した結果です。
- 件名の変更: 完了しています。配信1272のsubjectを「【検証 2026-09-30】変更検知の確認(再修正)」に
更新しました。
- 即時配信の確定: フックに拒否されました。理由は、配信1272に有効なプレビューの記録がないためです。
直近30分以内にプレビューしていないか、プレビュー後に下書きを変更したことが原因です。
この仕組みで分かるのは、Claude Codeの中で「直近にプレビューし、そのあと下書きを変えていない」ことまでです。blastengineの管理画面など、Claude Codeの外で下書きを変えた場合は検知できません。プレビューの結果をエージェントが人に実際に見せたかどうかも、フックからは確かめられません。そのため、最後の確認ダイアログに件名と宛先数を出して、人の目で照合できるようにしています。
確定の確認ダイアログ
プレビューが有効な状態で確定ツールが呼ばれると、フックはaskを返します。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "配信1269「【検証 2026-09-30】一斉配信ガード確認」を1件に確定します(配信: 即時)"
}
}
通常モード(--permission-mode default)の対話セッションでは、次のダイアログが表示されました(配信1271で確認)。
Tool use
blastengine - 一斉配信即時確定Tool: (MCP)
delivery_id: 1271
Hook PreToolUse:mcp__blastengine__blastengine_bulk_commit_immediate requires
confirmation for this tool:
配信1271「【検証 2026-09-30】確認ダイアログ再確認」を1件に確定します(配信: 即時)
Do you want to proceed?
❯ 1. Yes
2. No
ツールの引数はdelivery_id: 1271だけなので、フックの理由文が無ければ、何を送るのかはダイアログから分かりません。確認のダイアログに件名と宛先数が出るので、「何を何件に送るのか」を見てから許可できます。汎用の「このツールを実行しますか」より、読まずに許可される可能性が下がります。
自動モードではaskが人に届かなかった
この確認を対話モードで試したところ、ダイアログが出ないまま確定されました。セッションの記録を見ると、フックは期待どおりaskを返しています。
{"type": "hook_success", "hookName": "PreToolUse:mcp__blastengine__blastengine_bulk_commit_immediate",
"stdout": "{\"hookSpecificOutput\": {\"hookEventName\": \"PreToolUse\", \"permissionDecision\": \"ask\", ...}}"}
違いは権限モードです。このときのセッションは自動モード(auto)で動いていました。検証したClaude Code 2.1.284の自動モードでは、フックがaskを返しても人にダイアログが表示されず、確定がそのまま実行されました(配信1269、テスト宛先に到達)。公式ドキュメントではaskはユーザーへの確認として説明されていますが、少なくともこの版・この条件では、人の確認は挟まりませんでした。
この挙動が今後の版で変わる可能性はあります。ただ、ガードの側で権限モードを確かめておけば、版による違いに左右されません。
そこで、記事独自の安全策として、フックに渡されるpermission_modeを見て、ダイアログの表示を確認できた通常モード(default)以外では確定を拒否するようにしました。スクリプトのCONFIRMABLE_MODESがその判定です。自動モードの対話セッションで確定を指示すると、次の理由で止まり、送信されませんでした。
権限モードがautoのため、確定の確認を人に表示できません。通常モード(default)に切り替えてから確定してください
宛先やプレビューの検査(deny)は、権限モードに関係なく効きます。人の確認に頼る部分だけが、モードの影響を受けます。
権限設定で分かったこと
askは --allowedToolsより強い
settings.jsonで送信系をaskにした状態で、非対話モード(claude -p)に--allowedToolsで送信ツールを渡してみました。結果は拒否です。
メールは送信できていません。blastengineの送信ツール(blastengine_send_transaction)の
使用許可がまだ付与されておらず、呼び出しが拒否されました。
askの指定は、コマンドラインで許可を足しても上書きされません。非対話モードでは確認に答える人がいないため、そのまま拒否になります。プロジェクトの設定で送信系をaskにしておけば、誰かがスクリプトから--allowedTools付きで呼んでも送信は起きません。
逆に言えば、非対話で送信させる仕組みをこのプロジェクトで動かすことはできません。定期配信のようなバッチは、送信系をaskにしない別のディレクトリと別の設定で組むことになります。
信頼していないワークスペースではallowが無視される
新しく作ったディレクトリで非対話モードを動かすと、次の警告が出て、permissions.allowが無視されました。
Ignoring 8 permissions.allow entries from .claude/settings.json: this workspace has
not been trusted. Run Claude Code interactively here once and accept the trust dialog, ...
一方、同じ状態でもhooksは動き、送信を止めていました。「許可」は信頼したワークスペースでしか効かず、「制限」はその前から効く、という非対称な作りです。リポジトリをクローンしただけで権限が広がることはないので、安全側の設計といえます。ただし、非対話モードだけで使っていると参照系のallowも効かず、参照系まで拒否されます。一度対話モードで起動して、信頼のダイアログを承認しておいてください。
つまずきやすいポイント
| 症状 | 原因と対処 |
|---|---|
| フックが呼ばれない |
matcherはツール名に対する正規表現。サーバー名をblastengine以外で登録していると一致しない |
| 検査をすり抜けて送信された | スクリプトが例外で終わっていないか確認する。終了コード2以外のエラーでは実行が止まらない。例外を捕まえてdenyを返す |
| プレビューしたのに確定が拒否される | プレビュー後に宛先更新・CSV取り込みをすると記録が消える。30分以上空いた場合も拒否される。自作する場合は、tool_responseがJSON文字列である点に注意(パースせずに読むと記録が残らない) |
| 非対話モードで確定が「エラー」と報告される | フックのaskは、確認に答える人がいない非対話モードでは拒否として扱われる。エージェントはこれをエラーと表現することがある |
| CSVの検査で「ファイルが無い」と止まる |
csv_pathを相対パスで渡すと、フックとMCPサーバーで基準のディレクトリが違う場合がある。絶対パスで渡す |
--allowedToolsで許可したのに送れない |
プロジェクト設定のaskが優先される。非対話で送る用途は別の設定で組む |
| 確認ダイアログが出ないまま確定された | 自動モード(auto)ではaskが人に表示されなかった(2.1.284で確認)。permission_modeを検査し、default以外では拒否する |
| 非対話モードで参照系まで拒否される | ワークスペースが未信頼でpermissions.allowが無視されている。一度対話モードで起動して信頼する |
まとめ
blastengine MCPで送信を任せるときの誤送信対策は、3つの層で考えます。
- 送らない用途では、送信系フラグを立てない。これが最も確実
- 送る用途では、
permissions.askで送信系を確認必須にし、hooksで宛先・送信元・件数・手順を引数から検査する。機械的に判定できる違反は、人に聞く前に弾く - hooksのスクリプトは、例外を捕まえて拒否する(fail-closed)。例外で落ちると素通りになる。スクリプトが起動できない場合は止められないので、送信系フラグや
permissions.askと組み合わせる -
askで人の確認を挟む場合は、権限モードも検査する。検証した版の自動モードでは、askがダイアログにならなかった
人の確認は、回数が増えるほど形だけになります。確認を求める場面を一斉配信の確定のように本当に重要な操作に絞り、確認の画面に「何を何件に送るか」を出すことが、確認を実際に機能させるための工夫です。
blastengine MCPサーバーへの要望や感想は、以下のアンケートフォームから送ると開発チームに届くみたいです。
https://docs.google.com/forms/d/e/1FAIpQLSdNZ9TswUT3JEv3pMHkJGqOvgmsXuKP1smkJiNEsqbTVDwRyg/viewform