브라우저 사용 도구
브라우저 사용 도구 (Browser use tool)
브라우저 사용 도구는 Claude가 여러분의 애플리케이션이 실행하는 브라우저에서 웹페이지를 탐색하고, 읽고, 상호작용하게 해줘요. Claude는 구조(접근성 트리, 요소, 폼, 탭)와 스크린샷·뷰포트 좌표 두 가지를 통해 페이지와 함께 작업해요.
이 도구는 Anthropic이 정의한 클라이언트 도구셋이에요. tools에 browser_toolset_20260801 항목 하나를 추가하면 Claude가 기본적으로 27개의 구성원(member) 도구를 받게 돼요. navigate, read_page, left_click, screenshot 같은 도구들이죠. 네 개를 더 활성화할 수도 있어요. 여러분의 애플리케이션이 모든 호출을 자체 브라우저 자동화로 처리하고, Anthropic 쪽에서는 아무것도 실행되지 않아요. 이 도구는 현재 Claude Managed Agents에서는 사용할 수 없어요.
작업이 웹페이지 안에 머물면서 그 페이지에 무언가를 하는 일이거나, 페이지가 JavaScript로 내용을 만든다면 브라우저 사용을 선택하세요. 작업이 전체 데스크톱을 필요로 할 때는 컴퓨터 사용 도구를 쓰는데, 이 도구는 스크린샷과 좌표만으로 작동해요. Claude가 가리켜줄 페이지를 읽거나 웹에서 출처를 찾는 작업이라면 웹 페치 도구와 웹 검색 도구가 더 가벼워요. 이 둘은 API가 대신 실행해주는 서버 도구라서 조작할 브라우저가 필요 없어요.
브라우저 사용에서는 Claude가 라이브 웹페이지를 읽고 그 위에서 행동하기 때문에, 페이지가 제공하는 모든 것이 신뢰할 수 없는 입력이고 Claude가 취하는 행동은 실제 영향을 줄 수 있어요. 배포 전에 보안 고려 사항을 꼭 확인해주세요.
출처: 문서
본문
빠른 시작
브라우저 사용 도구는 Claude API와 Google Cloud에서 사용할 수 있어요. Messages API 요청의 tools 배열에 name 없이 browser_toolset_20260801 타입 항목 하나만 추가하면 돼요.
ant messages create <<'YAML'
model: claude-opus-5-5
max_tokens: 2048
tools:
- type: browser_toolset_20260801
messages:
- role: user
content: Open example.com/docs and tell me how to get started.
YAML
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=2048,
tools=[{"type": "browser_toolset_20260801"}],
messages=[
{
"role": "user",
"content": "Open example.com/docs and tell me how to get started.",
}
],
)
print(response)
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 2048,
tools: [{ type: "browser_toolset_20260801" }],
messages: [
{
role: "user",
content: "Open example.com/docs and tell me how to get started."
}
]
});
console.log(response);
var client = new AnthropicClient();
var parameters = new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 2048,
Tools = [new BrowserToolset20260801()],
Messages =
[
new MessageParam
{
Role = Role.User,
Content = "Open example.com/docs and tell me how to get started.",
},
],
};
var response = await client.Messages.Create(parameters);
Console.WriteLine(response);
client := anthropic.NewClient()
response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 2048,
Tools: []anthropic.ToolUnionParam{
{OfBrowserToolset20260801: &anthropic.BrowserToolset20260801Param{}},
},
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Open example.com/docs and tell me how to get started.")),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.RawJSON())
import com.anthropic.models.messages.BrowserToolset20260801;
// ...
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(2048L)
.addTool(BrowserToolset20260801.builder().build())
.addUserMessage("Open example.com/docs and tell me how to get started.")
.build();
Message response = client.messages().create(params);
IO.println(response);
}
$client = new Client();
$response = $client->messages->create(
maxTokens: 2048,
messages: [
['role' => 'user', 'content' => 'Open example.com/docs and tell me how to get started.'],
],
model: 'claude-opus-5-5',
tools: [
['type' => 'browser_toolset_20260801'],
],
);
echo $response;
client = Anthropic::Client.new
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 2048,
tools: [
{ type: "browser_toolset_20260801" }
],
messages: [
{
role: "user",
content: "Open example.com/docs and tell me how to get started."
}
]
)
puts response
Claude의 첫 응답은 stop_reason: "tool_use"로 끝나고 name에 구성원 도구 이름을 담은 tool_use 블록을 하나 이상 포함하며, 각 블록에는 "toolset_name": "browser"가 붙어요:
{
"id": "msg_01HCDu4XSTLzTAcodEQ58vDo",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{
"type": "text",
"text": "I'll open the documentation and read the page to find the getting-started instructions."
},
{
"type": "tool_use",
"id": "toolu_01NRLabsLyVHZPKxbKvkfSMn",
"name": "navigate",
"toolset_name": "browser",
"input": { "url": "https://example.com/docs" }
},
{
"type": "tool_use",
"id": "toolu_01UvHU5cDyTZ2vXKf5wCkPqR",
"name": "read_page",
"toolset_name": "browser",
"input": { "filter": "interactive" }
}
],
"stop_reason": "tool_use",
"stop_sequence": null
}
여러분의 실행기(executor, 브라우저를 구동하고 도구 결과를 만드는 애플리케이션의 부분)는 navigate를 실행하고 이어서 read_page를 실행해요. 애플리케이션은 다음 요청에서 블록당 하나씩 tool_result를 돌려주고, 각각에 toolset_name을 그대로 담아요. navigate 결과는 로드된 탭을 browser_state 블록에 보고하고, read_page 결과는 각 요소에 참조가 붙은 텍스트예요:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01NRLabsLyVHZPKxbKvkfSMn",
"toolset_name": "browser",
"content": [
{ "type": "text", "text": "Navigated to https://example.com/docs" },
{
"type": "browser_state",
"tabs": [
{
"tab_id": "tab-1",
"title": "Documentation",
"url": "https://example.com/docs",
"active": true
}
]
}
]
},
{
"type": "tool_result",
"tool_use_id": "toolu_01UvHU5cDyTZ2vXKf5wCkPqR",
"toolset_name": "browser",
"content": [
{
"type": "text",
"text": "link \"Documentation\" [ref_1]\nlink \"Getting started\" [ref_2]\ntextbox \"Search docs\" [ref_3]\nbutton \"Search\" [ref_4]\nlink \"Pricing\" [ref_5]"
}
]
}
]
}
이제 Claude는 행동할 수 있는 참조를 갖게 되므로 다음 턴에서 ref_2를 클릭해 시작하기 페이지를 열 수 있어요. 링크를 스크린샷에서 먼저 찾을 필요가 없죠.
브라우저 사용이 어떻게 작동하는지
브라우저 사용은 애플리케이션에서 에이전트 루프로 실행돼요. Claude가 구성원 도구 호출을 돌려주면 여러분의 실행기가 브라우저에서 실행하고, Claude가 텍스트로 답할 때까지 결과를 돌려주는 식이에요.
이 루프의 도구 호출 단계 뼈대는 두 부분으로 나뉘어요. 먼저 스텁 구성원 핸들러가 브라우저 자동화를 대신해요. 다섯 구성원(navigate, read_page, left_click, type, screenshot)이 결과 내용이 되는 텍스트(screenshot은 이미지 블록)를 돌려주고, 디스패처는 구현하지 않은 구성원에 대해 오류를 던져요.
def navigate(url): return f"navigated to {url}"
def read_page(): return 'link "Docs" [ref_1]\nbutton "Search" [ref_2]'
def click(target): # A target is an element reference from read_page or find, or a viewport coordinate if target["type"] == "ref": return f"clicked {target['ref']}" return f"clicked at ({target['x']}, {target['y']})"
def type_text(text): return f"typed: {text}"
def capture_screenshot() -> list[ImageBlockParam]: # screenshot answers with an image block rather than text: return the result content list return [ { "type": "image", "source": {"type": "base64", "media_type": "image/png", "data": PLACEHOLDER_PNG}, } ]
def handle_browser_action(name, tool_input): if name == "navigate": return navigate(tool_input["url"]) elif name == "read_page": return read_page() elif name == "left_click": return click(tool_input["target"]) elif name == "type": return type_text(tool_input["text"]) elif name == "screenshot": return capture_screenshot() # Handle other actions as needed raise ValueError(f"Unknown or unimplemented member: {name}")
```typescript TypeScript
// Placeholder image data; a real executor captures the viewport as PNG bytes
const PLACEHOLDER_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
function navigate(url: string): string {
return `navigated to ${url}`;
}
function readPage(): string {
return 'link "Docs" [ref_1]\nbutton "Search" [ref_2]';
}
function clickElement(ref: string): string {
return `clicked ${ref}`;
}
function clickAt(x: number, y: number): string {
return `clicked at (${x}, ${y})`;
}
function typeText(text: string): string {
return `typed: ${text}`;
}
function captureScreenshot(): Anthropic.ImageBlockParam[] {
// screenshot answers with an image block rather than text
return [
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: PLACEHOLDER_PNG,
},
},
];
}
function handleBrowserAction(
action: string,
input: unknown,
): string | Anthropic.ImageBlockParam[] {
const params: object =
typeof input === "object" && input !== null ? input : {};
if (action === "navigate" && "url" in params) {
return navigate(String(params.url));
} else if (action === "read_page") {
return readPage();
} else if (action === "left_click" && "target" in params) {
// target is an element reference from read_page or a viewport coordinate
const target: object =
typeof params.target === "object" && params.target !== null
? params.target
: {};
if ("type" in target && target.type === "ref" && "ref" in target) {
return clickElement(String(target.ref));
} else if ("x" in target && "y" in target) {
return clickAt(Number(target.x), Number(target.y));
}
} else if (action === "type" && "text" in params) {
return typeText(String(params.text));
} else if (action === "screenshot") {
return captureScreenshot();
}
// Handle other actions as needed
throw new Error(`Unknown or unimplemented member: ${action}`);
}
// Placeholder image data; a real executor captures the viewport and returns the PNG bytes
const string PlaceholderPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
string Navigate(string url) => $"navigated to {url}";
string ReadPage() =>
"""
link "Docs" [ref_1]
button "Search" [ref_2]
""";
string ClickRef(string elementRef) => $"clicked {elementRef}";
string ClickAt(int x, int y) => $"clicked at ({x}, {y})";
// target is {"type": "ref", "ref": "ref_1"} or {"type": "coordinate", "x": 640, "y": 380}
string Click(JsonElement target) =>
target.GetProperty("type").GetString() == "ref"
? ClickRef(target.GetProperty("ref").GetString()!)
: ClickAt(target.GetProperty("x").GetInt32(), target.GetProperty("y").GetInt32());
string TypeText(string text) => $"typed: {text}";
// screenshot answers with an image block rather than text: return the result content list
List<Block> CaptureScreenshot() =>
[
new ImageBlockParam(
new Base64ImageSource { Data = PlaceholderPng, MediaType = MediaType.ImagePng }
),
];
ToolResultBlockParamContent HandleBrowserAction(
string action,
IReadOnlyDictionary<string, JsonElement> input
) =>
action switch
{
"navigate" => Navigate(input["url"].GetString()!),
"read_page" => ReadPage(),
"left_click" => Click(input["target"]),
"type" => TypeText(input["text"].GetString()!),
"screenshot" => CaptureScreenshot(),
// Handle other actions as needed
_ => throw new NotSupportedException($"Unknown or unimplemented member: {action}"),
};
// placeholderPNG stands in for a real capture: an executor returns the
// viewport as base64-encoded PNG data.
const placeholderPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
// textContent wraps text as tool_result content.
func textContent(text string) []anthropic.ToolResultBlockParamContentUnion {
return []anthropic.ToolResultBlockParamContentUnion{
{OfText: &anthropic.TextBlockParam{Text: text}},
}
}
func navigate(url string) string {
return fmt.Sprintf("navigated to %s", url)
}
func readPage() string {
return "link \"Docs\" [ref_1]\nbutton \"Search\" [ref_2]"
}
func clickRef(ref string) string {
return fmt.Sprintf("clicked %s", ref)
}
func clickAt(x, y int) string {
return fmt.Sprintf("clicked at (%d, %d)", x, y)
}
func typeText(text string) string {
return fmt.Sprintf("typed: %s", text)
}
// captureScreenshot returns an image block rather than text.
func captureScreenshot() []anthropic.ToolResultBlockParamContentUnion {
return []anthropic.ToolResultBlockParamContentUnion{{
OfImage: &anthropic.ImageBlockParam{
Source: anthropic.ImageBlockParamSourceUnion{
OfBase64: &anthropic.Base64ImageSourceParam{
MediaType: anthropic.Base64ImageSourceMediaTypeImagePNG,
Data: placeholderPNG,
},
},
},
}}
}
func handleBrowserAction(action string, params map[string]any) ([]anthropic.ToolResultBlockParamContentUnion, error) {
switch action {
case "navigate":
if url, ok := params["url"].(string); ok {
return textContent(navigate(url)), nil
}
case "read_page":
return textContent(readPage()), nil
case "left_click":
// target is either an element reference from read_page or a viewport coordinate
target, _ := params["target"].(map[string]any)
if ref, ok := target["ref"].(string); ok && target["type"] == "ref" {
return textContent(clickRef(ref)), nil
}
x, xok := target["x"].(float64)
y, yok := target["y"].(float64)
if xok && yok {
return textContent(clickAt(int(x), int(y))), nil
}
case "type":
if text, ok := params["text"].(string); ok {
return textContent(typeText(text)), nil
}
case "screenshot":
return captureScreenshot(), nil
// Handle other actions as needed
default:
return nil, fmt.Errorf("unknown or unimplemented member: %s", action)
}
// Reached when a member's input is missing a field or a field has the wrong type
return nil, fmt.Errorf("invalid input for %s", action)
}
/** Placeholder pixels; a real executor captures the viewport and base64-encodes the PNG. */
static final String PLACEHOLDER_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
ToolResultBlockParam.Content captureScreenshot() {
ImageBlockParam image = ImageBlockParam.builder()
.source(Base64ImageSource.builder()
.mediaType(Base64ImageSource.MediaType.IMAGE_PNG)
.data(PLACEHOLDER_PNG)
.build())
.build();
return ToolResultBlockParam.Content.ofBlocks(
List.of(ToolResultBlockParam.Content.Block.ofImage(image)));
}
String navigate(String url) {
return "navigated to " + url;
}
String readPage() {
return """
link "Docs" [ref_1]
button "Search" [ref_2]""";
}
String clickRef(String ref) {
return "clicked " + ref;
}
String clickAt(long x, long y) {
return "clicked at (" + x + ", " + y + ")";
}
String typeText(String text) {
return "typed: " + text;
}
/** Runs one browser toolset member; {@code action} is the tool_use block's name. */
ToolResultBlockParam.Content handleBrowserAction(String action, Map<String, JsonValue> input) {
if (action.equals("screenshot")) {
return captureScreenshot(); // the one member here that answers with an image block
}
String output = switch (action) {
case "navigate" -> navigate(input.get("url").asStringOrThrow());
case "read_page" -> readPage();
case "left_click" -> {
// target is {"type": "ref", "ref": "ref_1"} or {"type": "coordinate", "x": 640, "y": 380}
Map<String, JsonValue> target =
(Map<String, JsonValue>) input.get("target").asObject().get();
if (target.get("type").asStringOrThrow().equals("ref")) {
yield clickRef(target.get("ref").asStringOrThrow());
}
long x = ((Number) target.get("x").asNumber().get()).longValue();
long y = ((Number) target.get("y").asNumber().get()).longValue();
yield clickAt(x, y);
}
case "type" -> typeText(input.get("text").asStringOrThrow());
// Handle other actions as needed
default -> throw new UnsupportedOperationException("Unknown or unimplemented member: " + action);
};
return ToolResultBlockParam.Content.ofString(output);
}
// Stand-in for real PNG bytes; a real executor captures the viewport
const PLACEHOLDER_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
function navigateTo(string $url): string
{
return "navigated to {$url}";
}
function readPage(): string
{
return <<<'TEXT'
link "Docs" [ref_1]
button "Search" [ref_2]
TEXT;
}
function clickTarget(array $target): string
{
// A target is an element reference from read_page or find, or a viewport pixel coordinate
if ($target['type'] === 'ref') {
return "clicked {$target['ref']}";
}
return "clicked at ({$target['x']}, {$target['y']})";
}
function typeText(string $text): string
{
return "typed: {$text}";
}
function captureScreenshot(): array
{
// screenshot answers with an image block rather than text, so return the result content list
$image = [
'type' => 'image',
'source' => ['type' => 'base64', 'media_type' => 'image/png', 'data' => PLACEHOLDER_PNG],
];
return [$image];
}
function handleBrowserAction(string $name, array $input): string|array
{
return match ($name) {
'navigate' => navigateTo($input['url']),
'read_page' => readPage(),
'left_click' => clickTarget($input['target']),
'type' => typeText($input['text']),
'screenshot' => captureScreenshot(),
// Handle other actions as needed
default => throw new RuntimeException("Unknown or unimplemented member: {$name}"),
};
}
# Stand-in image data; a real executor captures the viewport as a PNG.
PLACEHOLDER_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
def navigate(url)
"navigated to #{url}"
end
def read_page
<<~TREE
link "Docs" [ref_1]
button "Search" [ref_2]
TREE
end
def click(target)
return "clicked #{target[:ref]}" if target[:type] == "ref"
"clicked at (#{target[:x]}, #{target[:y]})"
end
def type_text(text)
"typed: #{text}"
end
def capture_screenshot
[
{
type: "image",
source: { type: "base64", media_type: "image/png", data: PLACEHOLDER_PNG }
}
]
end
def handle_browser_action(name, input)
case name
when "navigate"
navigate(input[:url])
when "read_page"
read_page
when "left_click"
# target is an element reference (from read_page or find) or a coordinate
click(input[:target])
when "type"
type_text(input[:text])
when "screenshot"
capture_screenshot
# Handle other actions as needed
else
raise ArgumentError, "Unknown or unimplemented member: #{name}"
end
end
두 번째 부분은 배치를 순서대로 실행하고, 각 블록을 그 핸들러로 디스패치하며, 모든 결과에 toolset_name을 그대로 담고, 배치 행동의 정지 규칙을 적용해서 핸들러 오류를 오류 결과로 바꿔요. 이 함수를 호출하는 샘플링 루프는 에이전트 루프 이해하기에 나온 것과 같고, tools에 브라우저 도구셋을 넣어요.
def process_tool_calls(response: Message) -> list[ToolResultBlockParam]: """ Run the browser actions in Claude's response in order and answer each one. After the first failure the rest are skipped, because Claude planned them assuming the earlier actions succeeded. """ tool_results: list[ToolResultBlockParam] = [] failed = False for block in response.content: # Only the browser toolset is declared; route other tools here if you add them if block.type != "tool_use" or block.toolset_name != "browser": continue result: ToolResultBlockParam = { "type": "tool_result", "tool_use_id": block.id, "toolset_name": "browser", } if failed: result["content"] = NOT_EXECUTED result["is_error"] = True else: try: # A string or a list of content blocks; a real executor also adds a # browser_state block to navigation and tab-management results result["content"] = handle_browser_action(block.name, block.input) except Exception as err: result["content"] = f"Error: {err}" result["is_error"] = True failed = True tool_results.append(result) return tool_results
```typescript TypeScript
const HALT_TEXT = "Not executed: an earlier action in this turn failed.";
function browserResult(
toolUseId: string,
content: string | Anthropic.ImageBlockParam[],
isError?: boolean,
): Anthropic.ToolResultBlockParam {
return {
type: "tool_result",
tool_use_id: toolUseId,
toolset_name: "browser",
content,
is_error: isError,
};
}
function processToolCalls(
response: Anthropic.Message,
): Anthropic.ToolResultBlockParam[] {
const toolResults: Anthropic.ToolResultBlockParam[] = [];
let failed = false;
for (const block of response.content) {
if (block.type !== "tool_use") {
continue;
}
if (block.toolset_name !== "browser") {
// This example declares only the browser toolset; route other tools
// here if you add them.
continue;
}
if (failed) {
// A batch stops at its first failure; answer later actions unexecuted
toolResults.push(browserResult(block.id, HALT_TEXT, true));
continue;
}
try {
// A string or an image block list; a real executor also adds a
// browser_state block to navigation and tab-management results
const result = handleBrowserAction(block.name, block.input);
toolResults.push(browserResult(block.id, result));
} catch (error) {
failed = true;
const message = error instanceof Error ? error.message : String(error);
toolResults.push(browserResult(block.id, `Error: ${message}`, true));
}
}
return toolResults;
}
const string HaltText = "Not executed: an earlier action in this turn failed.";
List<ContentBlockParam> ProcessToolCalls(Message response)
{
List<ContentBlockParam> toolResults = [];
var failed = false;
foreach (var block in response.Content)
{
if (!block.TryPickToolUse(out var toolUse))
{
continue;
}
if (toolUse.ToolsetName != "browser")
{
// This example declares only the browser toolset; route other tools
// here if you add them.
continue;
}
if (failed)
{
// A batch stops at its first failure; answer later actions without running them
toolResults.Add(
new ToolResultBlockParam(toolUse.ID)
{
Content = HaltText,
IsError = true,
ToolsetName = "browser",
}
);
continue;
}
try
{
// A string or a list of content blocks; a real executor also adds a
// browser_state block to navigation and tab-management results
var result = HandleBrowserAction(toolUse.Name, toolUse.Input);
toolResults.Add(
new ToolResultBlockParam(toolUse.ID) { Content = result, ToolsetName = "browser" }
);
}
catch (Exception e)
{
failed = true;
toolResults.Add(
new ToolResultBlockParam(toolUse.ID)
{
Content = $"Error: {e.Message}",
IsError = true,
ToolsetName = "browser",
}
);
}
}
return toolResults;
}
const notExecuted = "Not executed: an earlier action in this turn failed."
// browserToolResult builds the result for one browser action. Unlike an
// ordinary tool result, it must echo the toolset name. A real executor also
// adds a browser_state block to navigation and tab-management results.
func browserToolResult(toolUseID string, content []anthropic.ToolResultBlockParamContentUnion, isError bool) anthropic.ContentBlockParamUnion {
result := anthropic.ToolResultBlockParam{
ToolUseID: toolUseID,
ToolsetName: anthropic.String("browser"),
Content: content,
}
if isError {
result.IsError = anthropic.Bool(true)
}
return anthropic.ContentBlockParamUnion{OfToolResult: &result}
}
// processToolCalls runs the browser actions in Claude's response in order and
// builds one tool_result per tool_use block. After the first failure it skips
// the rest: Claude planned them assuming the earlier actions succeeded.
func processToolCalls(response *anthropic.Message) []anthropic.ContentBlockParamUnion {
var toolResults []anthropic.ContentBlockParamUnion
failed := false
for _, block := range response.Content {
switch variant := block.AsAny().(type) {
case anthropic.ToolUseBlock:
// This example declares only the browser toolset; route other tools here if you add them.
if variant.ToolsetName != "browser" {
continue
}
if failed {
toolResults = append(toolResults, browserToolResult(variant.ID, textContent(notExecuted), true))
continue
}
var input map[string]any
var content []anthropic.ToolResultBlockParamContentUnion
err := json.Unmarshal(variant.Input, &input)
if err == nil {
content, err = handleBrowserAction(variant.Name, input)
}
if err != nil {
failed = true
content = textContent("Error: " + err.Error())
}
toolResults = append(toolResults, browserToolResult(variant.ID, content, err != nil))
}
}
return toolResults
}
/** The exact text the toolset contract prescribes for member calls skipped after a failure. */
static final String HALT_TEXT = "Not executed: an earlier action in this turn failed.";
/** Every result answering a browser toolset member echoes toolset_name. */
ToolResultBlockParam.Builder browserResult(ToolUseBlock toolUse) {
return ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
.toolsetName("browser");
}
/**
* Run the browser actions in Claude's response in order and build one
* tool_result per tool_use block. After the first failure, skip the rest:
* Claude planned them assuming the earlier actions succeeded.
*/
List<ContentBlockParam> processToolCalls(Message response) {
List<ContentBlockParam> toolResults = new ArrayList<>();
boolean failed = false;
for (ContentBlock block : response.content()) {
// This example declares only the browser toolset; route other tools here if you add them.
if (!block.isToolUse() || !block.asToolUse().toolsetName().equals(Optional.of("browser"))) {
continue;
}
ToolUseBlock toolUse = block.asToolUse();
ToolResultBlockParam result;
if (failed) {
result = browserResult(toolUse).content(HALT_TEXT).isError(true).build();
} else {
try {
Map<String, JsonValue> input =
(Map<String, JsonValue>) toolUse._input().asObject().get();
ToolResultBlockParam.Content output = handleBrowserAction(toolUse.name(), input);
// A real executor also adds a browser_state block to navigation and
// tab-management results; see "Track tabs with browser_state" on this page.
result = browserResult(toolUse).content(output).build();
} catch (RuntimeException e) {
failed = true;
result = browserResult(toolUse).content("Error: " + e.getMessage()).isError(true).build();
}
}
toolResults.add(ContentBlockParam.ofToolResult(result));
}
return toolResults;
}
const HALT_TEXT = 'Not executed: an earlier action in this turn failed.';
function processToolCalls(Message $response): array
{
$toolResults = [];
$failed = false;
foreach ($response->content as $block) {
// This example declares only the browser toolset; route other tools here if you add them.
if (!($block instanceof \Anthropic\Messages\ToolUseBlock) || $block->toolsetName !== 'browser') {
continue;
}
$result = ['type' => 'tool_result', 'tool_use_id' => $block->id, 'toolset_name' => 'browser'];
if ($failed) {
// A batch stops at its first failure; the remaining actions are answered without running
$toolResults[] = [...$result, 'content' => HALT_TEXT, 'is_error' => true];
continue;
}
try {
// A real executor also returns a browser_state block on navigation and tab-management results
$toolResults[] = [...$result, 'content' => handleBrowserAction($block->name, $block->input)];
} catch (Throwable $e) {
$failed = true;
$toolResults[] = [...$result, 'content' => 'Error: ' . $e->getMessage(), 'is_error' => true];
}
}
return $toolResults;
}
NOT_EXECUTED = "Not executed: an earlier action in this turn failed."
# Run the browser actions in Claude's response in order and build one
# tool_result per tool_use block. After the first failure, skip the rest:
# Claude planned them assuming the earlier actions succeeded.
def process_tool_calls(response)
tool_results = []
failed = false
response.content.each do |block|
# This example declares only the browser toolset; route other tools here
# if you add them.
next unless block.type == :tool_use && block.toolset_name == "browser"
result = { type: "tool_result", tool_use_id: block.id, toolset_name: "browser" }
if failed
result.update(content: NOT_EXECUTED, is_error: true)
else
begin
# A String, or content blocks for a screenshot. A real executor also adds
# a browser_state block to navigation and tab-management results.
result[:content] = handle_browser_action(block.name, block.input)
rescue => e
result.update(content: "Error: #{e.message}", is_error: true)
failed = true
end
end
tool_results << result
end
tool_results
end
name만으로 디스패치하지 말고 (toolset_name, name) 쌍으로 디스패치하세요. 같은 요청의 커스텀 도구가 구성원과 이름을 공유할 수 있기 때문이에요. 클라이언트 도구셋은 두 도구셋이 공유하는 계약의 부분을 설명해요. Claude가 실행기가 구현하지 않았거나 비활성화한 구성원을 호명하면, 그 블록을 버리지 말고 오류 결과로 답하세요.
스트리밍할 때 각 구성원의 input은 조각이 아니라 완전한 input_json_delta 하나로 도착해요. 그러니 턴이 끝나기를 기다렸다가 배치를 실행하세요.
배치 행동
여러 구성원 호출이 있는 턴은 배치 행동이에요. 나타난 순서대로 호출을 실행하고, 첫 실패에서 멈추며, 이후 모든 호출에 is_error: true와 정확한 텍스트 Not executed: an earlier action in this turn failed.로 답해요. 배치는 병렬 도구 사용과 같은 응답 형태를 쓰는데, 차이는 블록을 동시에가 아니라 순서대로 실행한다는 점이에요. 여기 Claude는 한 턴에 이전에 찾은 검색 상자를 클릭하고, 쿼리를 입력하고, Enter를 누릅니다.
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01D7FLrfh4GYq7yT1ULFeyMV",
"name": "left_click",
"toolset_name": "browser",
"input": { "target": { "type": "ref", "ref": "ref_3" } }
},
{
"type": "tool_use",
"id": "toolu_01Ez4kLb1nQ2vXo8sJ9pWm3c",
"name": "type",
"toolset_name": "browser",
"input": { "text": "install" }
},
{
"type": "tool_use",
"id": "toolu_01FkP8rTz6uYh2mNq4LsXw7v",
"name": "key",
"toolset_name": "browser",
"input": { "text": "Enter" }
}
]
}
애플리케이션은 하나의 user 메시지에 tool_result 블록 세 개를 돌려주고, 각각 toolset_name과 Clicked element ref_3. 같은 짧은 텍스트 확인을 담아요. Enter를 누르면 결과 페이지가 로드되므로 key 결과는 탭의 갱신된 URL을 담은 browser_state 블록도 함께 실어요(다른 결과의 탭 컨텍스트). 클릭이 대신 실패했다면 클릭 결과는 오류 텍스트를, 나머지 두 결과는 실행기에서 오류 돌려주기에 나온 정지 텍스트를 담을 거예요.
모든 호출 후에 스크린샷을 돌려줄 필요는 없어요. Claude는 보통 배치를 관찰 호출(screenshot, read_page, get_page_text)로 끝내는데, 애플리케이션은 배치의 마지막 결과에 자체 관찰(새 스크린샷이나 접근성 트리 같은)을 추가 콘텐츠 블록으로 붙여 왕복을 아낄 수도 있어요. 탭 관리 결과는 정확히 browser_state 블록 하나여야 하므로, 탭 관리 호출이 아닌 마지막 결과에 붙이세요.
실행기가 한 번에 한 호출만 실행할 수 있다면 tool_choice에서 disable_parallel_tool_use를 true로 설정하세요. 그러면 Claude가 턴당 최대 하나의 구성원 호출을 돌려주지만, 왕복이 늘어나요(병렬 도구 사용 비활성화). 컴퓨터 사용 도구의 배치 행동의 나머지 계약은 그대로 이어져요. 다음 user 메시지의 모든 tool_use에 tool_result 하나씩인 것까지요. 단 두 가지가 달라요. 정지 텍스트와 성공 결과의 content가 담는 내용이죠. 결과 내용은 이 페이지의 구성원 도구를 따르는 대신, new_tab, switch_tab, close_tab, list_tabs 결과는 텍스트나 이미지 없이 정확히 browser_state 블록 하나이고(탭 관리 결과), 그 밖의 구성원 결과는 텍스트나 이미지에 browser_state 블록을 더할 수 있어요(다른 결과의 탭 컨텍스트). 배치 안의 캐시 중단점이 어디서 효과를 내는지는 컴퓨터 사용 도구의 도구 파라미터에서 cache_control 행에 설명되어 있어요.
대상과 좌표
위치에 행동하는 구성원 도구는 target 객체를 받아요. 이 객체는 뷰포트 픽셀 좌표이거나 read_page·find가 돌려준 요소에 대한 참조예요. 구성원 도구 표는 어떤 형태든 받는 파라미터에 Target이라고 써요.
| 형태 | target.type |
필드 | 허용되는 곳 |
|---|---|---|---|
CoordinateTarget |
"coordinate" |
x, y (정수, 뷰포트 픽셀) |
left_click, right_click, middle_click, double_click, triple_click, hover, left_click_drag (from과 target), left_mouse_down, left_mouse_up, mouse_move, scroll |
RefTarget |
"ref" |
ref ("ref_2" 같은 요소 참조) |
left_click, right_click, middle_click, double_click, triple_click, hover, scroll_to, form_input, file_upload |
좌표는 뷰포트 픽셀이에요. 렌더링된 페이지의 왼쪽 위를 원점으로 하는 전체 뷰포트 screenshot의 픽셀 공간이죠. 주변 데스크톱이나 창 프레임은 없어요. 도구셋은 디스플레이 크기를 선언하지 않으므로 Claude는 여러분이 돌려준 스크린샷에서 뷰포트 크기를 추론해요. 그래서 일관된 크기로 유지하세요. zoom은 프레임을 바꾸지 않으므로, 확대된 이미지를 본 뒤 Claude가 내는 region과 좌표도 여전히 전체 뷰포트 픽셀이에요.
스크린샷은 이미지 한도를 맞춰야 해요. API는 도구셋 이미지를 줄이지 않아요. 모델의 이미지 크기 한도를 넘는 스크린샷이나 확대 이미지, 또는 요청이 이미지 20개 이상을 담을 때 적용되는 더 엄격한 이미지당 한도를 넘는 이미지는 거부돼요. 돌려주기 전에 크기를 조정하고, dispatch 전에 Claude의 좌표를 조정 계수의 역수로 다시 키우세요(고해상도 좌표 스케일링 처리).
요소 참조는 read_page와 find에서 나와요. 이 둘의 출력의 각 요소는 퀵 스타트 결과처럼 [ref_2] 같은 태그를 담아요:
link "Documentation" [ref_1]
link "Getting started" [ref_2]
textbox "Search docs" [ref_3]
button "Search" [ref_4]
link "Pricing" [ref_5]
Claude는 나중에 클릭, hover, scroll_to, form_input, file_upload 호출에서 {"type": "ref", "ref": "ref_2"} 대상으로, 또는 하위 트리를 읽기 위해 read_page의 ref 파라미터로 참조를 다시 전달해요. 실행기는 참조를 할당하고, 각각을 기저 노드(접근성 노드 ID, 저장된 선택자 등)에 매핑해두고, 참조가 돌아오면 그 노드에 행동해요.
참조는 그것을 만든 탭으로 범위가 한정되고 그 탭이 탐색하거나 DOM이 크게 바뀔 때까지 유효해요. API는 낡았거나 알 수 없는 참조를 감지할 수 없으므로, Claude가 실행기가 더는 모르는 참조를 전달하면 Error: ref_3 is stale or not found on the current page. Re-read the page to get fresh references. 같은 오류 결과를 돌려주세요. 그러면 Claude가 페이지를 다시 읽어요. 탭이 탐색할 때까지 이미 건네준 참조는 번호를 다시 매기지 마세요. Claude가 아직 들고 있는 참조를 조용히 무효화하기 때문이에요.
Claude는 두 타겟팅 스타일을 모두 쓰고 페이지가 드러내는 것에 따라 전환해요. 여러분의 프롬프트와 실행기가 돌려주는 것이 선택을 조종해요:
- 페이지에 쓸 만한 접근성 트리가 있으면 참조를 선호하세요. 참조는 픽셀 좌표를 깨지게 하는 레이아웃 이동·리플로우에도 살아남고, 포인터로 맞히기 어려운 컨트롤에 행동하게 해줘요.
- 트리가 설명하지 않는 콘텐츠에는 좌표로 대체하세요. 캔버스 렌더링 인터페이스, 임베디드 비디오나 원격 데스크톱 화면, 과도한 가상화 리스트, cross-origin iframe 안의 요소는 종종 쓸 만한 노드가 없어요. 그래서 Claude는
screenshot과zoom으로 작업하고 좌표로 클릭하며, 실행기가 좌표가 놓인 프레임을 해석해요. - 읽기는 범위를 한정하고, 스크린샷 전에 트리를 읽으세요. 큰 페이지에서는
filter: "interactive"가 있는read_page나 컨테이너의ref가 초점 맞춘 하위 트리를 돌려주고, 일반적인 페이지의 트리 읽기는 스크린샷보다 입력 토큰이 적게 들면서 Claude가 즉시 행동할 수 있는 참조를 줘요. 시각적 레이아웃, 이미지, 렌더링 상태가 중요할 때는 스크린샷이 여전히 올바른 관찰이에요.
보안 고려 사항
브라우저 사용은 표준 API 기능이 없는 위험을 지녀요. Claude가 공개 웹의 콘텐츠를 읽고 행동하는데, 어떤 페이지든 그것을 조종하려고 쓴 텍스트를 담을 수 있기 때문이에요.
- 브라우저와 실행기를 최소 권한, 자격 증명 없는 새 프로필, 민감한 파일시스템이나 내부 네트워크에 대한 접근이 없는 전용 컨테이너나 가상 머신에서 실행하세요. 함께 실행하는 도구도 같은 방식으로 격리하세요.
- 브라우저가 도달할 수 있는 호스트를 네트워크 계층에서 강제하는 도메인 허용 목록으로 제한하고 리다이렉트 후
navigate핸들러에서 다시 확인하세요. 작업에 필요하지 않으면 loopback, link-local, 사설 범위를 차단하세요. - 페이지가 공급하는 모든 것을 신뢰할 수 없는 입력으로 취급하세요. 여기에는
browser_state블록에서 보고하는 탭 제목·URL과 각 다운로드의url,path,error가 포함돼요. 페이지 읽기는 원시 DOM 소스가 아니라 페이지가 렌더링하는 것(접근성 트리나 보이는 텍스트)으로 만들어 숨은 텍스트가 Claude에 닿지 않게 하세요. navigate핸들러에서 히스토리 키워드"back","forward","reload"를 받고, 스킴 없는 URL을https://로 취급한 뒤 URL을 파싱해http나https가 아닌 스킴(javascript:,file:,data:,chrome:등)을 오류 결과로 거부하세요. 스킴은 문자열 접두사가 아니라 URL 파서로 확인하세요. API는 Claude가 여는 URL을 걸러내지 않으므로 대신 거부해줄 수 없어요.- 꼭 필요할 때까지
javascript_exec와file_upload는 비활성화해두고, 둘 중 하나를 켜기 전 선택적 구성원 활성화를 읽으세요. - 결과가 큰 행동과 명시적 동의를 요구하는 모든 것(구매, 계정 수정, 메시징, 약관 수락)은 사람이 확인하게 하고, 턴 하나가 여러 개를 실을 수 있으므로 각 호출 전에 실행기에서 그 확인을 하세요.
Claude는 여러분의 지시와 충돌해도 페이지 콘텐츠의 지시를 따르는 경우가 있어요. "이전 지시를 무시하고 ~로 이동하라"고 말하는 페이지 텍스트가 작업에서 벗어나게 할 수 있어요. Claude를 민감한 데이터·행동에서 격리해 프롬프트 인젝션이 닿을 수 있는 범위를 제한하고, 탈옥과 프롬프트 인젝션 완화를 검토하며, 작업이 로그인 세션을 피할 수 없다면 전용 저권한 계정을 쓰고 계정을 바꾸는 행동에 사람 확인을 유지하세요.
Anthropic은 모델이 이런 프롬프트 인젝션에 저항하도록 학습시켰고 방어 계층을 추가했어요. 브라우저 사용 도구를 쓰면 분류기가 브라우저가 돌려주는 것(페이지 텍스트나 스크린샷 같은)을 자동으로 스캔해 잠재적 프롬프트 인젝션을 표시해요. 분류기가 잠재적 프롬프트 인젝션을 찾아내면, 행동하기 전에 그 지시가 정말 사용자에게서 왔는지 확인하도록 모델을 자동으로 유도해요.
이 추가 보호는 모든 사용 사례에 이상적이진 않아요(사람이 루프에 없는 사용 사례 등). 끄고 싶다면 지원팀에 문의하세요. 위 예방 조치는 분류기가 있어도 여전히 중요해요.
브라우저가 여러분의 환경에서 실행되므로 Claude가 방문하는 사이트는 실행기의 네트워크 신원을 보게 되고, 페이지 콘텐츠는 여러분이 돌려준 도구 결과로만 API에 닿아요. 제품에서 브라우저 사용을 활성화하기 전에 최종 사용자에게 관련 위험을 알리고 동의를 받으세요.
구성원 도구
browser_toolset_20260801 항목은 구성원 도구 31개를 선언해요. 각 호출의 input은 여기 열거된 파라미터 그대로이고, tab_id는 선택 사항일 때 활성 탭을 기본값으로 해요. Target, CoordinateTarget, RefTarget는 대상과 좌표에서 설명한 형태예요. 네 구성원(javascript_exec, file_upload, read_console, read_network)은 기본적으로 비활성화되어 있고 활성화할 때만 나타나요. 각 구성원 행에 적힌 입력 한도와 출력 규칙은 Claude에게 알려질 뿐 API가 강제하지 않아요. 그러니 좌표를 뷰포트에 맞게 검증하는 등 입력을 검증하고 규칙을 실행기에서 적용하세요.
tool_result 결과에 image 블록이 필요한 것은 screenshot과 zoom뿐이고, 네 탭 관리 구성원(new_tab, list_tabs, switch_tab, close_tab)은 정확히 browser_state 블록 하나를 돌려줘요(탭 관리 결과 참고). 그 밖의 모든 구성원은 text 블록을 돌려줘요. Clicked element ref_2. 같은 짧은 확인이거나 그 구성원의 출력이죠. 탭 관리 결과가 아닌 어떤 결과든 image 블록(보통 행동 후 찍은 스크린샷)을 실어 Claude가 별도 screenshot 호출 없이 결과를 보게 할 수 있어요. 배치 행동은 배치에서 어디에 붙일지 보여줘요. 구성원 tool_result는 text, image, browser_state 콘텐츠 블록만 담을 수 있어요.
탐색과 캡처
| 구성원 | 입력 | 설명 |
|---|---|---|
navigate |
url, tab_id? |
http 또는 https URL을 로드하거나 "back", "forward", "reload"로 히스토리를 이동해요. 스킴 없는 URL은 https://로 취급하고 다른 스킴은 오류 결과로 거부해요. 탭의 URL이나 제목이 바뀌면 browser_state 블록과 함께 짧은 확인을 돌려줘요. |
screenshot |
tab_id? |
뷰포트를 캡처하고 image 블록을 돌려줘요. |
zoom |
region, tab_id? |
작은 텍스트나 컨트롤을 자세히 보기 위해 뷰포트 픽셀로 [x0, y0, x1, y1]로 주어진 region의 잘려서 확대된 image를 돌려줘요. |
포인터
| 구성원 | 입력 | 설명 |
|---|---|---|
left_click |
target: Target, modifiers?, tab_id? |
좌표나 참조된 요소를 왼쪽 클릭해요. modifiers는 클릭 중 누르고 있는 코드 예: "shift" 또는 "ctrl+shift". |
right_click |
target: Target, modifiers?, tab_id? |
좌표나 요소를 오른쪽 클릭해요. |
middle_click |
target: Target, modifiers?, tab_id? |
좌표나 요소를 가운데 클릭해요. |
double_click |
target: Target, modifiers?, tab_id? |
좌표나 요소를 더블 왼쪽 클릭해요. |
triple_click |
target: Target, modifiers?, tab_id? |
좌표나 요소를 트리플 왼쪽 클릭해요. 보통 줄이나 문단을 선택해요. |
hover |
target: Target, tab_id? |
클릭하지 않고 좌표나 요소 위로 포인터를 옮겨요. |
left_click_drag |
from: CoordinateTarget, target: CoordinateTarget, tab_id? |
from에서 누르고 target으로 드래그한 뒤 놓아요. |
left_mouse_down |
target: CoordinateTarget, tab_id? |
좌표에서 왼쪽 버튼을 누르고 있어요. 커스텀 드래그를 위해 left_mouse_up과 짝을 이뤄요. |
left_mouse_up |
target: CoordinateTarget, tab_id? |
좌표에서 왼쪽 버튼을 놓아요. |
mouse_move |
target: CoordinateTarget, tab_id? |
포인터를 좌표로 옮겨요. |
scroll |
target: CoordinateTarget, scroll_direction, scroll_amount?, tab_id? |
뷰포트 위치에서 스크롤해요. scroll_direction은 "up", "down", "left", "right"이고 scroll_amount는 휠 노치 단위로 1~10, 기본 3. |
scroll_to |
target: RefTarget, tab_id? |
참조된 요소를 보이게 스크롤해요. |
키보드와 타이밍
| 구성원 | 입력 | 설명 |
|---|---|---|
type |
text, tab_id? |
현재 포커스에 리터럴 문자열을 입력해요. |
key |
text, repeat?, tab_id? |
키나 코드를 눌러요. text는 단일 키("Enter"), +로 연결된 코드("ctrl+a"), 또는 공백으로 구분된 시퀀스("Backspace Backspace"). repeat는 1~100, 기본 1. |
hold_key |
text, duration, tab_id? |
키나 코드를 duration초 동안 눌러요. 0~30. |
wait |
duration, tab_id? |
duration초 동안 멈춰요. 0~30. |
페이지 읽기
| 구성원 | 입력 | 설명 |
|---|---|---|
read_page |
filter?, depth?, ref?, tab_id? |
페이지의 접근성 트리를 각 요소에 [ref_2] 같은 참조가 붙은 텍스트로 돌려줘요. filter를 생략하면 보이는 모든 요소를, "interactive"면 보이는 상호작용 요소만, "all"이면 뷰포트 밖 요소도 돌려줘요. depth는 트리 깊이를 제한하고(최소 1, 기본 15), ref는 그 요소의 하위 트리로 읽기를 한정해요. 출력을 50,000자로 제한하고 그렇게 말해요. 그러면 Claude가 더 작은 depth나 ref로 좁혀요. |
find |
query, tab_id? |
"search field"나 "add to cart button" 같은 자연어 설명과 일치하는 요소를 검색하고 read_page와 같은 태그 형식으로 최대 20개 일치를 돌려줘요. |
get_page_text |
tab_id? |
페이지의 보이는 텍스트를 평문으로 돌려주고, 주 기사 콘텐츠를 우선해요. 기사, 문서 등 텍스트가 많은 페이지에 적합해요. |
폼과 파일
| 구성원 | 입력 | 설명 |
|---|---|---|
form_input |
target: RefTarget, value, tab_id? |
폼 요소의 값을 직접 설정해요. value는 string, number, boolean이고, 체크박스에는 boolean을, select에는 옵션 값이나 보이는 텍스트를 써요. |
file_upload (기본 비활성화) |
target: RefTarget, paths?, document_ids?, tab_id? |
실행기의 파일시스템 paths, 애플리케이션이 준비한 document_ids, 또는 둘 다에서 파일 입력 요소의 파일을 설정해요. 최소 하나는 필요해요. 파일 업로드 참고. |
진단과 스크립팅
| 구성원 | 입력 | 설명 |
|---|---|---|
read_console (기본 비활성화) |
tab_id? |
마지막 읽기 이후 누적된 탭의 콘솔 항목(로그, 경고, 오류 줄)을 항목당 한 줄로 돌려줘요. 콘솔과 네트워크 활동 읽기 참고. |
read_network (기본 비활성화) |
tab_id? |
마지막 읽기 이후 탭의 네트워크 요청(메서드, URL, 상태, MIME 타입, 타이밍)을 항목당 한 줄로 돌려줘요. |
javascript_exec (기본 비활성화) |
text, tab_id? |
페이지 컨텍스트에서 text를 JavaScript로 실행하고 마지막 표현식의 값을 텍스트로 돌려줘요. 선택적 구성원 활성화 참고. |
탭 관리
| 구성원 | 입력 | 설명 |
|---|---|---|
new_tab |
(없음) | 탭을 열고 활성 탭으로 만들어요. |
list_tabs |
(없음) | 탭 목록을 보고해요. |
switch_tab |
tab_id (필수) |
tab_id를 활성 탭으로 만들어요. |
close_tab |
tab_id (필수) |
tab_id를 닫아요. |
성공 시 각각은 텍스트나 이미지 없이 정확히 browser_state 블록 하나만 돌려줘요. 탭 관리 결과 참고.
도구셋 구성하기
type 외에 도구셋 항목은 configs, cache_control, allowed_callers를 받아요. 이 필드들이 컴퓨터 사용 도구셋과 공유하는 규칙은 클라이언트 도구셋 아래에 나열되어 있고, 이 절은 브라우저 특화 기본값을 다뤄요. configs는 구성원 이름을 키로 하는 객체이고, 각 구성원의 값은 두 필드를 받아요:
| 필드 | 기본값 | 의미 |
|---|---|---|
enabled |
true, 단 네 개 선택적 구성원은 false |
구성원을 Claude에게 제공할지 여부. |
defer_loading |
false |
도구셋 정의를 도구 검색용으로 지연시킬지 여부. 모든 활성 구성원에 같은 값으로 해석되어야 해요. 네 선택적 구성원을 비활성화한 채 도구셋을 지연하려면 나머지 27에 설정하세요. 클라이언트 도구셋 참고. |
구성원 도구 활성화/비활성화
configs에 바꾸고 싶은 구성원만 나열하세요. 생략한 구성원은 기본값을 유지해요. 예를 들어 콘솔 읽기를 구현하지만 저수준 포인터나 키-홀드 제어는 구현하지 않는 실행기는 read_console을 켜고 세 구성원을 숨겨요:
{
"type": "browser_toolset_20260801",
"configs": {
"read_console": { "enabled": true },
"left_mouse_down": { "enabled": false },
"left_mouse_up": { "enabled": false },
"hold_key": { "enabled": false }
}
}
비활성화된 구성원은 Claude가 보는 정의에서 사라져요. 그렇다고 Claude가 절대 호명하지 않는다는 보장은 없으므로, 실행기는 여전히 그런 호출에 오류 결과로 답하세요.
다른 도구와 결합하기
브라우저 사용 도구를 여러분의 도구 및 다른 Anthropic 제공 도구와 같은 tools 배열에 나란히 선언하세요. 커스텀 도구가 구성원과 이름을 공유할 수 있어요(예: 여러분만의 navigate). toolset_name이 Claude의 호출을 구별해주기 때문이죠. 단, browser라는 다른 항목은 없어야 하고 요청은 브라우저 도구셋 항목을 하나만 담을 수 있어요.
컴퓨터 사용 도구와도 나란히 선언할 수 있어요. 도구셋이든 이전 컴퓨터 사용 도구 버전이든요. 둘은 각자 좌표 프레임(여기선 뷰포트 픽셀, 거기선 데스크톱 스크린샷 픽셀)에서 독립적으로 작동하고, screenshot이나 key처럼 이름을 공유하는 구성원에 대한 Claude의 호출은 toolset_name으로 구별돼요.
선택적 구성원 활성화
네 구성원 도구가 기본적으로 비활성화되어 있어요. javascript_exec와 file_upload는 조종된 페이지가 Claude에게 시키게 할 수 있는 범위를 넓히기 때문이고, read_console과 read_network는 모든 브라우저 자동화 스택이 그 로그를 공급할 수는 없고 페이지가 제어하는 콘텐츠가 Claude에 닿는 범위를 넓히기 때문이에요. 각각은 실행기가 구현하고 작업에 필요할 때만 configs로 활성화하세요(예: "configs": {"file_upload": {"enabled": true}}).
파일 업로드
file_upload는 <input type="file"> 요소에 직접 파일을 설정해서 네이티브 파일 선택기를 구동하는 것보다 더 안정적이에요. target은 참조뿐인 이유는 호출에 요소의 정체성이 필요하기 때문이고, paths, document_ids, 또는 둘 다를 받아요:
paths는 실행기의 파일시스템에 있는 파일 경로로, 실행기가 애플리케이션의 파일을 직접 읽을 수 있는 배포용이에요(다운로드의path를 채우는 것과 같은 조건).document_ids는 애플리케이션이 브라우저를 위해 준비한 파일의 식별자로, 직접 읽을 수 없는 배포용이에요. 식별자가 무엇을 뜻하는지는 애플리케이션이 정의해요.paths처럼 이 작업을 위해 준비된 파일로 범위를 한정해 해석하세요.
{
"type": "tool_use",
"id": "toolu_01N7gVzFEfZjLjgsYwnrPgrF",
"name": "file_upload",
"toolset_name": "browser",
"input": {
"target": { "type": "ref", "ref": "ref_12" },
"paths": ["/home/user/uploads/summary.pdf"],
"tab_id": "tab-2"
}
}
Claude는 신뢰할 수 없는 페이지를 읽는 동안 이런 경로를 써요. 그래서 제한 없는 구현은 악성 페이지가 실행기가 읽을 수 있는 아무 파일이나 페이지가 제어하는 사이트로 업로드하게 할 수 있어요. 구성원은 실행기가 각 경로를 확인(symlink와 .. 세그먼트 추적)하고 전용 허용 목록 업로드 디렉터리(작업을 위한 파일만 담는 곳) 밖을 거부할 때만 활성화하세요. 브라우저의 다운로드 디렉터리를 여기에 재사용하지 마세요. 그러면 페이지가 브라우저가 다운로드하게 한 모든 파일이 업로드 가능해져요.
페이지에서 JavaScript 실행
javascript_exec는 Claude가 쓴 표현식을 페이지 컨텍스트에서 실행하고 마지막 표현식의 값을 텍스트로 돌려줘요. Claude는 return 문이 아니라 표현식을 써요. 코드는 페이지의 전체 권한(쿠키, 스토리지, 동일 출처 요청 포함)으로 실행돼요. 자격 증명이 없는 세션에서만 구성원을 활성화하고, 보안 고려 사항의 도메인 허용 목록을 유지하며, 돌려받은 값을 신뢰할 수 없는 입력으로 취급하고, Claude가 내는 코드를 기록하세요.
콘솔과 네트워크 활동 읽기
read_console은 탭의 콘솔 항목을, read_network는 탭의 네트워크 요청을 돌려줘요. 각각 텍스트로, 그 탭의 이전 읽기 이후 누적된 항목을 한 줄씩 담아요. 콘솔 줄은 로그, 경고, 오류 항목을, 네트워크 줄은 메서드, URL, 상태, MIME 타입, 타이밍을 담아요. 항목은 브라우저 자동화가 탭에 붙은 순간부터 존재하므로 빈 결과가 이미 열려 있던 탭에 트래픽이 없었다는 뜻은 아니에요.
이 구성원들은 Claude가 스크린샷을 반복하지 않고 잘못된 페이지(스피너 뒤의 실패한 요청, 죽은 버튼 뒤의 스크립트 오류)를 진단하게 해줘요. 콘솔·네트워크 항목은 페이지가 제어하고 요청 URL의 토큰 같은 비밀을 자주 담아요. Claude 컨텍스트에 넣고 싶지 않은 자격 증명 같은 값을 걸러내고 매우 긴 항목은 돌려주기 전에 잘라내세요.
browser_state로 탭 추적하기
Claude는 tab_id로 탭을 가리키고, 어떤 탭이 존재하는지의 진실의 출처는 애플리케이션이며, 그 상태를 Claude가 직접 보지 못하는 browser_state 콘텐츠 블록으로 보고해요. API가 그로부터 Claude가 읽는 텍스트를 렌더링해요.
{
"type": "browser_state",
"tabs": [
{
"tab_id": "tab-1",
"title": "Documentation",
"url": "https://example.com/docs",
"active": true
},
{ "tab_id": "tab-2", "title": "Pricing", "url": "https://example.com/pricing" }
]
}
tabs는 호출 후 열린 탭의 전체 목록이고, 델타가 아니에요. 비어 있을 수 있어요. 비어 있지 않으면 정확히 하나가"active": true를 담아요.state_changes(여기엔 없음)는 호출의 부수 효과를 보고해요. 호출이 열고 완료 시점에 여전히 열려 있는 각 탭의tab_opened항목(tab_id는tabs에도 나타나야 해요)과 다운로드 이벤트. 보고할 것이 없으면 필드를 생략하세요. 빈 배열은 거부돼요.- 브라우저 구성원 호출에 답하는 결과에만,
tool_result당 최대 한 번,is_error: true인 결과에는 절대 보내지 마세요. "보고할 탭 상태 없음"은 블록을 생략해서 표현해요. - API는
tabs와state_changes의 다운로드 항목을 Claude용 텍스트로 렌더링해요. 다음 두 절과 다운로드 보고가 그 텍스트를 보여줘요.
tab_id 값은 여러분이 지정해요. 자동화 라이브러리의 페이지 식별자든 여러분의 카운터든 안정적인 문자열이면 돼요. 단, 그 식별자를 가진 탭이 이전 결과에 아직 열려 있다고 나열되어 있는 동안 tab_id를 재사용하지 마세요. API는 블록에 다음 한도를 적용해요:
- 각
tab_id,title,url은 최대 4,096자,tab_id는 비어 있지 않아야 하고, 어떤 것도 제어 문자(개행 포함)나 유니코드 줄·문단 구분자를 담을 수 없어요. - 블록은 최대 100개 탭과 200개 상태 변화를 나열할 수 있어요.
- 같은 한도가 Claude가
switch_tab과close_tab에 전달하는tab_id에도 적용돼요. API가 결과 텍스트로 렌더링하기 때문이죠. 그래서 그 한도를 위반하는tab_id호출은browser_state블록 대신 오류 결과로 답하세요.
탭 관리 결과
new_tab, switch_tab, close_tab, list_tabs의 성공 결과 content는 텍스트나 이미지 없이 정확히 browser_state 블록 하나이고, API가 Claude가 보는 텍스트를 써요. new_tab 결과의 블록은 active: true로 표시된 항목과 일치하는 tab_id의 tab_opened 상태 변화도 정확히 하나 담아야 해요.
| 구성원 | Claude가 보는 텍스트 |
|---|---|
switch_tab |
Switched to tab {tab_id}, 호출의 input.tab_id에서 가져옴 |
close_tab |
Closed tab {tab_id}, 호출의 input.tab_id에서 가져옴 |
new_tab |
Created new tab with tab_id: {tab_id}, URL: {url}. It is now the current tab., active: true 표시 항목에서 가져옴 |
list_tabs |
Available tabs: 다음에 탭당 한 줄, tabs가 비면 No tabs available |
tabs가 비어 있지 않고 두 탭을 나열하고 첫 번째가 활성인 list_tabs 결과는, 각 줄이 두 칸 들여쓰기되고 활성 탭에만 (current)가 붙은 채로 다음과 같이 렌더링돼요:
Available tabs:
• tab_id tab-1: "Documentation" (https://example.com/docs) (current)
• tab_id tab-2: "Pricing" (https://example.com/pricing)
이 구성원의 오류 결과는 그 반대예요. content의 일반 오류 텍스트, is_error: true, browser_state 블록 없음.
예를 들어 Claude가 new_tab을 호출하면(input은 비어요) 실행기는 탭을 열고 활성으로 만든 뒤 tab_opened 항목 하나와 함께 목록을 돌려줘요:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01WvHSbQVV9j5nWGvTmk4vNL",
"toolset_name": "browser",
"content": [
{
"type": "browser_state",
"tabs": [
{ "tab_id": "tab-1", "title": "Documentation", "url": "https://example.com/docs" },
{ "tab_id": "tab-2", "title": "Pricing", "url": "https://example.com/pricing" },
{ "tab_id": "tab-3", "title": "", "url": "about:blank", "active": true }
],
"state_changes": [{ "type": "tab_opened", "tab_id": "tab-3" }]
}
]
}
]
}
Claude는 Created new tab with tab_id: tab-3, URL: about:blank. It is now the current tab.를 봐요. 여기처럼 탭이 열린 URL을 보고하세요. 나중에 리다이렉트되는 URL이 아니라요. 이후 결과는 그 탭의 그 시점 URL을 보고해요.
다른 결과의 탭 컨텍스트
다른 모든 구성원에서 블록은 선택 사항이에요. 열린 탭 집합, 활성 탭, 탭의 제목이나 URL이 바뀌었거나 보고할 state_changes가 있을 때 보내고, 항상 전체 tabs 목록을 포함하세요. 결과가 텍스트와 browser_state 블록을 모두 담을 때 API는 그 결과 텍스트에 빈 줄로 구분된 Tab Context 꼬리를 붙여서, 별도 list_tabs 호출 없이 Claude가 새 상태를 받게 해요:
Tab Context:
- Executed on tab_id: tab-1
- Available tabs:
• tab_id tab-1: "Documentation" (https://example.com/docs)
• tab_id tab-2: "Pricing" (https://example.com/pricing)
Executed on은 호출이 실행된 탭을 이름 짓는데, tab_id 입력이 있으면 그것이고 없으면 활성 탭이에요. 꼬리의 탭 줄은 (current) 표시를 담지 않아요. 이 텍스트를 직접 붙이지 말고 구조화된 블록을 보내 API가 렌더링하게 하세요. 꼬리는 중복 제거되므로 나중 결과에 동일한 탭 상태가 다시 렌더링되지 않고, 블록을 넉넉히 채우는 것은 비용이 들지 않아요.
세 경우가 블록이 있어도 꼬리를 렌더링하지 않아요:
- 모든
zoom결과. text블록이 없는 결과(예: 이미지만 있는screenshot결과). 그 결과에 대해 아무것도 렌더링되거나 기억되지 않아요. 탭 컨텍스트는 텍스트와browser_state블록을 모두 담는 다음 결과에 나타나요. 같은 결과에서 Claude가 탭 변화를 보게 하려면 이미지 옆에 짧은 텍스트 블록을 포함하세요. 단, 블록이 다운로드 이벤트를 보고하는 결과는 예외예요. API가 다운로드 줄을 텍스트 블록으로 추가하고 꼬리는 텍스트가 있는 결과와 같이 그 뒤를 따라요.tab_id를 담지 않은 호출에서tabs목록이 비어 있는 결과. 이름 지을 탭이 없기 때문이에요.
예를 들어 Claude가 이 세션 앞부분에서 "Pricing" 링크(ref_5)를 클릭했고 페이지가 Claude가 요청하지 않은 새 탭에서 열었다면, 보고가 없으면 Claude는 그것을 알기 위해 list_tabs를 호출해야 해요. 클릭의 확인과 함께 열린 탭을 이름 짓는 state_changes를 담은 블록을 돌려주고, 실행기가 활성으로 남긴 탭을 표시하세요:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01EgTXj1FjE2FCTt2zNFWLao",
"toolset_name": "browser",
"content": [
{ "type": "text", "text": "Clicked element ref_5." },
{
"type": "browser_state",
"tabs": [
{
"tab_id": "tab-1",
"title": "Documentation",
"url": "https://example.com/docs",
"active": true
},
{ "tab_id": "tab-2", "title": "Pricing", "url": "https://example.com/pricing" }
],
"state_changes": [{ "type": "tab_opened", "tab_id": "tab-2" }]
}
]
}
]
}
Claude는 Clicked element ref_5. 뒤에 앞에서 본 Tab Context 꼬리를 봐요. 실패한 호출 중에 열린 탭은 tab_opened 항목을 얻지 못해요. 오류 결과는 browser_state를 담지 않기 때문이죠. 그 탭은 다음 성공 결과의 tabs 목록에 나타나요. 배치에서는 변화가 일어난 호출의 결과에 블록을 붙이고, 같은 턴의 이전 결과가 같은 상태를 보고했어도 모든 성공 탭 관리 결과에 자체 블록을 주세요.
다운로드 보고
클릭이나 탐색이 파일 다운로드를 시작하면, 그것이 일어난 호출의 결과 state_changes에 보고하세요. 여러분이 지정한 download_id로 결과를 가로질러 상관시켜요. 다운로드는 비동기로 실행되고 여러 결과에 걸칠 수 있으므로 세 가지 이벤트 유형이 있어요:
type |
필드 | 보내는 때 |
|---|---|---|
download_started |
download_id, url |
다운로드가 시작된 호출의 결과에. url은 리다이렉트 후 파일이 제공되는 최종 URL. |
download_completed |
download_id, url, path?, size_bytes? |
다운로드가 끝날 때 실행 중인 나중 호출의 결과에. path는 같은 환경의 다른 도구(예: bash 도구 또는 file_upload)가 그 파일을 읽을 수 있을 때만 포함. 그렇지 않으면 download_id가 다운로드의 유일한 식별자. |
download_failed |
download_id, url, error? |
다운로드가 실패하거나 취소될 때, 브라우저가 이유를 주면 error에. |
API는 각 항목을 항목이 나타난 순서대로 Claude용 텍스트 한 줄로 렌더링해요. 결과 텍스트 뒤, 빈 줄로 구분되고 Tab Context 꼬리 앞에 추가해요. 모든 종류의 구성원 결과가 그 줄을 담아요. zoom과 탭 관리 결과까지요. text 블록이 없는 결과는 자체 텍스트 블록으로 받아요. 각 줄은 download_id와 url, 그리고 보낼 때 path·size_bytes(download_completed) 또는 error(download_failed)를 줘요. 자신의 텍스트에서 다운로드를 설명할 필요 없어요. API가 url, path, error를 큰따옴표로 감싸고 안의 큰따옴표·백슬래시를 이스케이프하므로 이 값을 미리 이스케이프하지 마세요.
예를 들어 Pricing 탭의 "Download price list (CSV)"(ref_8) 클릭이 다운로드를 시작하면, 클릭 결과가 download_id "dl-1"과 파일 URL을 담은 download_started 항목을 실어요. 다운로드는 나중 screenshot 호출이 실행되는 동안 끝나므로 그 결과의 content는 이미지, Screenshot captured. 같은 텍스트 블록, 그리고 같은 download_id 아래 완료를 보고하는 이 browser_state 블록을 담아요:
{
"type": "browser_state",
"tabs": [
{ "tab_id": "tab-1", "title": "Documentation", "url": "https://example.com/docs" },
{
"tab_id": "tab-2",
"title": "Pricing",
"url": "https://example.com/pricing",
"active": true
}
],
"state_changes": [
{
"type": "download_completed",
"download_id": "dl-1",
"url": "https://example.com/pricing/price-list.csv",
"path": "/home/user/downloads/price-list.csv",
"size_bytes": 48213
}
]
}
Claude는 Screenshot captured. 뒤에 빈 줄과 이런 줄을 봐요:
Download completed with download_id: dl-1, URL: "https://example.com/pricing/price-list.csv". Saved to "/home/user/downloads/price-list.csv". Size: 48213 bytes.
다운로드 보고는 다음 규칙을 따라요:
download_id당 블록 하나에 항목 최대 하나. 그래서 같은 호출 중에 시작하고 끝나는 다운로드는download_completed만 보고해요.is_error: true결과에state_changes를 절대 보내지 마세요. 실패한 호출 중에 일어난 다운로드 이벤트는 다음 성공 결과에 보고하세요.state_changes는 진행 중인 다운로드의 목록이 아니에요. 각 이벤트를 한 번 보고하세요.- 각 항목은
type이 선언하는 필드만 담아요.size_bytes는 음이 아닌 정수,download_id는 비어 있지 않고,download_id,url,path,error는 각각 최대 4,096자이며 제어 문자나 유니코드 줄·문단 구분자가 없어야 해요.url은 원격 서버에서 오고 리다이렉트 후에는 서명된 쿼리 문자열 자격 증명을 자주 실어요. Claude 컨텍스트에 넣고 싶지 않은 쿼리 파라미터를 제거하고 보고하거나 파일시스템 경로에 쓰기 전에 살균하세요.
오류 처리
실패한 호출을 Claude에게 일반 오류 결과로 보고하세요. is_error: true, 무슨 일이 있었는지 말하는 텍스트 내용, toolset_name 그대로, browser_state 블록 없음.
실행기에서 오류 돌려주기
오류 텍스트를 구체적으로 만드세요. Claude가 그것을 읽고 적응하기 때문이에요. Error: Navigation to https://example.com/status timed out after 30 seconds. The page may be unavailable.는 Error: navigation failed 같은 맨 오류보다 Claude가 행동할 무언가를 줘요. 다른 흔한 경우:
```json
{
"type": "tool_result",
"tool_use_id": "toolu_01FkP8rTz6uYh2mNq4LsXw7v",
"toolset_name": "browser",
"is_error": true,
"content": "Not executed: an earlier action in this turn failed."
}
```
요청 오류
API는 도구셋 항목과 대화의 모든 구성원 tool_use·tool_result 블록을 검증해요. 하나가 잘못되면 API가 Claude를 실행하기 전에 invalid_request_error를 돌려줘요. 아래 표에서 왼쪽 열은 여러분이 보낸 것을 이름 짓고 있어요.
| 요청 | 실패 이유와 할 일 |
|---|---|
도구셋 항목이 받지 않는 옵션 또는 조합, 예: name, strict: true, input_examples, 항목 자체의 defer_loading, 구성원 이름이 아닌 configs 키, 구성원 configs 값의 enabled·defer_loading 외 필드(도구셋 구성하기), defer_loading 값이 서로 다른 활성 구성원(도구셋 구성하기), 활성 구성원을 남기지 않는 configs, allowed_callers의 코드 실행 호출자, 요청의 레거시 fine-grained-tool-streaming-2025-05-14 베타 헤더, browser나 구성원을 이름 짓는 tool 타입 tool_choice, 또는 두 번째 브라우저 도구셋 항목이나 browser라는 다른 도구 |
이것들은 클라이언트 도구셋에서 지원되지 않아요. 각 규칙과 대안은 클라이언트 도구셋 참고. |
"toolset_name": "browser" 없이 또는 다른 값으로 구성원 호출에 답하는 tool_result, 또는 호출이 구성원 호출이 아니었는데 toolset_name을 담은 결과 |
구성원 결과에 toolset_name을 정확히 그대로 담고, 거기서만 그렇게 하세요. |
일치하는 tool_result가 없는 이전 턴의 구성원 tool_use |
실패 후 실행하지 않은 것까지 모든 구성원 호출에 답하세요. |
구성원 결과의 text, image, browser_state 외 콘텐츠 블록 |
구성원 결과는 그 세 블록 타입만 받아요. |
browser_state로 탭 추적하기의 규칙을 깨는 browser_state 블록, 예: is_error: true 결과에 있거나 브라우저 구성원 호출에 답하지 않는 결과에 있는 경우, 결과에 둘 이상, 비어 있지 않은 tabs에 active: true 항목이 정확히 하나가 아닌 경우, 중복 tab_id, 빈 state_changes 배열, tabs에 없는 tab_id의 tab_opened, 한 download_id에 두 상태 변화 또는 type이 선언하지 않는 상태 변화 필드(다운로드 보고), 또는 한도를 넘는 필드 |
블록을 고치세요. "보고할 것 없음"은 블록이나 state_changes 필드를 생략해서 표현하고, 빈 값으로는 절대 안 돼요. |
new_tab, switch_tab, close_tab, list_tabs의 성공 결과 content가 정확히 browser_state 블록 하나가 아니거나, 활성 탭과 일치하는 tab_opened가 정확히 하나 없는 new_tab 결과 |
API가 이런 결과를 블록에서 렌더링하므로 그 정확한 형태가 필요해요. 탭 관리 결과 참고. |
모델의 이미지 크기 한도를 넘거나, 요청이 이미지 20개 이상을 담을 때 적용되는 더 엄격한 이미지당 한도를 넘는 결과의 image(이전 결과의 스크린샷과 zoom 이미지 포함) |
API는 도구셋 이미지를 줄이지 않아요. 스크린샷은 돌려주기 전에 크기를 조정하세요(고해상도 좌표 스케일링 처리). |
browser_toolset_20260801을 지원하지 않는 model |
지원 모델은 호환성 참고. |
제한 사항
- 플랫폼 가용성: 브라우저 사용은 Claude API와 Google Cloud에서 사용할 수 있어요.
- 전체 입력 스트리밍만: 스트리밍할 때 각 구성원의
input은 완전한input_json_delta하나로 도착해요(클라이언트 도구셋). - 요소 참조는 최선 노력: 매우 동적인 페이지(가상화 리스트, 캔버스 렌더링 인터페이스, 스크롤에 재렌더링되는 페이지)는 안정적인 참조를 노출하지 않을 수 있고, Claude는 거기서 스크린샷·좌표 클릭으로 대체해요.
read_console과read_network는 브라우저 자동화에 의존해요: 캡처할 수 있는 것만, 그리고 탭에 붙은 시점부터만 보고해요.- 일반 에이전트 한도가 적용돼요: 지연, 시각 정확도, 프롬프트 인젝션 위험이 컴퓨터 사용에서 이어져요(컴퓨터 사용 도구의 제한 사항 참고). 프롬프팅으로 모델 성능 최적화, 스크린샷 히스토리 관리, 구현 모범 사례 따르기(행동 지연, 행동 검증, 로깅)의 지침도 브라우저 실행기에 적용돼요.
가격과 데이터 보존
브라우저 사용은 표준 도구 사용 가격을 따라요. 브라우저 사용 도구를 쓸 때:
도구셋 정의 오버헤드: 기본 구성원으로 browser_toolset_20260801을 선언하면 요청에 약 6,600 입력 토큰이 추가돼요(Claude Fable 5, Claude Mythos 5, Claude Opus 5, Claude Opus 4.8에서는 약 6,610, Claude Sonnet 5에서는 약 6,670). 구성원 도구 정의와 도구 사용 시스템 프롬프트를 포함해요. 네 선택적 구성원을 모두 활성화하면 약 880 토큰이 추가되고, configs로 구성원을 비활성화하면 줄어들어요. 요청의 정확한 수치는 응답 usage에 보고되고, 토큰 계산 엔드포인트로 미리 추정할 수 있어요.
추가 토큰 소비:
- 도구 결과로 돌려준 스크린샷과 확대 이미지. 이미지 입력으로 청구돼요(비전 가격 참고).
- Claude에게 돌려준 텍스트 도구 결과. 접근성 트리, 페이지 텍스트, 콘솔·네트워크 항목 등.
브라우저 세션, 다운로드, 업로드된 파일은 여러분의 환경에 남아요. 여러분이 돌려준 스크린샷, 페이지 텍스트, 탭 상태는 API 요청 콘텐츠의 일부이고 표준 보존 정책(또는 ZDR 약정이 있다면 그 약정)을 따라요. 브라우저 사용 도구는 ZDR 적격이에요. 기능별 보존 기간과 적격성은 API와 데이터 보존 참고.