+ );
+}
diff --git a/crates/bsk-cli/skill/SKILL.md b/crates/bsk-cli/skill/SKILL.md
index 98768eb8..d8c1110c 100644
--- a/crates/bsk-cli/skill/SKILL.md
+++ b/crates/bsk-cli/skill/SKILL.md
@@ -45,12 +45,34 @@ environment settings may not persist between shell calls. Keep browser commands
sandboxed. For other startup failures, retry once, then use `bsk doctor`.
A local process identity warning permits browser commands when IPC works.
+## Required browser profiles
+
+When the user requires a particular browser profile, bind the task to that
+profile's extension instance before starting a session, even if only one browser
+is connected. A Chrome profile name or directory is not a BrowserSkill instance
+ID or an automatically assigned label.
+
+Use the instance ID from the BrowserSkill popup in the required profile. The user
+can choose **Copy profile instructions** there and send the resulting instruction.
+If only a profile name/path is supplied and its mapping is unknown, ask the user
+to open that profile, verify its Profile Path at `chrome://version`, and copy the
+profile instructions. Do not infer the mapping from a single Connected browser
+or Chrome process command lines.
+
+Run `bsk browsers --json` to check that the supplied instance is connected, then
+pass `--browser ` on every new session for this task. A previously
+verified unique label also works. If the target is missing or ambiguous, stop and
+report it; never omit the selector or substitute another instance to recover.
+Opening another Chrome profile does not retarget an existing session. After an
+extension reinstall or storage reset, obtain the instance mapping again.
+
## Task workflow
-1. Define success from the user's request. Start `bsk session start --json` and
- retain its `session_id`. With multiple browsers, run `bsk browsers` and add
- `--browser ` to start. For background work, add `--no-focus` to
- `session start` only.
+1. Define success from the user's request. For a required browser profile, follow
+ **Required browser profiles** above and start with its explicit `--browser`
+ selector. Otherwise start `bsk session start --json`; with multiple browsers,
+ run `bsk browsers` and choose `--browser `. Retain the returned
+ `session_id`. For background work, add `--no-focus` to `session start` only.
2. For a new page, navigate; for an existing user tab, follow **Borrowing** below.
Read the page before interacting:
diff --git a/crates/bsk-cli/src/cli/session.rs b/crates/bsk-cli/src/cli/session.rs
index 1ecbdfb7..9bf3ce07 100644
--- a/crates/bsk-cli/src/cli/session.rs
+++ b/crates/bsk-cli/src/cli/session.rs
@@ -61,8 +61,8 @@ pub struct SessionStartArgs {
/// Optional task name displayed in local operation history.
#[arg(long)]
pub name: Option,
- /// Target browser instance id (only required when multiple browsers
- /// are connected).
+ /// Target browser instance ID or unique label. Always set this when
+ /// a specific browser profile is required.
#[arg(long)]
pub browser: Option,
diff --git a/docs/browser-profiles.md b/docs/browser-profiles.md
new file mode 100644
index 00000000..1118eaea
--- /dev/null
+++ b/docs/browser-profiles.md
@@ -0,0 +1,72 @@
+# Select a specific browser profile
+
+BrowserSkill connects to the extension installed in a browser profile. A successful
+connection alone does not identify the profile you intended to use. The CLI does
+not launch Chrome or accept Chrome's `--profile-directory` argument.
+
+## Bind a task to the intended profile
+
+1. Open the required profile in Chrome. If you need to verify its directory, open
+ `chrome://version` in that window and check **Profile Path**.
+2. Open BrowserSkill's popup in that same profile and ensure it is connected.
+3. Choose **Copy profile instructions** and send the instructions along with your
+ task to the agent. The copied instruction includes this profile's extension
+ instance ID and requires it on every new session for the task.
+
+You can also copy the **Instance ID** from the popup and use it directly:
+
+```sh
+bsk browsers --json
+bsk session start --browser --json
+```
+
+Replace the placeholder with the popup's instance ID, not Chrome's extension ID,
+profile display name or directory name. An existing, verified unique BrowserSkill
+label also works; labels are not populated from Chrome profile names automatically.
+The instruction only copies text: it does not start a session or change settings.
+
+If the target is offline, reconnect BrowserSkill in that profile and retry the
+same selector. Do not remove `--browser` to get past the error: that could select
+another profile. A missing selector is rejected when multiple browsers are online,
+but automatically selects the only connected browser when just one is online.
+
+The session remains bound to its selected instance. Opening or switching Chrome
+profiles does not move an existing session. Stop it with `bsk session stop `
+when the task ends. Reinstalling the extension or resetting its storage can change
+the instance ID; verify the mapping again instead of substituting another browser.
+Copying a whole profile can also copy its extension storage, so instance IDs are
+routing identifiers, not independent proof of a filesystem path.
+
+## DeepSeek Harness
+
+Use the verified popup instance ID in the plugin's session tool:
+
+```text
+browser_session({ action: "start", browser: "" })
+```
+
+**Copy profile instructions** includes this tool call alongside the CLI command.
+Use the DeepSeek Harness call instead of running a separate CLI session. The plugin
+manages its own sessions. Supply the same selector on every new session for the task, even if
+only one browser is connected. If the mapping is unknown or the target is offline,
+ask the user to confirm or reconnect it; do not retry without `browser`.
+
+## Scope and limitations
+
+This is a manual profile-to-instance workflow, not automatic profile discovery or
+a persisted task/workspace requirement. Explicit selectors reject unavailable or
+ambiguous targets. Neither the CLI nor the DSH plugin can infer a profile requirement
+from the user's conversation if the agent omits the selector: with only one browser
+connected, an unqualified start can still select the wrong profile. The skill and
+copied instructions guide the agent; they do not enforce a binding on future calls.
+
+## Windows and macOS
+
+The instance-selection workflow is the same on both platforms. The profile path
+location differs; use the value displayed by Chrome rather than guessing it.
+[Chromium documents this check](https://chromium.googlesource.com/chromium/src/+/main/docs/user_data_dir.md).
+
+Several profiles can run in one Chrome browser process. Its startup command line
+may still name the first profile after a second profile opens, so scanning process
+arguments cannot prove which profile owns a connected extension. There is no need
+to quit all other Chrome profiles to select a connected BrowserSkill instance.
diff --git a/packages/dsh-plugin-browserskill/skill/SKILL.md b/packages/dsh-plugin-browserskill/skill/SKILL.md
index 6ccfb579..ebb1452b 100644
--- a/packages/dsh-plugin-browserskill/skill/SKILL.md
+++ b/packages/dsh-plugin-browserskill/skill/SKILL.md
@@ -10,9 +10,31 @@ Do not control the browser through another process. Use the loaded action schema
For remote setup or pairing, follow the [remote guide](https://github.com/Tencent/BrowserSkill/blob/main/docs/remote-extension-connection.md) before using these tools.
+## Required browser profiles
+
+If the user or workspace requires a specific profile, confirm its instance ID before
+starting, even with only one connected browser. If unknown, ask the user to open the
+intended profile, check **Profile Path** at `chrome://version` if a directory was
+specified, and copy the **Instance ID** or **Copy profile instructions** from the
+connected BrowserSkill popup in that same profile. Connected alone and Chrome's
+process arguments do not prove the profile.
+
+Use the verified ID (or verified unique BrowserSkill label) on every new session:
+
+```text
+browser_session({ action: "start", browser: "" })
+```
+
+If the copied instructions contain a command-line example, use its instance ID in
+this tool call; do not run that command separately. A Chrome profile name, directory,
+or extension ID is not an instance ID. If the mapping is unclear, ambiguous, or the
+target is unavailable, stop and ask the user to confirm or reconnect it. Never omit
+`browser` or substitute another instance to recover.
+
## Mandatory workflow
-1. Define success. Start a session and retain `sessionId`. For a new page:
+1. Define success. Start a session and retain `sessionId`. Include `browser` as above
+ when a profile is required. Otherwise, for a new page:
```text
browser_session({ action: "start" })
@@ -83,7 +105,8 @@ unknown effects or switch backends to bypass limits. Borrow confirmation still a
again (users can enter `/browser-skill`), then retry the intended browser tool once
after its schema appears. If it remains unavailable, report the failure.
- Stale ref: observe, then retry the intended action once.
-- Unknown tab/session: list owned resources or start a session; never guess IDs.
+- Unknown tab/session: list owned resources or start a session with the required
+ browser selector, if any; never guess IDs.
- Failed or interrupted session stop: accepted cleanup continues in the background.
Retry the same stop; a completed previous stop returns `alreadyClosed: true`.
If several stops are pending, specify `session` or the owned `requestId` from the
diff --git a/packages/dsh-plugin-browserskill/src/browser-tools.ts b/packages/dsh-plugin-browserskill/src/browser-tools.ts
index 527cfcfe..03432687 100644
--- a/packages/dsh-plugin-browserskill/src/browser-tools.ts
+++ b/packages/dsh-plugin-browserskill/src/browser-tools.ts
@@ -7,6 +7,7 @@
import { defineTool, type ParameterSchemaSpec, type ToolDefinition } from "@deepseek-ai/dsh-tools";
import {
+ BROWSER_PARAM,
SESSION_PARAM,
SESSION_STOP_PARAMS,
TAB_ID_PARAM,
@@ -104,8 +105,11 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
name: "browser_session",
description:
"Manage plugin-owned browser sessions. Actions: start opens an Agent Window; stop closes an " +
- "owned session; list returns owned sessions. For start, url/device/width/height/noFocus/browser " +
- "are optional. For stop, specify session or requestId (not both), or omit both to retry an " +
+ "owned session; list returns owned sessions. For start, url/device/width/height/noFocus are " +
+ "optional. When a specific profile is required, always set browser to its verified instance " +
+ "ID or unique label, even with one connected browser; stop if the target is unknown or " +
+ "unavailable instead of omitting or changing browser. For stop, specify session or requestId " +
+ "(not both), or omit both to retry an " +
"unacknowledged stop before selecting the current owned session. If several stops await " +
"acknowledgement, specify a target. Once accepted, cleanup continues if the call is aborted.",
actions: {
@@ -119,7 +123,7 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
width: { type: "integer", description: "Agent Window width; start requires height too." },
height: { type: "integer", description: "Agent Window height; start requires width too." },
noFocus: { type: "boolean", description: "Start the Agent Window in the background." },
- browser: { type: "string", description: "Browser instance id for start." },
+ browser: BROWSER_PARAM,
device: { type: "string", enum: DEVICE_PRESETS, description: "Device preset for start." },
},
},
diff --git a/packages/dsh-plugin-browserskill/src/tool-params.ts b/packages/dsh-plugin-browserskill/src/tool-params.ts
index de3f776f..a651b1ac 100644
--- a/packages/dsh-plugin-browserskill/src/tool-params.ts
+++ b/packages/dsh-plugin-browserskill/src/tool-params.ts
@@ -1,5 +1,13 @@
/** Shared model-facing parameter schemas for browser tools. */
+export const BROWSER_PARAM = {
+ type: "string",
+ description:
+ "Target browser instance ID or verified unique label for start. Always set this when a " +
+ "specific profile is required, even if only one browser is connected. Confirm the mapping " +
+ "with the user if unknown; never omit or substitute the selector to recover from an unavailable target.",
+} as const;
+
export const SESSION_PARAM = {
type: "string",
description:
diff --git a/packages/dsh-plugin-browserskill/src/tools.ts b/packages/dsh-plugin-browserskill/src/tools.ts
index ad18dc9e..5f9835eb 100644
--- a/packages/dsh-plugin-browserskill/src/tools.ts
+++ b/packages/dsh-plugin-browserskill/src/tools.ts
@@ -30,7 +30,7 @@ import {
} from "./runner";
import { SessionStarts } from "./session-starts";
import type { SessionRegistry } from "./sessions";
-import { SESSION_PARAM, SESSION_STOP_PARAMS } from "./tool-params";
+import { BROWSER_PARAM, SESSION_PARAM, SESSION_STOP_PARAMS } from "./tool-params";
/** Plugin configuration resolved from the Schemastery schema in index.ts. */
export interface PluginConfig {
@@ -196,11 +196,7 @@ function defineBrowserOperations(deps: ToolDeps, register: DefinitionRegistrar):
type: "boolean",
description: "Open the Agent Window in the background without stealing focus.",
},
- browser: {
- type: "string",
- description:
- "Target browser instance id (only needed when multiple browsers are connected).",
- },
+ browser: BROWSER_PARAM,
device: {
type: "string",
enum: DEVICE_PRESETS,
diff --git a/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts b/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts
new file mode 100644
index 00000000..402142fe
--- /dev/null
+++ b/packages/dsh-plugin-browserskill/tests/profile-selection.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, it } from "vitest";
+import type { BskRunResult } from "../src/runner";
+import { harness, ok } from "./session-lifecycle-harness";
+
+describe("profile selection through browser_session", () => {
+ it.each([
+ "a1b2c3d4",
+ "Required Profile A",
+ ])("preserves the verified selector %s in the start command and returned identity", async (browser) => {
+ const h = harness(async (args) => {
+ if (args[1] === "start") {
+ return ok({ session_id: "selected", browser_instance_id: "a1b2c3d4" });
+ }
+ return ok({ state: args.includes("--claim") ? "active" : "closed" });
+ });
+ await expect(h.session({ action: "start", browser })).resolves.toMatchObject({
+ sessionId: "selected",
+ browserInstanceId: "a1b2c3d4",
+ });
+ const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start");
+ expect(starts).toHaveLength(1);
+ expect(starts[0].args).toEqual([
+ "session",
+ "start",
+ "--request-id",
+ expect.any(String),
+ "--browser",
+ browser,
+ ]);
+ });
+
+ it.each([
+ ["a1b2c3d4", "not_found"],
+ ["Shared label", "invalid_params"],
+ ])("does not fall back or navigate when %s fails with %s", async (browser, code) => {
+ const h = harness(async (args): Promise => {
+ if (args[1] === "start") {
+ if (args.includes("--browser")) {
+ return {
+ ...ok({ code, message: "Required browser is unavailable or ambiguous" }),
+ code: 1,
+ };
+ }
+ // An unqualified retry would succeed on the wrong, online browser.
+ return ok({ session_id: "wrong", browser_instance_id: "b1c2d3e4" });
+ }
+ return ok({ state: "closed" });
+ });
+ await expect(
+ h.session({ action: "start", browser, url: "https://example.test/" }),
+ ).rejects.toMatchObject({ code });
+ const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start");
+ expect(starts).toHaveLength(1);
+ expect(starts[0].args.slice(-2)).toEqual(["--browser", browser]);
+ expect(h.calls.some(({ args }) => args[0] === "navigate")).toBe(false);
+ expect(h.registry.current()).toBeUndefined();
+ expect(h.registry.size()).toBe(0);
+ });
+
+ it("keeps an unqualified start available when no profile selector was supplied", async () => {
+ const h = harness(async (args) => {
+ if (args[1] === "start") {
+ return ok({ session_id: "default", browser_instance_id: "b1c2d3e4" });
+ }
+ return ok({ state: args.includes("--claim") ? "active" : "closed" });
+ });
+ await expect(h.session({ action: "start" })).resolves.toMatchObject({
+ browserInstanceId: "b1c2d3e4",
+ });
+ const starts = h.calls.filter(({ args }) => args[0] === "session" && args[1] === "start");
+ expect(starts).toHaveLength(1);
+ expect(starts[0].args).not.toContain("--browser");
+ });
+});
diff --git a/packages/dsh-plugin-browserskill/tests/skill.test.ts b/packages/dsh-plugin-browserskill/tests/skill.test.ts
index a8943a9e..65b6d433 100644
--- a/packages/dsh-plugin-browserskill/tests/skill.test.ts
+++ b/packages/dsh-plugin-browserskill/tests/skill.test.ts
@@ -50,7 +50,11 @@ describe("registerBskSkill", () => {
// Keep the lazily injected instructions inside a bounded prompt budget,
// while the lower bound catches accidental truncation of the guidance.
expect(content.length).toBeGreaterThan(3_000);
- expect(content.length).toBeLessThan(7_000);
+ expect(content.length).toBeLessThan(8_000);
+ expect(content).toContain(
+ 'browser_session({ action: "start", browser: "" })',
+ );
+ expect(content).toMatch(/Never omit\s+`browser` or substitute another instance/);
expect(content).toContain("[visual:screenshot]");
expect(content).toContain("nextCursor");
expect(skill.source).toBe("bundled");
diff --git a/packages/i18n/src/locales/en-US/extension.json b/packages/i18n/src/locales/en-US/extension.json
index b79ac448..9e76942a 100644
--- a/packages/i18n/src/locales/en-US/extension.json
+++ b/packages/i18n/src/locales/en-US/extension.json
@@ -114,7 +114,13 @@
"remoteSavedDisabled": "Paired successfully. The connection switch is off; turn it on to connect.",
"remoteFormatError": "The pairing link is incomplete or invalid. Copy the full link provided by the server.",
"remotePermissionsInfo": "About remote connection permissions",
- "remotePermissionsHint": "The server can control dedicated task windows. Confirmation for borrowing existing tabs follows the Automation settings below."
+ "remotePermissionsHint": "The server can control dedicated task windows. Confirmation for borrowing existing tabs follows the Automation settings below.",
+ "profile": {
+ "hint": "Check that this is the browser profile you want to use, then send its instructions to your agent.",
+ "copyButton": "Copy profile instructions",
+ "copyFailed": "Could not copy the instructions. Please try again.",
+ "promptTemplate": "Use BrowserSkill in this browser profile for this task. Start every new session for this task with the appropriate interface:\nCLI: {{command}}\nDeepSeek Harness: {{toolCall}}\nIn DeepSeek Harness, use the tool call instead of running the CLI command separately.\nIf this instance is not connected, stop and ask me to reconnect this profile. Do not omit --browser / browser or switch to another instance."
+ }
},
"controlOverlay": {
"status": "Agent controlling",
diff --git a/packages/i18n/src/locales/ko-KR/extension.json b/packages/i18n/src/locales/ko-KR/extension.json
index bd87a0da..352c5bac 100644
--- a/packages/i18n/src/locales/ko-KR/extension.json
+++ b/packages/i18n/src/locales/ko-KR/extension.json
@@ -114,7 +114,13 @@
"remoteSavedDisabled": "페어링이 완료되었습니다. 연결 스위치가 꺼져 있습니다. 연결하려면 스위치를 켜세요.",
"remoteFormatError": "페어링 링크가 불완전하거나 올바르지 않습니다. 서버에서 제공한 전체 링크를 복사하세요.",
"remotePermissionsInfo": "원격 연결 권한 안내",
- "remotePermissionsHint": "서버는 전용 작업 창을 제어할 수 있습니다. 기존 탭을 빌릴 때의 확인 여부는 아래 자동화 설정을 따릅니다."
+ "remotePermissionsHint": "서버는 전용 작업 창을 제어할 수 있습니다. 기존 탭을 빌릴 때의 확인 여부는 아래 자동화 설정을 따릅니다.",
+ "profile": {
+ "hint": "사용하려는 브라우저 프로필이 맞는지 확인한 뒤 에이전트에게 지침을 보내세요.",
+ "copyButton": "프로필 지침 복사",
+ "copyFailed": "지침을 복사하지 못했습니다. 다시 시도해 주세요.",
+ "promptTemplate": "이 작업에는 이 브라우저 프로필의 BrowserSkill을 사용하세요. 이 작업의 새 세션을 시작할 때마다 해당 인터페이스를 사용하세요:\nCLI: {{command}}\nDeepSeek Harness: {{toolCall}}\nDeepSeek Harness에서는 CLI 명령을 별도로 실행하지 말고 도구 호출을 사용하세요.\n이 인스턴스가 연결되어 있지 않으면 중단하고 이 프로필을 다시 연결해 달라고 요청하세요. --browser / browser를 생략하거나 다른 인스턴스로 전환하지 마세요."
+ }
},
"controlOverlay": {
"status": "에이전트가 제어 중",
diff --git a/packages/i18n/src/locales/zh-CN/extension.json b/packages/i18n/src/locales/zh-CN/extension.json
index 04735e4d..996ad519 100644
--- a/packages/i18n/src/locales/zh-CN/extension.json
+++ b/packages/i18n/src/locales/zh-CN/extension.json
@@ -114,7 +114,13 @@
"remoteSavedDisabled": "配对成功,连接开关当前关闭。开启后即可连接。",
"remoteFormatError": "配对链接不完整或格式不正确,请重新复制服务器提供的完整链接。",
"remotePermissionsInfo": "远程连接权限说明",
- "remotePermissionsHint": "服务器可操作独立任务窗口。借用已有标签页时,是否请求确认取决于下方的“自动化设置”。"
+ "remotePermissionsHint": "服务器可操作独立任务窗口。借用已有标签页时,是否请求确认取决于下方的“自动化设置”。",
+ "profile": {
+ "hint": "确认当前是你要使用的浏览器个人资料,再将此处的指令发给 Agent。",
+ "copyButton": "复制此 Profile 的指令",
+ "copyFailed": "复制指令失败,请重试。",
+ "promptTemplate": "本次任务请使用此浏览器个人资料中的 BrowserSkill。为本次任务创建每个新会话时,请使用对应接口:\nCLI:{{command}}\nDeepSeek Harness:{{toolCall}}\n在 DeepSeek Harness 中请使用工具调用,不要另行执行 CLI 命令。\n如果此实例未连接,请停止并让我重新连接此 Profile。不要省略 --browser / browser,也不要切换到其他实例。"
+ }
},
"controlOverlay": {
"status": "Agent 正在控制",
diff --git a/skill/SKILL.md b/skill/SKILL.md
index 98768eb8..d8c1110c 100644
--- a/skill/SKILL.md
+++ b/skill/SKILL.md
@@ -45,12 +45,34 @@ environment settings may not persist between shell calls. Keep browser commands
sandboxed. For other startup failures, retry once, then use `bsk doctor`.
A local process identity warning permits browser commands when IPC works.
+## Required browser profiles
+
+When the user requires a particular browser profile, bind the task to that
+profile's extension instance before starting a session, even if only one browser
+is connected. A Chrome profile name or directory is not a BrowserSkill instance
+ID or an automatically assigned label.
+
+Use the instance ID from the BrowserSkill popup in the required profile. The user
+can choose **Copy profile instructions** there and send the resulting instruction.
+If only a profile name/path is supplied and its mapping is unknown, ask the user
+to open that profile, verify its Profile Path at `chrome://version`, and copy the
+profile instructions. Do not infer the mapping from a single Connected browser
+or Chrome process command lines.
+
+Run `bsk browsers --json` to check that the supplied instance is connected, then
+pass `--browser ` on every new session for this task. A previously
+verified unique label also works. If the target is missing or ambiguous, stop and
+report it; never omit the selector or substitute another instance to recover.
+Opening another Chrome profile does not retarget an existing session. After an
+extension reinstall or storage reset, obtain the instance mapping again.
+
## Task workflow
-1. Define success from the user's request. Start `bsk session start --json` and
- retain its `session_id`. With multiple browsers, run `bsk browsers` and add
- `--browser ` to start. For background work, add `--no-focus` to
- `session start` only.
+1. Define success from the user's request. For a required browser profile, follow
+ **Required browser profiles** above and start with its explicit `--browser`
+ selector. Otherwise start `bsk session start --json`; with multiple browsers,
+ run `bsk browsers` and choose `--browser `. Retain the returned
+ `session_id`. For background work, add `--no-focus` to `session start` only.
2. For a new page, navigate; for an existing user tab, follow **Borrowing** below.
Read the page before interacting: