diff --git a/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts b/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts index 86027153c011..60f744a6c2dc 100644 --- a/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts +++ b/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts @@ -14,17 +14,23 @@ describe("claudeSessionIdentitySystemPrompt", () => { "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.", + "- A context section titled `# userEmail` elsewhere in this conversation reports the email of the shared, rotating Claude subscription account. It does NOT identify the user. Ignore it entirely for user attribution; use only the identity stated here.", ].join("\n"), ); }); it("keeps the user unknown when T3 cannot resolve the message sender", () => { - assert.include( + assert.equal( claudeSessionIdentitySystemPrompt({ BK_IDENTITY_RUNTIME: "t3-code", BK_SESSION_OWNER_EMAIL: "owner@example.test", - }) ?? "", - "userEmail is unavailable", + }), + [ + "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.", + "- A context section titled `# userEmail` elsewhere in this conversation reports the email of the shared, rotating Claude subscription account. It does NOT identify the user. Ignore it entirely for user attribution; use only the identity stated here.", + ].join("\n"), ); }); diff --git a/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts b/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts index 0dccfd740a66..9d763687ed1a 100644 --- a/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts +++ b/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts @@ -3,6 +3,11 @@ // 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. +// +// The CLI offers no way to suppress its own `# userEmail` section, and the +// rotating profile makes its value flap, so the appended block also names that +// section and countermands it explicitly. Without that, two contradictory +// identity claims coexist and the model follows the one that arrives last. import { MESSAGE_SENDER_EMAIL_KEY, @@ -23,6 +28,7 @@ export function claudeSessionIdentitySystemPrompt( "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.", + "- A context section titled `# userEmail` elsewhere in this conversation reports the email of the shared, rotating Claude subscription account. It does NOT identify the user. Ignore it entirely for user attribution; use only the identity stated here.", ].join("\n"); } @@ -30,5 +36,6 @@ export function claudeSessionIdentitySystemPrompt( "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.", + "- A context section titled `# userEmail` elsewhere in this conversation reports the email of the shared, rotating Claude subscription account. It does NOT identify the user. Ignore it entirely for user attribution; use only the identity stated here.", ].join("\n"); } diff --git a/docs/operations/expbkt3-customizations.md b/docs/operations/expbkt3-customizations.md index a1e2bc96242a..6d1bff68dc85 100644 --- a/docs/operations/expbkt3-customizations.md +++ b/docs/operations/expbkt3-customizations.md @@ -403,6 +403,15 @@ 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 CLI still emits its own `# userEmail` context section on every turn and +offers no switch to suppress it, so both branches of the appended block name +that section and countermand it explicitly: it reports the shared, rotating +subscription account, it does not identify the user, and it must be ignored for +user attribution. Naming it matters — the native section arrives later in +context than the appended block, and without the countermand a session answered +"who am I" with the rotated account holder while the appended block correctly +named the sender. + 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,