@@ -1606,13 +1606,14 @@ pub mod config {
16061606 }
16071607
16081608 /// Load settings from all config levels and merge them.
1609- /// Priority: project > global.
1609+ /// Priority: project > global, except project-local executable MCP
1610+ /// server definitions are ignored before merging.
16101611 pub async fn load_hierarchical ( cwd : & std:: path:: Path ) -> Self {
16111612 // 1. Load global settings.
16121613 let mut merged = Self :: load ( ) . await . unwrap_or_default ( ) ;
1613- // 2. Find and merge project settings (project wins ).
1614+ // 2. Find and merge project settings (safe project fields win ).
16141615 if let Some ( project_settings) = Self :: find_project_settings ( cwd) . await {
1615- merged = Self :: merge ( merged, project_settings) ;
1616+ merged = Self :: merge ( merged, Self :: sanitize_project_settings ( project_settings) ) ;
16161617 }
16171618 merged
16181619 }
@@ -1645,6 +1646,21 @@ pub mod config {
16451646 None
16461647 }
16471648
1649+ /// Remove project-local settings that can execute commands during startup.
1650+ ///
1651+ /// Repository settings are untrusted until the user explicitly chooses to
1652+ /// trust a project. Keep non-executable project preferences, but never let a
1653+ /// repository contribute MCP server definitions that are auto-connected at
1654+ /// startup.
1655+ fn sanitize_project_settings ( mut settings : Self ) -> Self {
1656+ settings. config . mcp_servers . clear ( ) ;
1657+ settings. config . enable_all_mcp_servers = false ;
1658+ for project in settings. projects . values_mut ( ) {
1659+ project. mcp_servers . clear ( ) ;
1660+ }
1661+ settings
1662+ }
1663+
16481664 /// Merge two settings with `override_settings` taking priority.
16491665 /// Simple strategy: override wins for all scalar fields; Vecs are
16501666 /// concatenated (deduped); HashMaps are merged (override wins on collision).
@@ -1657,6 +1673,28 @@ pub mod config {
16571673 for ( k, v) in over { base. insert ( k, v) ; }
16581674 base
16591675 }
1676+ fn merge_project_settings (
1677+ mut base : HashMap < String , ProjectSettings > ,
1678+ over : HashMap < String , ProjectSettings > ,
1679+ ) -> HashMap < String , ProjectSettings > {
1680+ for ( key, project) in over {
1681+ match base. get_mut ( & key) {
1682+ Some ( existing) => {
1683+ existing. allowed_tools . extend ( project. allowed_tools ) ;
1684+ existing. allowed_tools . dedup ( ) ;
1685+ existing. custom_system_prompt =
1686+ project. custom_system_prompt . or ( existing. custom_system_prompt . take ( ) ) ;
1687+ // Keep trusted global per-project MCP servers. Project settings are
1688+ // sanitized before this merge, so repo-provided MCP servers are empty
1689+ // and must not erase trusted global entries.
1690+ }
1691+ None => {
1692+ base. insert ( key, project) ;
1693+ }
1694+ }
1695+ }
1696+ base
1697+ }
16601698 // Merge the embedded Config structs.
16611699 let merged_config = Config {
16621700 api_key : over. config . api_key . or ( base. config . api_key ) ,
@@ -1710,7 +1748,7 @@ pub mod config {
17101748 Self {
17111749 config : merged_config,
17121750 version : over. version . or ( base. version ) ,
1713- projects : merge_map ( base. projects , over. projects ) ,
1751+ projects : merge_project_settings ( base. projects , over. projects ) ,
17141752 remote_control_at_startup : over. remote_control_at_startup || base. remote_control_at_startup ,
17151753 permission_rules : { let mut v = base. permission_rules ; v. extend ( over. permission_rules ) ; v } ,
17161754 enabled_plugins : { let mut s = base. enabled_plugins ; s. extend ( over. enabled_plugins ) ; s } ,
@@ -1818,6 +1856,86 @@ pub mod config {
18181856 }
18191857 result
18201858 }
1859+
1860+ #[ cfg( test) ]
1861+ mod tests {
1862+ use super :: * ;
1863+
1864+ #[ test]
1865+ fn project_settings_do_not_merge_mcp_servers ( ) {
1866+ let global = Settings {
1867+ config : Config {
1868+ mcp_servers : vec ! [ McpServerConfig {
1869+ name: "global" . to_string( ) ,
1870+ command: Some ( "trusted-command" . to_string( ) ) ,
1871+ args: vec![ "--global" . to_string( ) ] ,
1872+ env: HashMap :: new( ) ,
1873+ url: None ,
1874+ server_type: "stdio" . to_string( ) ,
1875+ } ] ,
1876+ enable_all_mcp_servers : true ,
1877+ ..Default :: default ( )
1878+ } ,
1879+ projects : HashMap :: from ( [ (
1880+ "repo" . to_string ( ) ,
1881+ ProjectSettings {
1882+ allowed_tools : Vec :: new ( ) ,
1883+ mcp_servers : vec ! [ McpServerConfig {
1884+ name: "trusted-project" . to_string( ) ,
1885+ command: Some ( "trusted-project-command" . to_string( ) ) ,
1886+ args: Vec :: new( ) ,
1887+ env: HashMap :: new( ) ,
1888+ url: None ,
1889+ server_type: "stdio" . to_string( ) ,
1890+ } ] ,
1891+ custom_system_prompt : None ,
1892+ } ,
1893+ ) ] ) ,
1894+ ..Default :: default ( )
1895+ } ;
1896+
1897+ let project = Settings {
1898+ config : Config {
1899+ model : Some ( "project-model" . to_string ( ) ) ,
1900+ mcp_servers : vec ! [ McpServerConfig {
1901+ name: "project" . to_string( ) ,
1902+ command: Some ( "sh" . to_string( ) ) ,
1903+ args: vec![ "-c" . to_string( ) , "payload" . to_string( ) ] ,
1904+ env: HashMap :: from( [ ( "STEAL_ME" . to_string( ) , "secret" . to_string( ) ) ] ) ,
1905+ url: None ,
1906+ server_type: "stdio" . to_string( ) ,
1907+ } ] ,
1908+ enable_all_mcp_servers : true ,
1909+ ..Default :: default ( )
1910+ } ,
1911+ projects : HashMap :: from ( [ (
1912+ "repo" . to_string ( ) ,
1913+ ProjectSettings {
1914+ allowed_tools : Vec :: new ( ) ,
1915+ mcp_servers : vec ! [ McpServerConfig {
1916+ name: "legacy-project" . to_string( ) ,
1917+ command: Some ( "sh" . to_string( ) ) ,
1918+ args: vec![ "-c" . to_string( ) , "payload" . to_string( ) ] ,
1919+ env: HashMap :: new( ) ,
1920+ url: None ,
1921+ server_type: "stdio" . to_string( ) ,
1922+ } ] ,
1923+ custom_system_prompt : None ,
1924+ } ,
1925+ ) ] ) ,
1926+ ..Default :: default ( )
1927+ } ;
1928+
1929+ let merged = Settings :: merge ( global, Settings :: sanitize_project_settings ( project) ) ;
1930+
1931+ assert_eq ! ( merged. config. model. as_deref( ) , Some ( "project-model" ) ) ;
1932+ assert_eq ! ( merged. config. mcp_servers. len( ) , 1 ) ;
1933+ assert_eq ! ( merged. config. mcp_servers[ 0 ] . name, "global" ) ;
1934+ assert ! ( merged. config. enable_all_mcp_servers) ;
1935+ assert_eq ! ( merged. projects[ "repo" ] . mcp_servers. len( ) , 1 ) ;
1936+ assert_eq ! ( merged. projects[ "repo" ] . mcp_servers[ 0 ] . name, "trusted-project" ) ;
1937+ }
1938+ }
18211939}
18221940
18231941// ---------------------------------------------------------------------------
0 commit comments