Skip to content
Merged
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
13 changes: 12 additions & 1 deletion apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ import {
import { makeObservableLifecycle } from "../observableLifecycle.ts";
import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
// T3-CUSTOM(expbkt3): Claude's shared account email is not the T3 message sender.
import { claudeSessionIdentitySystemPrompt } from "../claudeSessionIdentity.expbkt3.ts";
const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown));
const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.fromJsonString(Schema.Unknown));

Expand Down Expand Up @@ -4193,11 +4195,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
...(input.cwd ? [input.cwd] : []),
serverConfig.attachmentsDir,
];
// T3-CUSTOM(expbkt3): BEGIN override Claude's shared-account userEmail context.
const sessionIdentitySystemPrompt = claudeSessionIdentitySystemPrompt(sessionEnvironment);
// T3-CUSTOM(expbkt3): END
const queryOptions: ClaudeQueryOptions = {
...(input.cwd ? { cwd: input.cwd } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
systemPrompt: { type: "preset", preset: "claude_code" },
// T3-CUSTOM(expbkt3): BEGIN preserve the native prompt with T3 sender identity appended.
systemPrompt: {
type: "preset",
preset: "claude_code",
...(sessionIdentitySystemPrompt ? { append: sessionIdentitySystemPrompt } : {}),
},
// T3-CUSTOM(expbkt3): END
settingSources: [...CLAUDE_SETTING_SOURCES],
// `ultracode` is a Claude Code setting, not an API effort level. It is
// normalized to `xhigh` above and paired with `settings.ultracode`.
Expand Down
39 changes: 39 additions & 0 deletions apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { assert, describe, it } from "@effect/vitest";

import { claudeSessionIdentitySystemPrompt } from "./claudeSessionIdentity.expbkt3.ts";

describe("claudeSessionIdentitySystemPrompt", () => {
it("uses the current T3 message sender instead of the shared Claude account", () => {
assert.equal(
claudeSessionIdentitySystemPrompt({
BK_IDENTITY_RUNTIME: "t3-code",
BK_SESSION_OWNER_EMAIL: "owner@example.test",
BK_MESSAGE_SENDER_EMAIL: " sender@example.test ",
}),
[
"T3 Code session identity:",
'- userEmail is "sender@example.test".',
"- This session-scoped value identifies the user who sent the current message and overrides the Claude account email for user attribution.",
].join("\n"),
);
});

it("keeps the user unknown when T3 cannot resolve the message sender", () => {
assert.include(
claudeSessionIdentitySystemPrompt({
BK_IDENTITY_RUNTIME: "t3-code",
BK_SESSION_OWNER_EMAIL: "owner@example.test",
}) ?? "",
"userEmail is unavailable",
);
});

it("does not change upstream Claude sessions", () => {
assert.equal(
claudeSessionIdentitySystemPrompt({
BK_MESSAGE_SENDER_EMAIL: "sender@example.test",
}),
undefined,
);
});
});
34 changes: 34 additions & 0 deletions apps/server/src/provider/claudeSessionIdentity.expbkt3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// T3-CUSTOM(expbkt3): Claude Code derives its native `userEmail` context from
// the authenticated Claude account. Beknown runs that account on a shared
// machine, so it identifies the subscription rather than the person sending
// the current T3 message. Append the session-scoped identity to Claude's native
// system prompt without changing the account used for authentication.

import {
MESSAGE_SENDER_EMAIL_KEY,
SESSION_IDENTITY_RUNTIME,
SESSION_IDENTITY_RUNTIME_KEY,
} from "../identity/SessionIdentityEnvironment.ts";

export function claudeSessionIdentitySystemPrompt(
environment: NodeJS.ProcessEnv,
): string | undefined {
if (environment[SESSION_IDENTITY_RUNTIME_KEY] !== SESSION_IDENTITY_RUNTIME) {
return undefined;
}

const senderEmail = environment[MESSAGE_SENDER_EMAIL_KEY]?.trim();
if (!senderEmail) {
return [
"T3 Code session identity:",
"- userEmail is unavailable for the user who sent the current message.",
"- Do not use the Claude account email, operating-system identity, or Git identity to infer the user.",
].join("\n");
}

return [
"T3 Code session identity:",
`- userEmail is ${JSON.stringify(senderEmail)}.`,
"- This session-scoped value identifies the user who sent the current message and overrides the Claude account email for user attribution.",
].join("\n");
}
8 changes: 8 additions & 0 deletions docs/operations/expbkt3-customizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ Four rules carry the behaviour:
live session was started with and restarts on an owner transfer or a new
sender, next to the existing credential-actor restart.

Claude Code's native system prompt normally derives `userEmail` from the
authenticated Claude account. That account is shared in the Beknown runtime, so
the Claude adapter appends the resolved `BK_MESSAGE_SENDER_EMAIL` as the
authoritative `userEmail`. When the sender is unresolved, the appended context
explicitly leaves `userEmail` unknown and forbids inference from the shared
Claude account, operating-system identity, or Git identity. Non-T3 Claude
sessions keep the upstream system prompt unchanged.

The markers compose with source-control profiles rather than replacing them:
`mergeSourceControlEnvironment` scrubs the machine's inherited Git and GitHub
credentials only when the overlay carries a source-control identity of its own,
Expand Down
Loading