multica-ai/multica · error

hermes config: unexpected root node

Error message

hermes config: unexpected root node

What it means

setHermesExternalDirs parsed the user's config.yaml successfully but the document's root node is not a YAML mapping (yamlDocumentRoot returned nil — e.g. the file is a top-level sequence, scalar, or empty document). skills.external_dirs can only be set on a mapping root, so the derived-config step refuses to proceed.

Source

Thrown at server/internal/daemon/execenv/hermes_home.go:833

	}
	if ed.Kind != yaml.SequenceNode {
		return nil
	}
	out := make([]string, 0, len(ed.Content))
	for _, c := range ed.Content {
		if c.Kind == yaml.ScalarNode {
			out = append(out, c.Value)
		}
	}
	return out
}

// setHermesExternalDirs sets skills.external_dirs on the config document,
// creating the skills mapping if needed and preserving every other setting.
func setHermesExternalDirs(doc *yaml.Node, dirs []string) error {
	top := yamlDocumentRoot(doc)
	if top == nil {
		return fmt.Errorf("hermes config: unexpected root node")
	}
	skills := yamlMapValue(top, "skills")
	if skills == nil || skills.Kind != yaml.MappingNode {
		skills = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
		yamlSetMapValue(top, "skills", skills)
	}
	yamlSetMapValue(skills, "external_dirs", yamlStringSeq(dirs))
	return nil
}

// yamlDocumentRoot returns the top-level mapping node of a parsed document, or
// nil if the shape isn't a mapping.
func yamlDocumentRoot(doc *yaml.Node) *yaml.Node {
	if doc == nil {
		return nil
	}
	node := doc
	if node.Kind == yaml.DocumentNode {

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Open the shared home's config.yaml and check the top level is a mapping of key: value pairs (no leading '-').
  2. Validate with a YAML linter or `python -c "import yaml,sys; d=yaml.safe_load(open(...)); print(type(d))"` — it must be a dict.
  3. Fix or replace the config with a valid Hermes config; if none is wanted, deleting config.yaml is fine (missing config is a supported path).
  4. Rerun the task after fixing.

Example fix

# before (~/.hermes/config.yaml — invalid, top-level sequence)
- model: gpt-4
- skills:
    external_dirs: [/x]

# after
model: gpt-4
skills:
  external_dirs:
    - /x
Defensive patterns

Strategy: validation

Validate before calling

var doc yaml.Node
if err := yaml.Unmarshal(data, &doc); err != nil { /* handled by verbatim-copy path */ }
if root := yamlDocumentRoot(&doc); root == nil {
	return fmt.Errorf("config.yaml root is not a mapping — fix %s", srcConfig)
}

Type guard

func isMappingRoot(doc *yaml.Node) bool {
	return doc != nil && yamlDocumentRoot(doc) != nil
}

Try / catch

if err := setHermesExternalDirs(&doc, dirs); err != nil {
	if errors.Is(err, errHermesConfigRoot) || strings.Contains(err.Error(), "unexpected root node") {
		// degrade gracefully: copy user config verbatim, log which file is malformed
		return writeFileAtomic(dstConfig, data, 0o600)
	}
	return err
}

Prevention

When it happens

Trigger: A config.yaml whose top level is a list ('- a\n- b'), a bare scalar ('hello'), or an effectively empty document reaches writeDerivedHermesConfig and the parse succeeds but yields a non-mapping root.

Common situations: Hand-edited or template-generated config.yaml with wrong structure; a config meant for a different tool pasted into ~/.hermes/config.yaml; truncated multi-document YAML where the first document is a flow sequence.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/08aa207c5127abde. Report an issue: GitHub.