Claude SDKのbrowser toolsetでブラウザドライバーを実装する
PythonとTypeScriptのSDKで、BetaAbstractBrowserToolset20260801を継承してドライバーを書く手順。最小実装、configsとフック、tool runnerなしのループまでを扱います。
ブラウザドライバーとは何か — SDKが担う範囲
Claude SDKのbrowser toolsetは、ブラウザ操作の呼び出しを受け取る側を自分のクラスとして書くための仕組みです。PythonとTypeScriptのSDKには、browser use tool用のクラスBetaAbstractBrowserToolset20260801が入っています。これを継承し、navigateやleft_clickのようなメンバーツールごとに1メソッドを実装したものが「ドライバー」です。どちらのSDKでもベータ提供です。
SDKが受け持つのは、呼び出しの振り分け、渡したポリシーの実行、承認コールバックの呼び出し、tool_resultの組み立てです。ブラウザ本体、デスクトップ、できあいのドライバー、URLポリシーは含まれません。Playwrightなどの自動化ライブラリをラップする部分は自分で書きます。
Messages APIにJSONを直接組んで往復を自前で回す方法は、Browser Useツールの実装で扱っています。本記事は、その実行側をSDKのクラスに任せる書き方です。
最小のドライバーを書く — navigate・screenshot・left_click
最小構成は4つのメソッドです。navigate、screenshot、left_clickの3メンバーと、すべてのドライバーに必須の状態報告_browser_state(TypeScriptではbrowserState)を実装します。以下は、SDKのクイックスタートに沿った形の例です。backendはPlaywrightなどを包んだ自作のラッパーを指します。
from anthropic import Anthropic
from anthropic.tools.browser import (
BetaAbstractBrowserToolset20260801,
BetaBrowserNavigateResult,
BetaBrowserState,
BetaScreenshotResult,
BetaToolsetCallContext,
)
from anthropic.types.beta import (
BetaBrowserLeftClickInput,
BetaBrowserNavigateInput,
BetaBrowserScreenshotInput,
BetaBrowserStateTabEntryParam,
)
class MyBrowser(BetaAbstractBrowserToolset20260801):
def __init__(self, backend, **options):
super().__init__(**options)
self.backend = backend
def _browser_state(self, context: BetaToolsetCallContext) -> BetaBrowserState:
return BetaBrowserState(
tabs=[
BetaBrowserStateTabEntryParam(
tab_id=tab.id, title=tab.title, url=tab.url,
active=tab.id == self.backend.active,
)
for tab in self.backend.tabs()
],
state_changes=self.backend.drain_changes(),
)
def navigate(
self, context: BetaToolsetCallContext, input: BetaBrowserNavigateInput
) -> BetaBrowserNavigateResult:
page = self.backend.goto(input.url, input.tab_id)
return BetaBrowserNavigateResult(
url=page.url, status=page.status, title=page.title
)
def screenshot(
self, context: BetaToolsetCallContext, input: BetaBrowserScreenshotInput
) -> BetaScreenshotResult:
data = self.backend.png_base64(input.tab_id)
return BetaScreenshotResult(data=data, media_type="image/png")
def left_click(
self, context: BetaToolsetCallContext, input: BetaBrowserLeftClickInput
) -> None:
self.backend.click(input.target, input.tab_id) # 戻り値なし
def close(self) -> None:
super().close() # 先に親を閉じ、実行中の呼び出しがなくなってからブラウザを閉じる
if not self.backend.closed:
self.backend.close()メンバーのメソッドは、呼び出しコンテキストと、BetaBrowserNavigateInputのような型付きの入力を受け取ります。navigateのinput.urlは、"back"・"forward"・"reload"のいずれか、またはURLポリシーを通過したURLがClaudeの書いたままの形で届きます。https://のないURLはbackend.goto側で補います。
left_clickがNoneを返すと、ClaudeにはClicked.のような短い確認だけが渡ります。クリックの結果を伝えたいときは、1行のテキストを返します。
TypeScript版は構造が少し違います。状態報告はコンストラクタのbrowserStateオプションとしてsuperに渡し、メンバーはprotected overrideのメソッドとして書きます。
class MyBrowser extends BetaAbstractBrowserToolset20260801 {
constructor(
private backend: Backend,
options: Omit<BetaBrowserToolsetOptions, "browserState"> = {}
) {
super({
...options,
browserState: () => ({
tabs: backend.tabs().map((tab) => ({
tab_id: tab.id, title: tab.title, url: tab.url,
active: tab.id === backend.active
})),
state_changes: backend.drainChanges()
})
});
}
protected override async screenshot(
ctx: BetaToolsetCallContext,
input: BetaBrowserScreenshotInput
): Promise<BetaScreenshotResult> {
return { data: await this.backend.pngBase64(input.tab_id), mediaType: "image/png" };
}
}メンバーはアロー関数のフィールドではなくメソッドで書きます。SDKがプロトタイプ上でメンバーを探すためです。入力にtypeメンバーがある場合、TypeScriptではtype_と綴ります。Pythonはtypeのままです。
実行する — tool runnerにインスタンスを渡す
ドライバーのインスタンスは、そのままtoolsの要素として渡します。
client = Anthropic()
with MyBrowser(backend, url_policy=url_policy) as browser:
runner = client.beta.messages.tool_runner(
model="claude-opus-5-5",
max_tokens=1024,
tools=[browser],
messages=[{"role": "user", "content": "Open example.com and tell me the page heading."}],
stream=True,
run_tools_eagerly=True,
)
for stream in runner:
print(stream.get_final_message())tool runnerはツールセットを閉じません。1つのインスタンスを複数の実行で使い回せるので、終わったらwith文(TypeScriptではfinallyのclose())で自分で閉じます。
url_policyはここでは渡すだけにとどめます。SDKはURLポリシーを同梱せず、自分で書いて渡さないと、navigateのURLは誰も検査しません。書き方は後の「動かす前に」で触れます。
実装していないメンバーはどう扱われるか — configsで有効・無効を決める
BetaAbstractBrowserToolset20260801のメンバーのうち、クラスで実装していないものは、APIへ無効として送られます。Claudeがそれでも呼び出した場合、SDKはエラーを返し、実行は続きます。上の最小例なら、3メンバーだけがClaudeに提示される構成です。
メンバーごとの有効・無効はconfigsで指定します。変えたいメンバーだけを書けば足ります。
# read_console も実装した MyBrowser の場合
browser = MyBrowser(
backend,
configs={"read_console": {"enabled": True}, "navigate": {"enabled": False}},
)無効なメンバーへの呼び出しは、自分のコードが動く前にSDKが拒否します。一方、実装していないメンバーを有効にすると構成エラーです。この構成エラーが免除されるのは、クラスがexecuteをオーバーライドしている場合だけです。
既定で無効の4メンバー(javascript_exec・file_upload・read_console・read_network)を有効にするときの条件と危険性は、Browser use toolを有効化にまとめています。SDK側の追加条件は、javascript_execかfile_uploadを有効にするならconfirmコールバックが必須になる点です。渡さないとコンストラクタが構成エラーを出します。
呼び出しの前後に処理を挟む — executeのオーバーライド
ログ出力や結果の加工は、executeをオーバーライドして親のexecuteを呼ぶ形で書きます。親を呼ぶ前のコードはURLポリシー、ファイルポリシー、confirmのあとに動き、入力を書き換えられます。ただし書き換えた入力は、SDKが検査し直しません。親を呼んだ後のコードは結果を受け取って、これを書き換えられます。呼び出しを拒否したいときはToolErrorを送出します。
import time
class TracedBrowser(MyBrowser):
def execute(self, context, name, input):
started = time.monotonic()
result = super().execute(context, name, input)
elapsed_ms = (time.monotonic() - started) * 1000
call_id = context.tool_use.id if context.tool_use else "-"
log.info("%s %s %.0fms", call_id, name, elapsed_ms)
return redact(result) if name == "get_page_text" else resultここに落とし穴があります。executeを上書きしたクラスでは、SDKがすべてのメンバーを実装済みとして数えます。その結果、既定で有効な27メンバーがすべてClaudeに提示されます。最小例のMyBrowserは3メンバーしか動かないため、TracedBrowserをそのまま使うと、処理できないメンバーまでClaudeに見せてしまいます。実装していないメンバーはconfigsで{"enabled": False}にしてから使います。
結果の加工は、メンバーが何を返すかで意味が変わります。返り値とClaudeの読むものの対応は次の表のとおりです。
メンバーが返す値でClaudeの読むものが変わる
メンバーの戻り値は、そのままClaudeが読む内容を決めます。成功した結果には、状態報告から作られたbrowser_stateブロックが末尾に付きます。エラー結果にはこのブロックは付きません。
| メンバー | 返すもの | Claudeが読むもの |
|---|---|---|
screenshot、zoom | 返すものBetaScreenshotResult | Claudeが読むもの画像ブロック1つ |
navigate | 返すものBetaBrowserNavigateResult | Claudeが読むものNavigated to {url} — {title} (HTTP {status}) |
new_tab、switch_tab、list_tabs、close_tab | 返すものタブ情報、タブ情報のリスト、または何も返さない | Claudeが読むものbrowser_stateブロックのみ |
read_page、get_page_text、find、read_console、read_network、javascript_exec | 返すもの文字列 | Claudeが読むものその文字列(空文字列なら(empty)) |
| 上記以外 | 返すもの何も返さない、または1行のテキスト | Claudeが読むものClicked.のような短い確認。1行返すと別のテキストブロックで続く |
_browser_stateに何を入れるか
_browser_stateは、結果を返した呼び出しのたびにSDKが呼びます。拒否された呼び出しと失敗した呼び出しも含みます。返すのは、開いているすべてのタブと、前回の報告からの変化です。
state_changesに入れるのは次のものです。
- 新しく開いたタブとダウンロードのイベント
- 自分のリクエストフックがブロックしたナビゲーションごとの
BetaNavigationRefused - ドライバーが閉じたネイティブダイアログごとの
BetaDialogDismissed
タブが1つでも開いているなら、アクティブなタブはちょうど1つにします。tab_idを受け取るメンバーは、必ずそのタブに対して動かします。この報告を受けたAPI側の制約と、タブ管理メンバーの結果の形はbrowser use toolのbrowser_stateが詳しいです。
注意したいのは、URLの扱いです。タブのURL、ダウンロードのURL、navigateが返すURLは、ドライバーが書いたままClaudeに渡ります。SDKはURLを解釈せず、改行などの制御文字を空白に置き換え、前後を整え、4,096文字で切るだけです。そのためdata: URLの中身、file: URLのパス、URLに含まれるユーザー名やパスワードもClaudeに届きます。
エラーを返す — ToolErrorかそれ以外か
メンバーの中でどの例外を出すかで、Claudeの見るものと実行の続き方が変わります。
| 送出されるもの | Claudeが読むもの | 実行 |
|---|---|---|
ToolError | Claudeが読むものそのメッセージ(エラー結果として) | 実行続く |
ToolsetUsageError | Claudeが読むもの何も読まない | 実行止まる |
| それ以外の例外 | Claudeが読むものPythonではClassName: message、TypeScriptではError: message(エラー結果として) | 実行続く |
ToolsetUsageErrorは、構成エラー、呼び出し中のSDKの誤用、close後の呼び出しでSDKが出します。_browser_stateが例外を出したときも、それがToolErrorであってもToolsetUsageErrorになります。tool runnerとtool_resultはこれを捕まえず、呼び出し元までそのまま上がります。
SDKはメンバーのエラーテキストやleft_clickのような動作が返す1行の中のローカルパスを隠しません。それ以外の例外はクラス名とメッセージがそのままClaudeに渡るため、ドライバー側で例外を捕まえ、パスを含まない文面のToolErrorに包み直します。URLポリシー、ファイルポリシー、confirmが出すToolErrorも書いた文面のまま届くので、同じ配慮が要ります。
tool runnerなしで回す
自前のループで往復を管理するなら、インスタンスをtoolsに渡し(TypeScriptではbrowser.toJSON())、各呼び出しにtool_result(TypeScriptではtoolResult)で答えます。
押さえるべき規則は1つです。browser use toolでは、1ターンの中で最初に失敗した呼び出しで止める必要があります。失敗以降の呼び出しは実行せず、is_error付きのtool_resultで答えます。文面はNot executed: an earlier action in this turn failed.と決まっています。tool runnerも同じ答えを返します。
NOT_EXECUTED = "Not executed: an earlier action in this turn failed."
results = []
failed = False
for call in calls: # calls は browser.toolset_name に一致する tool_use ブロック
if failed:
results.append({
"type": "tool_result",
"tool_use_id": call.id,
"toolset_name": call.toolset_name,
"content": NOT_EXECUTED,
"is_error": True,
})
continue
result = browser.tool_result(call)
failed = bool(result.get("is_error"))
results.append(result)callsは、レスポンスのtool_useブロックのうちtoolset_nameがbrowser.toolset_nameと一致するものを集めたリストです。1ターンに複数の操作を詰めさせる仕組みはBrowser use toolのバッチアクションで説明しています。
ストリーミング中に呼び出しを始める — 早期開始
tool runnerは通常、Claudeのレスポンスが終わってから呼び出しを実行します。早期開始を有効にすると、レスポンスのストリーミング中にブラウザ操作を始められます。有効にするには、tool runnerにstream=Trueとrun_tools_eagerly=True(TypeScriptではstream: trueとrunToolsEagerly: true)を渡します。streamを付けると、ループが受け取るのはメッセージではなくストリームになります。
変わるのは開始時刻だけです。
- URLポリシー、ファイルポリシー、
confirmは、呼び出しの前にこれまでどおり動く。confirmはストリーミング中に呼ばれうる - 1つのツールセットの呼び出しは、書かれた順に1つずつ動く。失敗した呼び出しがあれば、同じターンの後続は止まる
- 始まった呼び出しは取り消せない。
max_tokensでレスポンスが途切れた場合やループを途中で止めた場合も、操作は実行され、Claudeがその結果を読むことはない
動かす前に — 使い捨てでないブラウザでは安全策が要る
最小例を使い捨てのブラウザ以外に向ける前に、最低限次の2つを済ませます。
- URLポリシーを書く。ポリシーがなければSDKはURLを一切検査せず、APIもClaudeが開くURLを絞りません。SDKはスキームも検査しません。
javascript:、view-source:、data:、file:は、ドライバー側で拒否する必要があります。file:はホスト上のファイルの読み取りに、javascript:はjavascript_execが無効でもページ上のスクリプト実行につながるためです。 - コンテナのegressルールで、プライベートネットワークの範囲とクラウドのメタデータアドレス
169.254.169.254を塞ぐ。また、アカウントにログインしていないブラウザプロファイルを使う。
URLポリシーが見るのはnavigateのURLだけです。Claudeがクリックしたリンク、フォームの送信、リダイレクトは対象外です。リクエストフック(Playwrightならcontext.route)で同じ判定関数を再利用し、ネットワークの層では別にegressを絞る、という三層になります。
ポリシーの書き方、アップロードとダウンロードの範囲設定、confirmの実装は、ドライバーの先にある運用の話で、Browser use toolのセキュリティ対策6原則が6つの対策の観点で整理しています。
使える条件と制約
SDKの経路にも、browser use tool自体の条件が掛かります。
- 提供面はClaude APIとGoogle Cloud。BedrockなどではSDK経由でも使えない
- 1つのツールセットの呼び出しは1つずつ実行され、並列化の設定はない
- 承認は直近の状態報告に基づく。承認後にページが変わっても、SDKは実行前にページを確認し直さない
- タブIDの重複やタブ数はSDKが検査せず、アクティブなタブが1つかどうかも常には確認しない。違反した報告はAPIが拒否する
- 既定のメンバー定義で、1リクエストに約6,600入力トークンが加わる(
configsで無効にした分は減る)
同じSDKには、デスクトップ用のBetaAbstractComputerToolset20260801もあります。構成と書き方は同じで、受け付けるオプションはconfigs・confirm・tool_configsの3つです。URLポリシー、ファイルポリシー、状態報告はありません。両方をtools=[browser, desktop]と並べて渡すことも可能です。
まとめ
ドライバーの実装は、継承、3メンバー、状態報告の4点から始まります。実装していないメンバーは自動で無効になりますが、executeを上書くとその仕組みが外れ、configsでの無効化が必須になります。失敗の扱い、早期開始、confirmの要否は、いずれもSDKが既定で処理するので、ドライバーは「ブラウザに何をさせるか」に集中できます。ただしURLポリシーとegress制御はSDKに含まれないため、使い捨てでないブラウザに向ける前に自分で用意します。