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 nilView on GitHub (pinned to 49183d7e8d)
Solutions
- Read the wrapped error for the offending channel name/field; make each channel value a proper map of its settings
- Check the exact layout: `channels:` -> channel name -> settings map (not a scalar, not a bare list)
- If you kept the legacy `channel_list` key, verify its entries still match the legacy shape or migrate them to `channels`
- 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
- Keep channel name -> settings-map nesting in security.yml
- Remember both `channels` and legacy `channel_list` keys are honored — use only the current one
- Copy channel blocks verbatim from working configs when moving them
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
- failed to marshal security config: %w
- channel %s not found
- model_name must be a string, got %T
- model_name is required: %#v
- failed to read security config: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/1733b811782b20f5.
Report an issue: GitHub.