Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-contexts-shine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/browse-cli": patch
---

Add context label resolution for --context-id flag
12 changes: 11 additions & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ function getLocalInfoPath(session: string): string {
return path.join(SOCKET_DIR, `browse-${session}.local-info`);
}

// ==================== CONTEXT LABEL RESOLUTION ====================

import { resolveContextLabel } from "./resolve-context";

// ==================== LOCAL STRATEGY CONFIG ====================

type LocalStrategy = "auto" | "isolated" | "cdp";
Expand Down Expand Up @@ -2163,8 +2167,14 @@ program
process.exit(1);
}

// Resolve named labels (e.g. "latest", "work") to actual context IDs
const resolvedContextId = await resolveContextLabel(cmdOpts.contextId);
if (resolvedContextId !== cmdOpts.contextId) {
console.error(`Resolved context "${cmdOpts.contextId}" → ${resolvedContextId}`);
}

const newConfig = JSON.stringify({
id: cmdOpts.contextId,
id: resolvedContextId,
persist: cmdOpts.persist ?? false,
});

Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/resolve-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";

/**
* Resolve a --context-id value that may be a named label.
*
* Label files live at:
* <configDir>/contexts/<label> → raw UUID
*
* The base path respects BROWSERBASE_CONFIG_DIR env var.
*/
export async function resolveContextLabel(value: string): Promise<string> {
// Raw UUIDs and ctx_ prefixes pass through
if (/^[0-9a-f-]{36}$/i.test(value) || value.startsWith("ctx_")) {
return value;
}
// Look up as a label file
const configDir =
process.env.BROWSERBASE_CONFIG_DIR ||
path.join(os.homedir(), ".config", "browserbase");
const labelPath = path.join(configDir, "contexts", value);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unvalidated --context-id label input allows path traversal outside the contexts directory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/resolve-context.ts, line 22:

<comment>Unvalidated `--context-id` label input allows path traversal outside the `contexts` directory.</comment>

<file context>
@@ -0,0 +1,33 @@
+  const configDir =
+    process.env.BROWSERBASE_CONFIG_DIR ||
+    path.join(os.homedir(), ".config", "browserbase");
+  const labelPath = path.join(configDir, "contexts", value);
+  try {
+    const id = (await fs.readFile(labelPath, "utf-8")).trim();
</file context>
Fix with Cubic

try {
const id = (await fs.readFile(labelPath, "utf-8")).trim();
if (id) {
return id;
}
} catch {
// Label file doesn't exist – fall through
}
// Return as-is (might be a raw ID in a format we don't recognize)
return value;
}
63 changes: 63 additions & 0 deletions packages/cli/tests/resolve-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { resolveContextLabel } from "../src/resolve-context";

let tmpDir: string;
let contextsDir: string;
const originalEnv = process.env.BROWSERBASE_CONFIG_DIR;

beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "browse-ctx-test-"));
contextsDir = path.join(tmpDir, "contexts");
fs.mkdirSync(contextsDir, { recursive: true });
process.env.BROWSERBASE_CONFIG_DIR = tmpDir;

// Write test label files
fs.writeFileSync(
path.join(contextsDir, "latest"),
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
);
fs.writeFileSync(
path.join(contextsDir, "work"),
"11111111-2222-3333-4444-555555555555\n",
);
fs.writeFileSync(path.join(contextsDir, "empty"), "");
});

afterAll(() => {
process.env.BROWSERBASE_CONFIG_DIR = originalEnv;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Restoring the env var by assigning undefined leaves BROWSERBASE_CONFIG_DIR set to the literal string "undefined" when it was originally unset. Delete the env var instead when originalEnv is undefined to avoid leaking state into other tests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/tests/resolve-context.test.ts, line 30:

<comment>Restoring the env var by assigning `undefined` leaves `BROWSERBASE_CONFIG_DIR` set to the literal string "undefined" when it was originally unset. Delete the env var instead when `originalEnv` is undefined to avoid leaking state into other tests.</comment>

<file context>
@@ -0,0 +1,63 @@
+});
+
+afterAll(() => {
+  process.env.BROWSERBASE_CONFIG_DIR = originalEnv;
+  fs.rmSync(tmpDir, { recursive: true, force: true });
+});
</file context>
Fix with Cubic

fs.rmSync(tmpDir, { recursive: true, force: true });
});

describe("resolveContextLabel", () => {
it("passes through raw UUIDs unchanged", async () => {
const uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
expect(await resolveContextLabel(uuid)).toBe(uuid);
});

it("passes through ctx_ prefixed IDs unchanged", async () => {
expect(await resolveContextLabel("ctx_abc123")).toBe("ctx_abc123");
});

it("resolves a label to its context ID", async () => {
expect(await resolveContextLabel("latest")).toBe(
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
);
});

it("trims whitespace from label file contents", async () => {
expect(await resolveContextLabel("work")).toBe(
"11111111-2222-3333-4444-555555555555",
);
});

it("returns the value as-is when label file does not exist", async () => {
expect(await resolveContextLabel("nonexistent")).toBe("nonexistent");
});

it("returns the value as-is when label file is empty", async () => {
expect(await resolveContextLabel("empty")).toBe("empty");
});
});
Loading