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

  1. Change tool_permissions to an object, e.g. "agent": { "tool_permissions": { "default": "allow" } }
  2. Or set it to null (or delete the key) so the migration inserts the correct default structure
  3. 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

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


AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20). Data as JSON: /api/errors/df05eaa82ca013f1. Report an issue: GitHub.