sipeed/picoclaw · error

failed to merge channels from security config: %w

Error message

failed to merge channels from security config: %w

What it means

Returned by loadSecurityConfig when security.yml contains a channels/channel_list node and cfg.Channels.UnmarshalYAML(channelsNode) rejects it. Config.Channels has a custom YAML unmarshaler; it fails when a channel entry under `channels:` (or legacy `channel_list:`) has an invalid type or shape — e.g. a channel value that is not a map, or a nested field the channel decoder refuses.

Source

Thrown at pkg/config/security.go:93

	// Unmarshal non-channel fields from security.yml
	// This will resolve encrypted values for model_list, tools, etc.
	if err := yaml.Unmarshal(data, cfg); err != nil {
		return fmt.Errorf("failed to parse security config %s: %w", securityPath, err)
	}
	if err := applyLegacySkillsSecurityConfig(cfg, data); err != nil {
		return fmt.Errorf("failed to parse legacy skills security config: %w", err)
	}

	// Restore channels from saved, then manually merge from security.yml
	cfg.Channels = make(ChannelsConfig)
	for name, savedBC := range savedChannels {
		cfg.Channels[name] = savedBC
	}

	// If we found a channels node in security.yml, merge it into existing channels
	if channelsNode != nil {
		if err := cfg.Channels.UnmarshalYAML(channelsNode); err != nil {
			return fmt.Errorf("failed to merge channels from security config: %w", err)
		}
	}

	return nil
}

func applyLegacySkillsSecurityConfig(cfg *Config, data []byte) error {
	var root yaml.Node
	if err := yaml.Unmarshal(data, &root); err != nil {
		return err
	}
	if len(root.Content) == 0 {
		return nil
	}

	rootMap := root.Content[0]
	if rootMap == nil || rootMap.Kind != yaml.MappingNode {
		return nil

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Read the wrapped error for the offending channel name/field; make each channel value a proper map of its settings
  2. Check the exact layout: `channels:` -> channel name -> settings map (not a scalar, not a bare list)
  3. If you kept the legacy `channel_list` key, verify its entries still match the legacy shape or migrate them to `channels`
  4. Reload and confirm Channels merges cleanly

Example fix

# before
channels:
  slack: slack-token-value

# after
channels:
  slack:
    token: slack-token-value
Defensive patterns

Strategy: validation

Validate before calling

// Structural check: every channels value must be a map.
func channelsShapeOK(node *yaml.Node) error {
	for i, n := range node.Content {
		_ = i
		if n.Tag == "!!map" { continue }
		return fmt.Errorf("channel value is %s, want map", n.Tag)
	}
	return nil
}

Type guard

func isChannelMap(v any) bool {
	_, ok := v.(map[string]any)
	return ok
}

Try / catch

if err := loadSecurityConfig(cfg, p); err != nil {
	if strings.Contains(err.Error(), "merge channels") {
		// inspect security.yml channels/channel_list nesting
	}
	return err
}

Prevention

When it happens

Trigger: security.yml has `channels: { slack: "just-a-string" }` (channel value not a map), an unknown nested structure inside a channel, or a channel_list entry whose elements don't fit the ChannelsConfig decode. The custom UnmarshalYAML's error is wrapped with %w.

Common situations: Moving channel config into security.yml and getting the nesting wrong (channel name mapped to a scalar instead of its settings map), or version drift where a channel's expected fields changed. Note the code accepts both `channels` and `channel_list` keys, so a legacy `channel_list` with new-format contents also triggers it.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/1733b811782b20f5. Report an issue: GitHub.