Skip to content
Closed
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 docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,11 @@ Two host-daemon environment variables reach the Claude Code bridge:
such as a CI runner (mint it with `claude setup-token`). See the
`claude-code-provider` skill.

Claude Code sessions and the model and command probes run with
`CLAUDE_CODE_ENTRYPOINT=bb`, replacing an inherited value. A
`CLAUDE_CODE_ENTRYPOINT` in the thread's environment variables wins for
sessions.

### Account Pooler [Experimental]

The builtin Account Pooler plugin pools Claude and Codex accounts behind the bb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ with `bb plugin config provider-claude-code set <key> <value>`.
- bb passes only `BB_CLAUDE_CODE_EXECUTABLE` and `CLAUDE_CODE_OAUTH_TOKEN` to
the CLI. Mint the token with `claude setup-token` for machines with no
interactive login.
- Claude Code sessions and the model and command probes run with
`CLAUDE_CODE_ENTRYPOINT=bb`, replacing an inherited value, so Anthropic sees
`cc_entrypoint=bb`. A `CLAUDE_CODE_ENTRYPOINT` in the thread's environment
variables wins for sessions.
- Claude Code's own slash commands, including bundled skills such as
`/simplify` and `/code-review`, appear in the composer's `/` menu,
`bb project commands`, and `bb thread commands`. The provider reads them from
Expand Down
66 changes: 65 additions & 1 deletion plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3718,6 +3718,7 @@ describe("bridge", () => {
...process.env,
ANTHROPIC_MODEL: "claude-previous",
CLAUDE_CODE_CLIENT_DATA_URL: "https://example.com/client-data",
CLAUDE_CODE_ENTRYPOINT: "inherited",
};

await expect(listClaudeCodeBridgeModels(env)).resolves.toMatchObject({
Expand All @@ -3727,6 +3728,7 @@ describe("bridge", () => {
expect(queryMock.mock.calls[1]?.[0]?.options.env).toEqual({
...env,
ANTHROPIC_MODEL: undefined,
CLAUDE_CODE_ENTRYPOINT: "bb",
});
expect(env.ANTHROPIC_MODEL).toBe("claude-previous");
expect(failedClose).toHaveBeenCalledOnce();
Expand Down Expand Up @@ -3780,7 +3782,7 @@ describe("bridge", () => {

const queryOptions = getLatestQueryOptions();
expect(queryOptions.env?.HOME).toBe("/Users/test-bb");
expect(queryOptions.env?.CLAUDE_CODE_ENTRYPOINT).toBe("cli");
expect(queryOptions.env?.CLAUDE_CODE_ENTRYPOINT).toBe("bb");
expect(queryOptions.env?.CLAUDE_AGENT_SDK_CLIENT_APP).toBeUndefined();
expect(queryOptions.settingSources).toEqual(["user", "project", "local"]);

Expand All @@ -3803,6 +3805,68 @@ describe("bridge", () => {
}
});

it.each([
{ configured: undefined, expected: "bb" },
{ configured: "custom", expected: "custom" },
])(
"replaces an inherited Claude entrypoint with $expected on thread/start",
async ({ configured, expected }) => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
queryMock.mockImplementation(() => {
const query = createControlledClaudeQuery();
queries.push(query);
return query;
});

const threadId = `thread-entrypoint-${expected}`;
const originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT;
process.env.CLAUDE_CODE_ENTRYPOINT = "inherited";
try {
bridge.sendRequest(1, "thread/start", {
threadId,
cwd: "/tmp/worktree",
instructionMode: "append",
options: {
permissionMode: "accept-edits",
permissionScope: "workspace",
approvalReviewer: "user",
permissionEscalation: "ask",
instructions: "test",
...(configured
? { envVars: { CLAUDE_CODE_ENTRYPOINT: configured } }
: {}),
providerOptions: {
workflowsEnabled: false,
},
},
});
await bridge.waitForResponse(1);

expect(getLatestQueryOptions().env?.CLAUDE_CODE_ENTRYPOINT).toBe(
expected,
);

bridge.sendRequest(2, "thread/stop", {
threadId,
providerThreadId: threadId,
intent: "interrupt",
activeTurnId: null,
});
await bridge.flushWork();
queries[0]?.finish();
await bridge.waitForResponse(2);
} finally {
if (originalEntrypoint === undefined) {
delete process.env.CLAUDE_CODE_ENTRYPOINT;
} else {
process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint;
}
bridge.restore();
}
},
);

it("includes captured Claude stderr when the SDK stream fails", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
Expand Down
3 changes: 2 additions & 1 deletion plugins/provider-claude-code/src/bridge/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
getClaudeProviderUsage,
} from "./provider-maintenance.js";
import {
BB_CLAUDE_CODE_ENTRYPOINT,
buildChromeExtraArgs,
buildClaudeFeatureEnv,
buildReadonlyDenialMessage,
Expand Down Expand Up @@ -1786,8 +1787,8 @@ function buildSessionEnv(
): NodeJS.ProcessEnv {
const sessionEnv: NodeJS.ProcessEnv = {
...withoutBridgeRuntimeEnv(process.env),
CLAUDE_CODE_ENTRYPOINT: BB_CLAUDE_CODE_ENTRYPOINT,
...envOverrides,
CLAUDE_CODE_ENTRYPOINT: "cli",
};
delete sessionEnv.CLAUDE_AGENT_SDK_CLIENT_APP;
return sessionEnv;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
type SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { translateMissingClaudeCliCatalogError } from "./missing-cli-error.js";
import { resolveClaudeCodeExecutable } from "./session-options.js";
import {
BB_CLAUDE_CODE_ENTRYPOINT,
resolveClaudeCodeExecutable,
} from "./session-options.js";

async function* promptWithoutMessages(
signal: AbortSignal,
Expand All @@ -31,7 +34,7 @@ export async function probeClaudeInitialization(args: {
prompt: promptWithoutMessages(promptEnd.signal),
options: {
cwd: args.cwd,
env: args.env,
env: { ...args.env, CLAUDE_CODE_ENTRYPOINT: BB_CLAUDE_CODE_ENTRYPOINT },
persistSession: false,
settingSources: ["user", "project", "local"],
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
Expand Down
2 changes: 2 additions & 0 deletions plugins/provider-claude-code/src/bridge/session-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ function wellKnownClaudeExecutablePaths(env: NodeJS.ProcessEnv): string[] {
return candidatePaths;
}

export const BB_CLAUDE_CODE_ENTRYPOINT = "bb";

export function resolveClaudeCodeExecutable(
args: ResolveClaudeCodeExecutableArgs,
): string | null {
Expand Down
Loading