1
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 のブラウザ操作で「送信・公開」ボタンだけを PreToolUse フックで止める(言い換えとJSクリックの抜け道つき)

1
Posted at

何に困ったか

Claude Code にブラウザ拡張(Claude in Chrome)を使わせて、フォームへの入力を任せていました。
ある日、「先に送っちゃおう」と私が言ったのを許可と受け取って、Claude が送信ボタンを押してしまいました。私は承認したつもりがありませんでした。

CLAUDE.md に「送信は自分が押す」と書く方法もあります。ただ、文章のお願いは守られないことがあります。
そこで、外に出る最後のボタンのクリックだけを、PreToolUse フックで機械的に止めることにしました。入力・下書き保存・ページ移動は止めません。

この記事では、実際に動かしているフックと、手元で実行して確かめた結果、止められなかった抜け道を書きます。

環境

  • Claude Code(CLI)、Python 3
  • ブラウザ操作は Claude in Chrome の MCP ツール(mcp__claude-in-chrome__computer など)
  • フックは ~/.claude/hooks/boss-button-guard.py に置き、~/.claude/settings.json から呼ぶ

設定: settings.json

PreToolUse の matcher に、クリックやキー入力ができるツールを並べます。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__claude-in-chrome__computer|mcp__claude-in-chrome__browser_batch|mcp__claude-in-chrome__javascript_tool",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/boss-button-guard.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

フックは終了コード 2 で終わると、そのツール呼び出しを止め、標準エラーに出した文章を Claude に返します。

フックの中身

判断に使うのは、computer ツールの引数にある action_summary(これから何を押すかの短い説明)です。
Claude は押す前にこの欄へ「何のボタンか」を書くので、その言葉で判定します。

import json, re, sys

data = json.load(sys.stdin)
tool = data.get("tool_name", "")
inp = data.get("tool_input", {}) or {}

# 押したら外に出る・取り消せないボタンの言葉
FINAL = re.compile(r"送信|送る|投稿|公開する|公開し|応募|購入|申し込|申込|予約設定|更新する|返信する|フォローする|いいね|リポスト|提案する|契約する|署名|承諾|同意する|submit|send|post|publish", re.I)
# 押してよいもの(外に出ない)
SAFE = re.compile(r"下書き|公開に進む|公開設定へ|プレビュー|入力|貼り付け|クリックする前|開く|選ぶ|閉じる")

CLICKS = {"left_click", "double_click", "triple_click", "key", "right_click"}

def bad_computer(i):
    if i.get("action") not in CLICKS:
        return None
    s = i.get("action_summary", "") or ""
    if i.get("action") == "key" and not re.search(r"Return|Enter|cmd\+Return|ctrl\+Return", i.get("text", "") or "", re.I):
        return None
    if FINAL.search(s) and not SAFE.search(s):
        return s
    return None

def bad_js(t):
    # JS で押すボタンを、押す言葉で見つけているもの
    if re.search(r"\.click\(\)|dispatchEvent\(|requestSubmit\(|\.submit\(\)", t) and re.search(
        r"送信|投稿|公開する|応募|購入|申し込|更新する|返信|フォロー|予約設定|提案する|契約", t
    ):
        return "JavaScript でのクリック"
    return None

why = None
if tool.endswith("__computer"):
    why = bad_computer(inp)
elif tool.endswith("__javascript_tool"):
    why = bad_js(inp.get("text", "") or "")
elif tool.endswith("__browser_batch"):
    for it in inp.get("actions", []) or []:
        n, i = it.get("name", ""), it.get("input", {}) or {}
        if n == "computer":
            why = bad_computer(i)
        elif n == "javascript_tool":
            why = bad_js(i.get("text", "") or "")
        if why:
            break

if not why:
    sys.exit(0)

print(f"止めた: 「{why}」は外に出る最後のボタンなので、人間が押す。入力までで止め、「〇〇のタブの△△を押してください」と頼む。", file=sys.stderr)
sys.exit(2)

ポイントは3つです。

  • 止める言葉(FINAL)と通す言葉(SAFE)の2段にする。「下書き保存」は「保存」でも外に出ないので、SAFE に入れて通します
  • browser_batch(複数の操作をまとめて渡すツール)の中身も見る。まとめた操作の中にクリックが混ざっていても止まります
  • Enter キーも対象にする。フォームは Enter で送れるので、key の Return / Enter も同じ言葉で判定します

動かして確かめた結果

標準入力に JSON を渡して、終了コードを見ました(echo '<JSON>' | python3 boss-button-guard.py; echo $?)。2026-11-05 に実行した結果です。

  • action_summary が「「投稿する」ボタンを押す」の左クリック → 終了コード 2(止まる)
  • 「下書き保存を押す」 → 0(通る)
  • 「公開に進むを押す」 → 0(通る。この画面は外に出ない)
  • 「本文欄をクリックする(投稿の入力)」 → 0(通る。「投稿」という言葉があるが、SAFE の「入力」で通る)
  • javascript_tool で document.querySelector("button").click(); // 送信 → 2(止まる)
  • key の Return で、説明が「Enterで送信」 → 2(止まる)
  • 「予約設定を押す」 → 2(止まる)

止まったとき、Claude には「止めた: 〇〇は外に出る最後のボタンなので、人間が押す」という文が返り、入力までで手を止めて、押してほしいボタンを私に頼む動きになりました。

つまずき・抜け道

1. 言葉で判定しているので、説明の書き方で通ったり止まったりする

最初、ボタンの説明に「投稿」と書いただけで、本文を入れる欄のクリックまで止まりました。
説明を「本文欄をクリックする(入力)」のように言い換えると通ります(上の4つ目)。

逆の問題もあります。FINAL にない言葉で説明すれば、本当は送信ボタンでもすり抜けます。
このフックは「説明文を正直に書く」ことが前提です。完全な壁ではなく、うっかり押すのを防ぐ柵です。

2. 例外は、書式を固定して1つだけ

予約の時間が画面に出る「予約設定」のように、取り消せるボタンだけは Claude に押させたい場面がありました。
私は例外を、画面に出た日時の文をそのまま写した説明にだけ合う正規表現で1つだけ作りました。

X_SCHEDULE = re.compile(r"^X の予約設定を押す\(2026年\d{1,2}月\d{1,2}日\([月火水木金土日]\)の午[前後]\d{1,2}:\d{2}に送信されます\)$")

left_click で、説明がこの文面に完全一致したときだけ通します。上の「予約設定を押す」は一致しないので止まります。
「すぐ出る」ボタン、返信、JS のクリックは例外にしていません。

3. 口頭の許可より、仕組みの止めのほうが強い

「いいよ、送っちゃおう」と私が言っても、このフックは止めます。
許可を広げたいときは、会話で言うのではなく、フックの例外として書き直すことにしました。

まとめ

  • 外に出る最後のボタンは、CLAUDE.md の文章ではなく、PreToolUse フック(終了コード 2)で止めた
  • 止める言葉と通す言葉の2段にして、browser_batch と Enter キーも見る
  • JS のクリックも止まる。ただし説明文の言い方次第で抜けられるので、これは柵であって壁ではない
  • 例外は、画面の文を写した正規表現に完全一致するものだけ

動かない場合の確認は、echo '<JSON>' | python3 boss-button-guard.py; echo $? で終了コードを見るのが早いです。

参考

1
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
1
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?