zeroclaw-labs/zeroclaw · error

IAM policy: duplicate role mapping for normalized key '{}' (

Error message

IAM policy: duplicate role mapping for normalized key '{}' (from nevis_role '{}') — remove or merge the duplicate entry

What it means

IamPolicy::from_mappings found two role mappings whose nevis_role values collide after normalization (trim + ASCII lowercase). Duplicates are rejected instead of silently applying last-wins because an accidental overwrite could broaden or revoke access in the deny-by-default policy engine.

Source

Thrown at crates/zeroclaw-runtime/src/security/iam_policy.rs:93

                .zeroclaw_permissions
                .iter()
                .filter(|p| !p.eq_ignore_ascii_case("all"))
                .map(|p| p.trim().to_ascii_lowercase())
                .collect();

            let all_workspaces = mapping
                .workspace_access
                .iter()
                .any(|w| w.eq_ignore_ascii_case("all"));
            let allowed_workspaces: Vec<String> = mapping
                .workspace_access
                .iter()
                .filter(|w| !w.eq_ignore_ascii_case("all"))
                .map(|w| w.trim().to_ascii_lowercase())
                .collect();

            if role_map.contains_key(&key) {
                bail!(
                    "IAM policy: duplicate role mapping for normalized key '{}' \
                     (from nevis_role '{}') — remove or merge the duplicate entry",
                    key,
                    mapping.nevis_role
                );
            }

            role_map.insert(
                key,
                CompiledRole {
                    all_tools,
                    allowed_tools,
                    all_workspaces,
                    allowed_workspaces,
                },
            );
        }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Find the two entries named in the message (the normalized key and the offending nevis_role) and merge their permissions/workspaces into one.
  2. Add a config-lint step that fails on case-insensitive duplicate role keys before deploy.
  3. Adopt one casing convention (e.g. always lowercase) for nevis_role values across all sources.
  4. If roles truly differ, rename one of them in Nevis so normalized keys are distinct.

Example fix

# before
[[iam.role_mappings]]
nevis_role = "Admin"
zeroclaw_permissions = ["all"]

[[iam.role_mappings]]
nevis_role = "admin"
zeroclaw_permissions = ["fs_read"]

# after — one merged entry
[[iam.role_mappings]]
nevis_role = "Admin"
zeroclaw_permissions = ["all"]
Defensive patterns

Strategy: validation

Validate before calling

fn duplicate_role_key(ms: &[RoleMapping]) -> Option<String> {
    let mut seen = std::collections::HashSet::new();
    for m in ms {
        let k = m.nevis_role.trim().to_ascii_lowercase();
        if !k.is_empty() && !seen.insert(k) {
            return Some(k);
        }
    }
    None
}

Try / catch

if let Some(key) = duplicate_role_key(&mappings) {
    anyhow::bail!("config invalid: duplicate role '{key}' — merge before deploy");
}
let policy = IamPolicy::from_mappings(&mappings)?;

Prevention

When it happens

Trigger: Config lists the same role with different casing ("Admin" and "admin"), or the same role in two files/sections merged at load; YAML anchors or includes duplicating a role mapping block; refactors that copied a block and edited only permissions.

Common situations: Multi-file config merges where casing drifts; hand-edited configs adding a role that already exists upstream; CI environments that concatenate config fragments.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/23e02bdc470f8d9f. Report an issue: GitHub.