zed-industries/zed · error
agent.tool_permissions should be an object or null when migr
Error message
agent.tool_permissions should be an object or null when migrating always_allow_tool_actions
What it means
The m_2026_02_04 migration moves the legacy `agent.always_allow_tool_actions` list into `agent.tool_permissions.default`. It tolerates `tool_permissions` being absent or null (inserting a fresh object), but if the existing value is a string, number, array, or boolean it cannot be treated as a map and the migration bails.
Source
Thrown at crates/migrator/src/migrations/m_2026_02_04/settings.rs:77
false
}
};
if should_migrate_always_allow {
if matches!(
agent_object.get(TOOL_PERMISSIONS_KEY),
None | Some(Value::Null)
) {
agent_object.insert(
TOOL_PERMISSIONS_KEY.to_string(),
Value::Object(Default::default()),
);
}
let Some(Value::Object(tool_permissions_object)) =
agent_object.get_mut(TOOL_PERMISSIONS_KEY)
else {
bail!(
"agent.tool_permissions should be an object or null when migrating \
always_allow_tool_actions"
);
};
if !tool_permissions_object.contains_key(DEFAULT_KEY)
&& !tool_permissions_object.contains_key(DEFAULT_MODE_KEY)
{
tool_permissions_object
.insert(DEFAULT_KEY.to_string(), Value::String("allow".to_string()));
}
}
if let Some(tool_permissions) = agent_object.get_mut(TOOL_PERMISSIONS_KEY) {
migrate_default_mode_to_default(tool_permissions)?;
}
Ok(())View on GitHub (pinned to f4178619ac)
Solutions
- Change tool_permissions to an object, e.g. "agent": { "tool_permissions": { "default": "allow" } }
- Or set it to null (or delete the key) so the migration inserts the correct default structure
- If you also have always_allow_tool_actions, keep it as a string array and let the migration fold it into tool_permissions.default
Example fix
// before
"agent": { "tool_permissions": "allow" }
// after
"agent": { "tool_permissions": { "default": "allow" } } Defensive patterns
Strategy: validation
Validate before calling
fn tool_permissions_ok(value: &serde_json::Value) -> bool {
value
.pointer("/agent/tool_permissions")
.map_or(true, |tp| tp.is_object() || tp.is_null())
} Type guard
fn is_tool_permissions_migratable(v: &serde_json::Value) -> bool {
v.pointer("/agent/tool_permissions")
.map_or(true, |tp| tp.is_object() || tp.is_null())
} Try / catch
if let Err(e) = run_migration(&mut settings) {
if e.to_string().contains("tool_permissions") {
// replace the non-object value with {} or null, then retry
}
} Prevention
- Never write tool_permissions as a bare string or array
- Keep permission mode inside an object keyed by tool name plus `default`
- After agent-config schema changes, re-check the nested shape
When it happens
Trigger: settings.json contains "agent": { "tool_permissions": "allow" } (or an array like ["edit"]) instead of an object or null, and the always_allow_tool_actions migration runs on startup.
Common situations: Users abbreviating the permission model as a single string; copied snippets from pre-release agent docs that used a flat list; hand-merging old and new agent config shapes.
Related errors
- Expected play_sound_when_agent_done to be a boolean or valid
- Cannot list directory because its path matches the user's gl
- Cannot list directory because its path matches the user's gl
- Cannot list directory because its path matches the user's wo
- Cannot list directory because its path matches the user's wo
AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20).
Data as JSON: /api/errors/df05eaa82ca013f1.
Report an issue: GitHub.