@@ -356,9 +356,15 @@ function waitForIpcMessage(): Promise<string | null> {
356356 * allowing agent teams subagents to run to completion.
357357 * Also pipes IPC messages into the stream during the query.
358358 */
359+ // Names reserved for programmatic servers — cannot be overridden via settings.json.
360+ const RESERVED_MCP_NAMES = new Set ( [ 'nanoclaw' ] ) ;
361+
362+ // Validate that a key is a safe MCP server name (alphanumeric, hyphens, underscores).
363+ const VALID_MCP_NAME = / ^ [ a - z A - Z 0 - 9 _ - ] + $ / ;
364+
359365/**
360366 * Read mcpServers from the per-group settings.json (at CLAUDE_CONFIG_DIR).
361- * Returns an empty object if not found or on error .
367+ * Filters out reserved names and entries with invalid shape .
362368 */
363369function loadSettingsMcpServers ( ) : Record < string , { command : string ; args ?: string [ ] ; env ?: Record < string , string > } > {
364370 const configDir = process . env . CLAUDE_CONFIG_DIR ;
@@ -367,7 +373,27 @@ function loadSettingsMcpServers(): Record<string, { command: string; args?: stri
367373 try {
368374 if ( ! fs . existsSync ( settingsPath ) ) return { } ;
369375 const settings = JSON . parse ( fs . readFileSync ( settingsPath , 'utf-8' ) ) ;
370- return settings . mcpServers || { } ;
376+ const raw = settings . mcpServers ;
377+ if ( ! raw || typeof raw !== 'object' ) return { } ;
378+
379+ const validated : Record < string , { command : string ; args ?: string [ ] ; env ?: Record < string , string > } > = { } ;
380+ for ( const [ name , config ] of Object . entries ( raw ) ) {
381+ if ( RESERVED_MCP_NAMES . has ( name ) ) {
382+ log ( `Ignoring reserved MCP server name in settings: ${ name } ` ) ;
383+ continue ;
384+ }
385+ if ( ! VALID_MCP_NAME . test ( name ) ) {
386+ log ( `Ignoring invalid MCP server name in settings: ${ name } ` ) ;
387+ continue ;
388+ }
389+ const cfg = config as Record < string , unknown > ;
390+ if ( ! cfg || typeof cfg !== 'object' || typeof cfg . command !== 'string' ) {
391+ log ( `Ignoring MCP server with invalid config: ${ name } ` ) ;
392+ continue ;
393+ }
394+ validated [ name ] = cfg as { command : string ; args ?: string [ ] ; env ?: Record < string , string > } ;
395+ }
396+ return validated ;
371397 } catch {
372398 return { } ;
373399 }
@@ -438,7 +464,8 @@ async function runQuery(
438464 // Skills and agents add servers by editing settings.json — they're picked up here.
439465 const settingsMcpServers = loadSettingsMcpServers ( ) ;
440466 const allMcpServers : Record < string , unknown > = {
441- // The nanoclaw IPC server is always programmatic (needs runtime vars)
467+ // Settings-based servers first, then nanoclaw last (cannot be overridden)
468+ ...settingsMcpServers ,
442469 nanoclaw : {
443470 command : 'node' ,
444471 args : [ mcpServerPath ] ,
@@ -448,8 +475,6 @@ async function runQuery(
448475 NANOCLAW_IS_MAIN : containerInput . isMain ? '1' : '0' ,
449476 } ,
450477 } ,
451- // All other servers come from settings.json
452- ...settingsMcpServers ,
453478 } ;
454479
455480 // Build allowedTools dynamically: base tools + mcp__{name}__* for each server
0 commit comments