1- use serde :: { Deserialize , Serialize } ;
1+ use std :: collections :: HashMap ;
22use std:: path:: { Path , PathBuf } ;
33
4+ use serde:: { Deserialize , Serialize } ;
5+
46/// Global config directory name.
57const CONFIG_DIR : & str = ".crab" ;
68/// Settings file name within config directories.
@@ -26,6 +28,9 @@ pub struct Settings {
2628 pub hooks : Option < serde_json:: Value > ,
2729 pub theme : Option < String > ,
2830 pub git_context : Option < GitContextConfig > ,
31+ /// Environment variables to inject into the process.
32+ /// CC-compatible: `{"env": {"ANTHROPIC_API_KEY": "sk-ant-xxx"}}`.
33+ pub env : Option < HashMap < String , String > > ,
2934}
3035
3136/// Configuration for git context injection into system prompts.
@@ -65,6 +70,16 @@ impl Settings {
6570 hooks : other. hooks . clone ( ) . or ( self . hooks ) ,
6671 theme : other. theme . clone ( ) . or ( self . theme ) ,
6772 git_context : other. git_context . clone ( ) . or ( self . git_context ) ,
73+ env : match ( & self . env , & other. env ) {
74+ ( Some ( base) , Some ( over) ) => {
75+ let mut merged = base. clone ( ) ;
76+ merged. extend ( over. iter ( ) . map ( |( k, v) | ( k. clone ( ) , v. clone ( ) ) ) ) ;
77+ Some ( merged)
78+ }
79+ ( None , Some ( over) ) => Some ( over. clone ( ) ) ,
80+ ( Some ( base) , None ) => Some ( base. clone ( ) ) ,
81+ ( None , None ) => None ,
82+ } ,
6883 }
6984 }
7085}
@@ -248,8 +263,19 @@ where
248263 merged
249264 } ;
250265
251- // 5. Environment variable overrides (always applied, highest priority)
252- let env_overlay = build_env_overlay ( & env_lookup, merged. api_provider . as_deref ( ) ) ;
266+ // 5. Environment variable overrides (always applied, highest priority).
267+ // The env_lookup falls back to settings.env (CC-compatible) so that
268+ // `{"env": {"ANTHROPIC_API_KEY": "sk-..."}}` in settings.json works.
269+ let settings_env = merged. env . clone ( ) ;
270+ let env_with_fallback = move |key : & str | -> std:: result:: Result < String , std:: env:: VarError > {
271+ env_lookup ( key) . or_else ( |_| {
272+ settings_env
273+ . as_ref ( )
274+ . and_then ( |m| m. get ( key) . cloned ( ) )
275+ . ok_or ( std:: env:: VarError :: NotPresent )
276+ } )
277+ } ;
278+ let env_overlay = build_env_overlay ( & env_with_fallback, merged. api_provider . as_deref ( ) ) ;
253279 Ok ( merged. merge ( & env_overlay) )
254280}
255281
@@ -262,7 +288,11 @@ where
262288 F : Fn ( & str ) -> std:: result:: Result < String , std:: env:: VarError > ,
263289{
264290 let api_provider = env_lookup ( "CRAB_API_PROVIDER" ) . ok ( ) ;
265- let api_base_url = env_lookup ( "CRAB_API_BASE_URL" ) . ok ( ) ;
291+ let api_base_url = env_lookup ( "CRAB_API_BASE_URL" )
292+ . ok ( )
293+ // CC-compatible: ANTHROPIC_BASE_URL as fallback
294+ . or_else ( || env_lookup ( "ANTHROPIC_BASE_URL" ) . ok ( ) )
295+ . filter ( |v| !v. is_empty ( ) ) ;
266296 let model = env_lookup ( "CRAB_MODEL" ) . ok ( ) ;
267297
268298 // For API key: CRAB_API_KEY takes priority, then provider-specific vars
@@ -293,7 +323,19 @@ where
293323 "openai" | "ollama" | "vllm" => "OPENAI_API_KEY" ,
294324 _ => "ANTHROPIC_API_KEY" ,
295325 } ;
296- env_lookup ( var_name) . ok ( ) . filter ( |v| !v. is_empty ( ) )
326+ env_lookup ( var_name)
327+ . ok ( )
328+ . filter ( |v| !v. is_empty ( ) )
329+ // CC-compatible: ANTHROPIC_AUTH_TOKEN as fallback for Anthropic providers
330+ . or_else ( || {
331+ if var_name == "ANTHROPIC_API_KEY" {
332+ env_lookup ( "ANTHROPIC_AUTH_TOKEN" )
333+ . ok ( )
334+ . filter ( |v| !v. is_empty ( ) )
335+ } else {
336+ None
337+ }
338+ } )
297339}
298340
299341#[ cfg( test) ]
@@ -465,6 +507,7 @@ mod tests {
465507 enabled : true ,
466508 max_diff_lines : 100 ,
467509 } ) ,
510+ env : None ,
468511 } ;
469512 let overlay = Settings {
470513 api_provider : Some ( "openai" . into ( ) ) ,
@@ -482,6 +525,7 @@ mod tests {
482525 enabled : false ,
483526 max_diff_lines : 50 ,
484527 } ) ,
528+ env : None ,
485529 } ;
486530 let merged = base. merge ( & overlay) ;
487531 assert_eq ! ( merged. api_provider. as_deref( ) , Some ( "openai" ) ) ;
@@ -545,6 +589,7 @@ mod tests {
545589 hooks : Some ( serde_json:: json!( [ { "trigger" : "pre_tool_use" , "command" : "echo" } ] ) ) ,
546590 theme : Some ( "dark" . into ( ) ) ,
547591 git_context : Some ( GitContextConfig :: default ( ) ) ,
592+ env : Some ( HashMap :: from ( [ ( "FOO" . into ( ) , "bar" . into ( ) ) ] ) ) ,
548593 } ;
549594 let json = serde_json:: to_string_pretty ( & s) . unwrap ( ) ;
550595 let deserialized: Settings = serde_json:: from_str ( & json) . unwrap ( ) ;
@@ -696,6 +741,39 @@ mod tests {
696741 assert_eq ! ( provider_api_key_env( "unknown" , & env) , Some ( "ant" . into( ) ) ) ;
697742 }
698743
744+ #[ test]
745+ fn build_env_overlay_anthropic_auth_token_fallback ( ) {
746+ let env = fake_env ( HashMap :: from ( [ ( "ANTHROPIC_AUTH_TOKEN" , "cr_token123" ) ] ) ) ;
747+ let overlay = build_env_overlay ( & env, None ) ;
748+ assert_eq ! ( overlay. api_key. as_deref( ) , Some ( "cr_token123" ) ) ;
749+ }
750+
751+ #[ test]
752+ fn build_env_overlay_anthropic_base_url_fallback ( ) {
753+ let env = fake_env ( HashMap :: from ( [ (
754+ "ANTHROPIC_BASE_URL" ,
755+ "http://proxy.example.com/api" ,
756+ ) ] ) ) ;
757+ let overlay = build_env_overlay ( & env, None ) ;
758+ assert_eq ! (
759+ overlay. api_base_url. as_deref( ) ,
760+ Some ( "http://proxy.example.com/api" )
761+ ) ;
762+ }
763+
764+ #[ test]
765+ fn build_env_overlay_crab_base_url_overrides_anthropic ( ) {
766+ let env = fake_env ( HashMap :: from ( [
767+ ( "CRAB_API_BASE_URL" , "http://crab.example.com" ) ,
768+ ( "ANTHROPIC_BASE_URL" , "http://anthropic.example.com" ) ,
769+ ] ) ) ;
770+ let overlay = build_env_overlay ( & env, None ) ;
771+ assert_eq ! (
772+ overlay. api_base_url. as_deref( ) ,
773+ Some ( "http://crab.example.com" )
774+ ) ;
775+ }
776+
699777 #[ test]
700778 fn load_merged_env_overrides_project ( ) {
701779 // Set up a project with model = "project-model"
0 commit comments