@@ -14,9 +14,11 @@ const {
1414 addCodexConfigArgs,
1515 addCodexModelArgs,
1616 addCodexSandboxArgs,
17+ assertClaudeAuthContextSupported,
1718 assertClaudeVersionAllowed,
1819 buildAcpxJsonArgs,
1920 claudeArgs,
21+ claudeAuthContext,
2022 claudeEffort,
2123 claudeEnv,
2224 claudeExitCode,
@@ -711,6 +713,7 @@ describe("Claude provider helpers", () => {
711713 { type : "object" } ,
712714 { model : null , reasoningEffort : null , skipGitRepoCheck : false } ,
713715 true ,
716+ "isolated" ,
714717 ) ;
715718
716719 expect ( args ) . toEqual ( [
@@ -738,6 +741,7 @@ describe("Claude provider helpers", () => {
738741 { type : "object" } ,
739742 { model : null , reasoningEffort : null , skipGitRepoCheck : false } ,
740743 false ,
744+ "isolated" ,
741745 ) ;
742746
743747 expect ( args ) . toContain ( "default" ) ;
@@ -746,6 +750,20 @@ describe("Claude provider helpers", () => {
746750 expect ( args ) . not . toContain ( "dontAsk" ) ;
747751 } ) ;
748752
753+ it ( "uses safe mode instead of bare mode for host auth context" , ( ) => {
754+ const args = claudeArgs (
755+ { type : "object" } ,
756+ { model : null , reasoningEffort : null , skipGitRepoCheck : false } ,
757+ true ,
758+ "host" ,
759+ ) ;
760+
761+ expect ( args ) . toContain ( "--safe-mode" ) ;
762+ expect ( args ) . not . toContain ( "--bare" ) ;
763+ expect ( args ) . toContain ( "--strict-mcp-config" ) ;
764+ expect ( args ) . toContain ( "--disable-slash-commands" ) ;
765+ } ) ;
766+
749767 it ( "passes model and supported effort while ignoring skipGitRepoCheck" , ( ) => {
750768 const args = [ "-p" ] ;
751769
@@ -776,7 +794,7 @@ describe("Claude provider helpers", () => {
776794 CLAUDE_CODE_OAUTH_TOKEN : "must-not-leak" ,
777795 } ;
778796
779- expect ( claudeEnv ( false , "/tmp/claude" ) ) . toEqual ( {
797+ expect ( claudeEnv ( false , "/tmp/claude" , "isolated" ) ) . toEqual ( {
780798 PATH : "/bin" ,
781799 HOME : "/tmp/claude/home" ,
782800 XDG_CONFIG_HOME : "/tmp/claude/xdg-config" ,
@@ -787,7 +805,7 @@ describe("Claude provider helpers", () => {
787805 TMP : "/tmp/claude" ,
788806 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB : "1" ,
789807 } ) ;
790- expect ( claudeEnv ( true , "/tmp/claude" ) ) . toEqual ( {
808+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . toEqual ( {
791809 PATH : "/bin" ,
792810 HOME : "/tmp/claude/home" ,
793811 XDG_CONFIG_HOME : "/tmp/claude/xdg-config" ,
@@ -801,6 +819,53 @@ describe("Claude provider helpers", () => {
801819 } ) ;
802820 } ) ;
803821
822+ it ( "exposes only Claude host auth locators in host auth context" , ( ) => {
823+ process . env = {
824+ PATH : "/bin" ,
825+ HOME : "/host-home" ,
826+ USERPROFILE : "C:\\Users\\operator" ,
827+ CLAUDE_CONFIG_DIR : "/host-claude-config" ,
828+ CLAUDE_CODE_OAUTH_TOKEN : "oauth-token" ,
829+ ANTHROPIC_API_KEY : "api-key" ,
830+ OPENAI_API_KEY : "must-not-leak" ,
831+ DATABASE_URL : "must-not-leak" ,
832+ } ;
833+
834+ expect ( claudeEnv ( true , "/tmp/claude" , "host" ) ) . toEqual ( {
835+ PATH : "/bin" ,
836+ HOME : "/host-home" ,
837+ USERPROFILE : "C:\\Users\\operator" ,
838+ CLAUDE_CONFIG_DIR : "/host-claude-config" ,
839+ XDG_CONFIG_HOME : "/tmp/claude/xdg-config" ,
840+ XDG_CACHE_HOME : "/tmp/claude/xdg-cache" ,
841+ XDG_DATA_HOME : "/tmp/claude/xdg-data" ,
842+ TMPDIR : "/tmp/claude" ,
843+ TEMP : "/tmp/claude" ,
844+ TMP : "/tmp/claude" ,
845+ CLAUDE_CODE_SUBPROCESS_ENV_SCRUB : "1" ,
846+ CLAUDE_CODE_OAUTH_TOKEN : "oauth-token" ,
847+ ANTHROPIC_API_KEY : "api-key" ,
848+ } ) ;
849+ } ) ;
850+
851+ it ( "validates Claude auth context and safe-mode version" , ( ) => {
852+ delete process . env [ "CLAWPATCH_CLAUDE_AUTH_CONTEXT" ] ;
853+ expect ( claudeAuthContext ( ) ) . toBe ( "isolated" ) ;
854+
855+ process . env [ "CLAWPATCH_CLAUDE_AUTH_CONTEXT" ] = "host" ;
856+ expect ( claudeAuthContext ( ) ) . toBe ( "host" ) ;
857+ expect ( ( ) => assertClaudeAuthContextSupported ( "2.1.169 (Claude Code)" , "host" ) ) . not . toThrow ( ) ;
858+ expect ( ( ) => assertClaudeAuthContextSupported ( "2.1.168 (Claude Code)" , "host" ) ) . toThrow (
859+ / 2 \. 1 \. 1 6 9 o r n e w e r / u,
860+ ) ;
861+ expect ( ( ) => assertClaudeAuthContextSupported ( "unknown" , "host" ) ) . toThrow (
862+ / 2 \. 1 \. 1 6 9 o r n e w e r / u,
863+ ) ;
864+
865+ process . env [ "CLAWPATCH_CLAUDE_AUTH_CONTEXT" ] = "everything" ;
866+ expect ( ( ) => claudeAuthContext ( ) ) . toThrow ( / m u s t b e i s o l a t e d o r h o s t / u) ;
867+ } ) ;
868+
804869 it ( "passes Vertex AI auth env vars only when auth is included" , ( ) => {
805870 process . env = {
806871 PATH : "/bin" ,
@@ -819,7 +884,7 @@ describe("Claude provider helpers", () => {
819884 OPENAI_API_KEY : "must-not-leak" ,
820885 } ;
821886
822- expect ( claudeEnv ( false , "/tmp/claude" ) ) . toEqual ( {
887+ expect ( claudeEnv ( false , "/tmp/claude" , "isolated" ) ) . toEqual ( {
823888 PATH : "/bin" ,
824889 HOME : "/tmp/claude/home" ,
825890 XDG_CONFIG_HOME : "/tmp/claude/xdg-config" ,
@@ -830,7 +895,7 @@ describe("Claude provider helpers", () => {
830895 TMP : "/tmp/claude" ,
831896 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB : "1" ,
832897 } ) ;
833- expect ( claudeEnv ( true , "/tmp/claude" ) ) . toMatchObject ( {
898+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . toMatchObject ( {
834899 CLAUDE_CODE_USE_VERTEX : "1" ,
835900 ANTHROPIC_BASE_URL : "https://llm-gateway.example.com" ,
836901 ANTHROPIC_AUTH_TOKEN : "gateway-token" ,
@@ -844,7 +909,7 @@ describe("Claude provider helpers", () => {
844909 CLOUDSDK_CORE_PROJECT : "sdk-project" ,
845910 CLAUDE_CODE_SKIP_VERTEX_AUTH : "1" ,
846911 } ) ;
847- expect ( claudeEnv ( true , "/tmp/claude" ) ) . not . toHaveProperty ( "OPENAI_API_KEY" ) ;
912+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . not . toHaveProperty ( "OPENAI_API_KEY" ) ;
848913 } ) ;
849914
850915 it ( "passes Bedrock auth env vars only when auth is included" , ( ) => {
@@ -867,8 +932,10 @@ describe("Claude provider helpers", () => {
867932 DATABASE_URL : "must-not-leak" ,
868933 } ;
869934
870- expect ( claudeEnv ( false , "/tmp/claude" ) ) . not . toHaveProperty ( "CLAUDE_CODE_USE_BEDROCK" ) ;
871- expect ( claudeEnv ( true , "/tmp/claude" ) ) . toMatchObject ( {
935+ expect ( claudeEnv ( false , "/tmp/claude" , "isolated" ) ) . not . toHaveProperty (
936+ "CLAUDE_CODE_USE_BEDROCK" ,
937+ ) ;
938+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . toMatchObject ( {
872939 CLAUDE_CODE_USE_BEDROCK : "1" ,
873940 CLAUDE_CODE_SKIP_BEDROCK_AUTH : "1" ,
874941 ANTHROPIC_BEDROCK_BASE_URL : "https://bedrock-runtime.us-east-1.amazonaws.com" ,
@@ -884,7 +951,7 @@ describe("Claude provider helpers", () => {
884951 AWS_ROLE_ARN : "arn:aws:iam::123456789012:role/clawpatch" ,
885952 AWS_WEB_IDENTITY_TOKEN_FILE : "/var/aws/web-identity-token" ,
886953 } ) ;
887- expect ( claudeEnv ( true , "/tmp/claude" ) ) . not . toHaveProperty ( "DATABASE_URL" ) ;
954+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . not . toHaveProperty ( "DATABASE_URL" ) ;
888955 } ) ;
889956
890957 it ( "passes AWS_PROFILE only with explicit AWS config or credentials file paths" , ( ) => {
@@ -895,17 +962,17 @@ describe("Claude provider helpers", () => {
895962 AWS_PROFILE : "clawpatch" ,
896963 } ;
897964
898- expect ( claudeEnv ( true , "/tmp/claude" ) ) . not . toHaveProperty ( "AWS_PROFILE" ) ;
965+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . not . toHaveProperty ( "AWS_PROFILE" ) ;
899966
900967 process . env [ "AWS_CONFIG_FILE" ] = "/var/aws/config" ;
901- expect ( claudeEnv ( true , "/tmp/claude" ) ) . toMatchObject ( {
968+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . toMatchObject ( {
902969 AWS_CONFIG_FILE : "/var/aws/config" ,
903970 AWS_PROFILE : "clawpatch" ,
904971 } ) ;
905972
906973 delete process . env [ "AWS_CONFIG_FILE" ] ;
907974 process . env [ "AWS_SHARED_CREDENTIALS_FILE" ] = "/var/aws/credentials" ;
908- expect ( claudeEnv ( true , "/tmp/claude" ) ) . toMatchObject ( {
975+ expect ( claudeEnv ( true , "/tmp/claude" , "isolated" ) ) . toMatchObject ( {
909976 AWS_SHARED_CREDENTIALS_FILE : "/var/aws/credentials" ,
910977 AWS_PROFILE : "clawpatch" ,
911978 } ) ;
@@ -917,11 +984,11 @@ describe("Claude provider helpers", () => {
917984 ANTHROPIC_API_KEY : "secret" ,
918985 } ;
919986
920- expect ( claudeEnv ( true , "C:\\Temp\\claude" ) ) . toMatchObject ( {
987+ expect ( claudeEnv ( true , "C:\\Temp\\claude" , "isolated" ) ) . toMatchObject ( {
921988 Path : "C:\\Tools" ,
922989 ANTHROPIC_API_KEY : "secret" ,
923990 } ) ;
924- expect ( claudeEnv ( true , "C:\\Temp\\claude" ) ) . not . toHaveProperty ( "PATH" ) ;
991+ expect ( claudeEnv ( true , "C:\\Temp\\claude" , "isolated" ) ) . not . toHaveProperty ( "PATH" ) ;
925992 } ) ;
926993
927994 it ( "extracts structured_output from Claude JSON envelopes" , ( ) => {
@@ -985,6 +1052,32 @@ describe("Claude provider helpers", () => {
9851052 throw new Error ( "expected Claude provider failure" ) ;
9861053 } ) ;
9871054
1055+ it ( "classifies string Claude error envelopes" , ( ) => {
1056+ try {
1057+ extractClaudeStructuredOutput ( JSON . stringify ( { error : "authentication_failed" } ) ) ;
1058+ } catch ( err ) {
1059+ expect ( err ) . toBeInstanceOf ( ClawpatchError ) ;
1060+ expect ( ( err as ClawpatchError ) . message ) . toContain ( "authentication_failed" ) ;
1061+ expect ( ( err as ClawpatchError ) . exitCode ) . toBe ( 4 ) ;
1062+ return ;
1063+ }
1064+ throw new Error ( "expected Claude provider failure" ) ;
1065+ } ) ;
1066+
1067+ it ( "does not preview arbitrary or token-shaped string Claude errors" , ( ) => {
1068+ for ( const secret of [ "SOURCE CONTEXT SECRET" , "sk-ant-secret-shaped-value" ] ) {
1069+ try {
1070+ extractClaudeStructuredOutput ( JSON . stringify ( { error : secret } ) ) ;
1071+ } catch ( err ) {
1072+ expect ( err ) . toBeInstanceOf ( ClawpatchError ) ;
1073+ expect ( ( err as ClawpatchError ) . message ) . toBe ( "claude provider error: provider-error" ) ;
1074+ expect ( ( err as ClawpatchError ) . message ) . not . toContain ( secret ) ;
1075+ continue ;
1076+ }
1077+ throw new Error ( "expected Claude provider failure" ) ;
1078+ }
1079+ } ) ;
1080+
9881081 it ( "does not include stdout or prompt previews in Claude failure messages" , ( ) => {
9891082 const message = claudeFailureMessage ( "SOURCE_CONTEXT_SECRET" , "SOURCE_CONTEXT_SECRET" , 1 ) ;
9901083
@@ -1013,7 +1106,8 @@ describe("Claude provider helpers", () => {
10131106
10141107 const message = claudeFailureMessage ( stdout , "" , 1 ) ;
10151108
1016- expect ( message ) . toBe ( "claude provider auth/config failed" ) ;
1109+ expect ( message ) . toContain ( "claude provider auth/config failed" ) ;
1110+ expect ( message ) . toContain ( "error=authentication_failed" ) ;
10171111 expect ( message ) . not . toContain ( "SOURCE_CONTEXT_SECRET" ) ;
10181112 expect ( claudeExitCode ( stdout , "" , 1 ) ) . toBe ( 4 ) ;
10191113 } ) ;
@@ -1034,14 +1128,36 @@ describe("Claude provider helpers", () => {
10341128 result : "SOURCE_CONTEXT_SECRET" ,
10351129 } ) ;
10361130
1037- expect ( claudeFailureMessage ( auth , "" , 1 ) ) . toBe ( "claude provider auth/config failed" ) ;
1131+ expect ( claudeFailureMessage ( auth , "" , 1 ) ) . toContain ( "claude provider auth/config failed" ) ;
10381132 expect ( claudeExitCode ( auth , "" , 1 ) ) . toBe ( 4 ) ;
1039- expect ( claudeFailureMessage ( quota , "" , 1 ) ) . toBe ( "claude provider quota/rate-limit failed" ) ;
1133+ expect ( claudeFailureMessage ( quota , "" , 1 ) ) . toContain ( "claude provider quota/rate-limit failed" ) ;
10401134 expect ( claudeExitCode ( quota , "" , 1 ) ) . toBe ( 5 ) ;
10411135 expect ( claudeFailureMessage ( auth , "" , 1 ) ) . not . toContain ( "SOURCE_CONTEXT_SECRET" ) ;
10421136 expect ( claudeFailureMessage ( quota , "" , 1 ) ) . not . toContain ( "SOURCE_CONTEXT_SECRET" ) ;
10431137 } ) ;
10441138
1139+ it ( "surfaces the reported OAuth failure shape without leaking result text" , ( ) => {
1140+ const stdout = [
1141+ JSON . stringify ( { type : "assistant" , error : "authentication_failed" } ) ,
1142+ JSON . stringify ( {
1143+ type : "result" ,
1144+ subtype : "success" ,
1145+ is_error : true ,
1146+ result : "Not logged in · Please run /login" ,
1147+ terminal_reason : "completed" ,
1148+ } ) ,
1149+ ] . join ( "\n" ) ;
1150+
1151+ const message = claudeFailureMessage ( stdout , "" , 1 ) ;
1152+
1153+ expect ( message ) . toContain ( "claude provider auth/config failed" ) ;
1154+ expect ( message ) . toContain ( "error=authentication_failed" ) ;
1155+ expect ( message ) . toContain ( "is_error=true" ) ;
1156+ expect ( message ) . toContain ( "reason=not-logged-in" ) ;
1157+ expect ( message ) . not . toContain ( "Please run /login" ) ;
1158+ expect ( claudeExitCode ( stdout , "" , 1 ) ) . toBe ( 4 ) ;
1159+ } ) ;
1160+
10451161 it ( "omits Claude error.message from stdout failure signals" , ( ) => {
10461162 const stdout = JSON . stringify ( {
10471163 type : "result" ,
0 commit comments