diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs
index 798afc3..375db0d 100644
--- a/rust/crates/runtime/src/config.rs
+++ b/rust/crates/runtime/src/config.rs
@@ -9,6 +9,27 @@ use crate::sandbox::{FilesystemIsolationMode, SandboxConfig};
/// Schema name advertised by generated settings files.
pub const CLAW_SETTINGS_SCHEMA_NAME: &str = "SettingsSchema";
+/// Top-level settings keys recognized by the runtime configuration loader.
+const KNOWN_TOP_LEVEL_KEYS: &[&str] = &[
+ "$schema",
+ "enabledPlugins",
+ "env",
+ "hooks",
+ "mcpServers",
+ "model",
+ "oauth",
+ "permissionMode",
+ "permissions",
+ "plugins",
+ "sandbox",
+];
+
+/// Deprecated top-level keys mapped to their replacement guidance.
+const DEPRECATED_TOP_LEVEL_KEYS: &[(&str, &str)] = &[
+ ("allowedTools", "permissions.allow"),
+ ("ignorePatterns", "permissions.deny"),
+];
+
/// Origin of a loaded settings file in the configuration precedence chain.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfigSource {
@@ -272,9 +293,10 @@ impl ConfigLoader {
let mut mcp_servers = BTreeMap::new();
for entry in self.discover() {
- let Some(value) = read_optional_json_object(&entry.path)? else {
+ let Some((value, source)) = read_optional_json_object(&entry.path)? else {
continue;
};
+ validate_known_top_level_keys(&value, source.as_deref(), &entry.path)?;
validate_optional_hooks_config(&value, &entry.path)?;
merge_mcp_servers(&mut mcp_servers, entry.source, &value, &entry.path)?;
deep_merge_objects(&mut merged, &value);
@@ -629,7 +651,7 @@ impl McpServerConfig {
fn read_optional_json_object(
path: &Path,
-) -> Result