From d8437d1f4cb2723fc584d028f0d28be659532111 Mon Sep 17 00:00:00 2001 From: Eric Provencher Date: Thu, 9 Oct 2025 10:59:18 -0400 Subject: [PATCH] feat(exec): Add --disable-builtin-tools flag for MCP-only mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new CLI flag that allows users to disable all built-in tools (shell, file operations, plan, apply_patch) while keeping MCP tools and web search available. This is useful for running codex exec in headless mode with only custom MCP tools. Changes: - Add --disable-builtin-tools CLI flag to exec - Thread flag through ConfigOverrides and Config - Update ToolsConfig to conditionally register built-in tools - Keep MCP tools, web_search, and view_image available 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- codex-rs/core/src/codex.rs | 5 ++ codex-rs/core/src/config.rs | 11 +++ codex-rs/core/src/tools/spec.rs | 153 +++++++++++++++++--------------- codex-rs/exec/src/cli.rs | 5 ++ codex-rs/exec/src/lib.rs | 2 + 5 files changed, 106 insertions(+), 70 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cae47cb3222..664f484c527 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -452,6 +452,7 @@ impl Session { use_streamable_shell_tool: config.use_experimental_streamable_shell_tool, include_view_image_tool: config.include_view_image_tool, experimental_unified_exec_tool: config.use_experimental_unified_exec_tool, + disable_builtin_tools: config.disable_builtin_tools, }), user_instructions, base_instructions, @@ -1201,6 +1202,7 @@ async fn submission_loop( use_streamable_shell_tool: config.use_experimental_streamable_shell_tool, include_view_image_tool: config.include_view_image_tool, experimental_unified_exec_tool: config.use_experimental_unified_exec_tool, + disable_builtin_tools: config.disable_builtin_tools, }); let new_turn_context = TurnContext { @@ -1305,6 +1307,7 @@ async fn submission_loop( include_view_image_tool: config.include_view_image_tool, experimental_unified_exec_tool: config .use_experimental_unified_exec_tool, + disable_builtin_tools: config.disable_builtin_tools, }), user_instructions: turn_context.user_instructions.clone(), base_instructions: turn_context.base_instructions.clone(), @@ -1544,6 +1547,7 @@ async fn spawn_review_thread( use_streamable_shell_tool: false, include_view_image_tool: false, experimental_unified_exec_tool: config.use_experimental_unified_exec_tool, + disable_builtin_tools: config.disable_builtin_tools, }); let base_instructions = REVIEW_PROMPT.to_string(); @@ -2756,6 +2760,7 @@ mod tests { use_streamable_shell_tool: config.use_experimental_streamable_shell_tool, include_view_image_tool: config.include_view_image_tool, experimental_unified_exec_tool: config.use_experimental_unified_exec_tool, + disable_builtin_tools: config.disable_builtin_tools, }); let turn_context = TurnContext { client, diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index c715651851e..3f9dc57b609 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -218,6 +218,10 @@ pub struct Config { /// Include the `view_image` tool that lets the agent attach a local image path to context. pub include_view_image_tool: bool, + /// When true, disables all built-in tools (shell, read_file, list_dir, grep_files, apply_patch). + /// MCP tools and web search remain available if configured. + pub disable_builtin_tools: bool, + /// The active profile name used to derive this `Config` (if any). pub active_profile: Option, @@ -957,6 +961,7 @@ pub struct ConfigOverrides { pub include_view_image_tool: Option, pub show_raw_agent_reasoning: Option, pub tools_web_search_request: Option, + pub disable_builtin_tools: Option, } impl Config { @@ -985,6 +990,7 @@ impl Config { include_view_image_tool, show_raw_agent_reasoning, tools_web_search_request: override_tools_web_search_request, + disable_builtin_tools, } = overrides; let active_profile_name = config_profile_key @@ -1177,6 +1183,7 @@ impl Config { .unwrap_or(false), use_experimental_use_rmcp_client: cfg.experimental_use_rmcp_client.unwrap_or(false), include_view_image_tool, + disable_builtin_tools: disable_builtin_tools.unwrap_or(false), active_profile: active_profile_name, windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false), disable_paste_burst: cfg.disable_paste_burst.unwrap_or(false), @@ -2120,6 +2127,7 @@ model_verbosity = "high" use_experimental_unified_exec_tool: false, use_experimental_use_rmcp_client: false, include_view_image_tool: true, + disable_builtin_tools: false, active_profile: Some("o3".to_string()), windows_wsl_setup_acknowledged: false, disable_paste_burst: false, @@ -2183,6 +2191,7 @@ model_verbosity = "high" use_experimental_unified_exec_tool: false, use_experimental_use_rmcp_client: false, include_view_image_tool: true, + disable_builtin_tools: false, active_profile: Some("gpt3".to_string()), windows_wsl_setup_acknowledged: false, disable_paste_burst: false, @@ -2261,6 +2270,7 @@ model_verbosity = "high" use_experimental_unified_exec_tool: false, use_experimental_use_rmcp_client: false, include_view_image_tool: true, + disable_builtin_tools: false, active_profile: Some("zdr".to_string()), windows_wsl_setup_acknowledged: false, disable_paste_burst: false, @@ -2325,6 +2335,7 @@ model_verbosity = "high" use_experimental_unified_exec_tool: false, use_experimental_use_rmcp_client: false, include_view_image_tool: true, + disable_builtin_tools: false, active_profile: Some("gpt5".to_string()), windows_wsl_setup_acknowledged: false, disable_paste_burst: false, diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index e38095ecc91..2ac7bdce242 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -29,6 +29,7 @@ pub(crate) struct ToolsConfig { pub include_view_image_tool: bool, pub experimental_unified_exec_tool: bool, pub experimental_supported_tools: Vec, + pub disable_builtin_tools: bool, } pub(crate) struct ToolsConfigParams<'a> { @@ -39,6 +40,7 @@ pub(crate) struct ToolsConfigParams<'a> { pub(crate) use_streamable_shell_tool: bool, pub(crate) include_view_image_tool: bool, pub(crate) experimental_unified_exec_tool: bool, + pub(crate) disable_builtin_tools: bool, } impl ToolsConfig { @@ -51,6 +53,7 @@ impl ToolsConfig { use_streamable_shell_tool, include_view_image_tool, experimental_unified_exec_tool, + disable_builtin_tools, } = params; let shell_type = if *use_streamable_shell_tool { ConfigShellToolType::Streamable @@ -80,6 +83,7 @@ impl ToolsConfig { include_view_image_tool: *include_view_image_tool, experimental_unified_exec_tool: *experimental_unified_exec_tool, experimental_supported_tools: model_family.experimental_supported_tools.clone(), + disable_builtin_tools: *disable_builtin_tools, } } } @@ -681,87 +685,90 @@ pub(crate) fn build_specs( let view_image_handler = Arc::new(ViewImageHandler); let mcp_handler = Arc::new(McpHandler); - if config.experimental_unified_exec_tool { - builder.push_spec(create_unified_exec_tool()); - builder.register_handler("unified_exec", unified_exec_handler); - } else { - match &config.shell_type { - ConfigShellToolType::Default => { - builder.push_spec(create_shell_tool()); - } - ConfigShellToolType::Local => { - builder.push_spec(ToolSpec::LocalShell {}); - } - ConfigShellToolType::Streamable => { - builder.push_spec(ToolSpec::Function( - create_exec_command_tool_for_responses_api(), - )); - builder.push_spec(ToolSpec::Function( - create_write_stdin_tool_for_responses_api(), - )); - builder.register_handler(EXEC_COMMAND_TOOL_NAME, exec_stream_handler.clone()); - builder.register_handler(WRITE_STDIN_TOOL_NAME, exec_stream_handler); + // Skip built-in tools if the flag is set + if !config.disable_builtin_tools { + if config.experimental_unified_exec_tool { + builder.push_spec(create_unified_exec_tool()); + builder.register_handler("unified_exec", unified_exec_handler); + } else { + match &config.shell_type { + ConfigShellToolType::Default => { + builder.push_spec(create_shell_tool()); + } + ConfigShellToolType::Local => { + builder.push_spec(ToolSpec::LocalShell {}); + } + ConfigShellToolType::Streamable => { + builder.push_spec(ToolSpec::Function( + create_exec_command_tool_for_responses_api(), + )); + builder.push_spec(ToolSpec::Function( + create_write_stdin_tool_for_responses_api(), + )); + builder.register_handler(EXEC_COMMAND_TOOL_NAME, exec_stream_handler.clone()); + builder.register_handler(WRITE_STDIN_TOOL_NAME, exec_stream_handler); + } } } - } - // Always register shell aliases so older prompts remain compatible. - builder.register_handler("shell", shell_handler.clone()); - builder.register_handler("container.exec", shell_handler.clone()); - builder.register_handler("local_shell", shell_handler); + // Always register shell aliases so older prompts remain compatible. + builder.register_handler("shell", shell_handler.clone()); + builder.register_handler("container.exec", shell_handler.clone()); + builder.register_handler("local_shell", shell_handler); - if config.plan_tool { - builder.push_spec(PLAN_TOOL.clone()); - builder.register_handler("update_plan", plan_handler); - } + if config.plan_tool { + builder.push_spec(PLAN_TOOL.clone()); + builder.register_handler("update_plan", plan_handler); + } - if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { - match apply_patch_tool_type { - ApplyPatchToolType::Freeform => { - builder.push_spec(create_apply_patch_freeform_tool()); - } - ApplyPatchToolType::Function => { - builder.push_spec(create_apply_patch_json_tool()); + if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type { + match apply_patch_tool_type { + ApplyPatchToolType::Freeform => { + builder.push_spec(create_apply_patch_freeform_tool()); + } + ApplyPatchToolType::Function => { + builder.push_spec(create_apply_patch_json_tool()); + } } + builder.register_handler("apply_patch", apply_patch_handler); } - builder.register_handler("apply_patch", apply_patch_handler); - } - if config - .experimental_supported_tools - .contains(&"grep_files".to_string()) - { - let grep_files_handler = Arc::new(GrepFilesHandler); - builder.push_spec_with_parallel_support(create_grep_files_tool(), true); - builder.register_handler("grep_files", grep_files_handler); - } + if config + .experimental_supported_tools + .contains(&"grep_files".to_string()) + { + let grep_files_handler = Arc::new(GrepFilesHandler); + builder.push_spec_with_parallel_support(create_grep_files_tool(), true); + builder.register_handler("grep_files", grep_files_handler); + } - if config - .experimental_supported_tools - .contains(&"read_file".to_string()) - { - let read_file_handler = Arc::new(ReadFileHandler); - builder.push_spec_with_parallel_support(create_read_file_tool(), true); - builder.register_handler("read_file", read_file_handler); - } + if config + .experimental_supported_tools + .contains(&"read_file".to_string()) + { + let read_file_handler = Arc::new(ReadFileHandler); + builder.push_spec_with_parallel_support(create_read_file_tool(), true); + builder.register_handler("read_file", read_file_handler); + } - if config - .experimental_supported_tools - .iter() - .any(|tool| tool == "list_dir") - { - let list_dir_handler = Arc::new(ListDirHandler); - builder.push_spec_with_parallel_support(create_list_dir_tool(), true); - builder.register_handler("list_dir", list_dir_handler); - } + if config + .experimental_supported_tools + .iter() + .any(|tool| tool == "list_dir") + { + let list_dir_handler = Arc::new(ListDirHandler); + builder.push_spec_with_parallel_support(create_list_dir_tool(), true); + builder.register_handler("list_dir", list_dir_handler); + } - if config - .experimental_supported_tools - .contains(&"test_sync_tool".to_string()) - { - let test_sync_handler = Arc::new(TestSyncHandler); - builder.push_spec_with_parallel_support(create_test_sync_tool(), true); - builder.register_handler("test_sync_tool", test_sync_handler); + if config + .experimental_supported_tools + .contains(&"test_sync_tool".to_string()) + { + let test_sync_handler = Arc::new(TestSyncHandler); + builder.push_spec_with_parallel_support(create_test_sync_tool(), true); + builder.register_handler("test_sync_tool", test_sync_handler); + } } if config.web_search_request { @@ -853,6 +860,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: true, experimental_unified_exec_tool: true, + disable_builtin_tools: false, }); let (tools, _) = build_specs(&config, Some(HashMap::new())).build(); @@ -873,6 +881,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: true, experimental_unified_exec_tool: true, + disable_builtin_tools: false, }); let (tools, _) = build_specs(&config, Some(HashMap::new())).build(); @@ -895,6 +904,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: false, experimental_unified_exec_tool: true, + disable_builtin_tools: false, }); let (tools, _) = build_specs(&config, None).build(); @@ -916,6 +926,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: false, experimental_unified_exec_tool: false, + disable_builtin_tools: false, }); let (tools, _) = build_specs(&config, None).build(); @@ -948,6 +959,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: true, experimental_unified_exec_tool: true, + disable_builtin_tools: false, }); let (tools, _) = build_specs( &config, @@ -1409,6 +1421,7 @@ mod tests { use_streamable_shell_tool: false, include_view_image_tool: true, experimental_unified_exec_tool: true, + disable_builtin_tools: false, }); let (tools, _) = build_specs( &config, diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index d264eb61b0f..ab181923b2b 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -71,6 +71,11 @@ pub struct Cli { #[arg(long = "include-plan-tool", default_value_t = false)] pub include_plan_tool: bool, + /// Disable all built-in tools (shell, read_file, list_dir, grep_files, apply_patch). + /// MCP tools and web search will still be available if configured. + #[arg(long = "disable-builtin-tools", default_value_t = false)] + pub disable_builtin_tools: bool, + /// Specifies file where the last message from the agent should be written. #[arg(long = "output-last-message", short = 'o', value_name = "FILE")] pub last_message_file: Option, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 967da52b8a6..25af15c8a6e 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -69,6 +69,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any prompt, output_schema: output_schema_path, include_plan_tool, + disable_builtin_tools, config_overrides, } = cli; @@ -181,6 +182,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any include_view_image_tool: None, show_raw_agent_reasoning: oss.then_some(true), tools_web_search_request: None, + disable_builtin_tools: Some(disable_builtin_tools), }; // Parse `-c` overrides. let cli_kv_overrides = match config_overrides.parse_overrides() {