diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index cacfe42a..8cbef7ab 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -379,12 +379,6 @@ impl ConfigLoader { loaded_entries.push(entry); } - // Still emit to stderr for non-JSON callers that go through the normal load() path; - // here we just *also* return them so callers can surface them structurally. - for warning in &all_warnings { - emit_config_warning_once(warning); - } - let merged_value = JsonValue::Object(merged.clone()); let feature_config = RuntimeFeatureConfig { diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index fd3a7c1a..3d677566 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -1157,7 +1157,11 @@ fn parse_args(args: &[String]) -> Result { return action; } - let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode); + // Keep config-backed defaults lazy so pure-local JSON surfaces (notably + // `claw --output-format json config`) can report config warnings + // structurally without an earlier default-resolution load writing prose + // warnings to stderr. + let permission_mode = || permission_mode_override.unwrap_or_else(default_permission_mode); match rest[0].as_str() { "dump-manifests" => parse_dump_manifests_args(&rest[1..], output_format), @@ -1301,7 +1305,7 @@ fn parse_args(args: &[String]) -> Result { model, output_format, allowed_tools, - permission_mode, + permission_mode: permission_mode(), compact, base_commit, reasoning_effort: reasoning_effort.clone(), @@ -1338,7 +1342,7 @@ fn parse_args(args: &[String]) -> Result { model, output_format, allowed_tools, - permission_mode, + permission_mode: permission_mode(), compact, base_commit: base_commit.clone(), reasoning_effort: reasoning_effort.clone(), @@ -1350,7 +1354,7 @@ fn parse_args(args: &[String]) -> Result { model, output_format, allowed_tools, - permission_mode, + permission_mode(), compact, base_commit, reasoning_effort, @@ -1389,7 +1393,7 @@ fn parse_args(args: &[String]) -> Result { model, output_format, allowed_tools, - permission_mode, + permission_mode: permission_mode(), compact, base_commit, reasoning_effort: reasoning_effort.clone(), diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index 06d9f287..4e653842 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -1259,6 +1259,66 @@ fn inventory_commands_deduplicate_config_deprecation_warnings_per_process() { } } +#[test] +fn config_json_reports_deprecations_structurally_without_stderr_duplicate_815() { + let root = unique_temp_dir("config-json-warning-815"); + let config_home = root.join("config-home"); + let home = root.join("home"); + fs::create_dir_all(&config_home).expect("config home should exist"); + fs::create_dir_all(&home).expect("home should exist"); + fs::write( + config_home.join("settings.json"), + r#"{"enabledPlugins": {}}"#, + ) + .expect("deprecated config fixture should write"); + + let envs = [ + ( + "CLAW_CONFIG_HOME", + config_home.to_str().expect("utf8 config home"), + ), + ("HOME", home.to_str().expect("utf8 home")), + ]; + let output = run_claw(&root, &["--output-format", "json", "config"], &envs); + assert!( + output.status.success(), + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let parsed: Value = + serde_json::from_slice(&output.stdout).expect("stdout should be valid json"); + let warnings = parsed["warnings"] + .as_array() + .expect("config JSON should include warnings[]"); + assert!( + warnings.iter().any(|warning| warning + .as_str() + .is_some_and(|text| text.contains("field \"enabledPlugins\" is deprecated"))), + "config JSON warnings[] should include enabledPlugins deprecation: {parsed}" + ); + + let stderr = String::from_utf8(output.stderr).expect("stderr utf8"); + assert!( + !stderr.contains("field \"enabledPlugins\" is deprecated"), + "JSON config should not duplicate collected config deprecations on stderr:\n{stderr}" + ); + + let text_output = run_claw(&root, &["config"], &envs); + assert!( + text_output.status.success(), + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8_lossy(&text_output.stdout), + String::from_utf8_lossy(&text_output.stderr) + ); + let text_stderr = String::from_utf8(text_output.stderr).expect("stderr utf8"); + assert!( + text_stderr.contains("field \"enabledPlugins\" is deprecated"), + "text config should keep human-readable config warnings on stderr" + ); +} + fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value { assert_json_command_with_env(current_dir, args, &[]) }