multica-ai/multica · error

marshal hermes config: %w

Error message

marshal hermes config: %w

What it means

marshalYAMLToFile failed to yaml.Marshal the in-memory config node tree. With a Node-based document this is rare: it indicates content the encoder cannot represent (invalid character data, incompatible tag/type on a node) rather than a filesystem problem — the write happens afterwards.

Source

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

		val,
	)
}

// yamlStringSeq builds a YAML sequence node of string scalars.
func yamlStringSeq(vals []string) *yaml.Node {
	seq := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
	for _, v := range vals {
		seq.Content = append(seq.Content, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: v})
	}
	return seq
}

// marshalYAMLToFile renders a YAML node to dst as a 0600 file (it can hold
// inline secrets) via atomic replace.
func marshalYAMLToFile(doc *yaml.Node, dst string) error {
	out, err := yaml.Marshal(doc)
	if err != nil {
		return fmt.Errorf("marshal hermes config: %w", err)
	}
	return writeFileAtomic(dst, out, 0o600)
}

// writeFileAtomic writes data to a temp file in the destination directory with
// the given perms, then renames it over dst — so readers never see a partial
// file and a prior file's looser permissions are replaced.
func writeFileAtomic(dst string, data []byte, perm os.FileMode) error {
	dir := filepath.Dir(dst)
	tmp, err := os.CreateTemp(dir, ".hermes-tmp-*")
	if err != nil {
		return fmt.Errorf("create temp for %s: %w", dst, err)
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName) // no-op once renamed
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return fmt.Errorf("write temp for %s: %w", dst, err)

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Inspect config.yaml for custom tags (!!python/..., !binary), anchors/aliases, or non-UTF-8 bytes; simplify them.
  2. Re-save the config with a standard YAML writer to normalize it.
  3. Copy the user's essential settings (model, api_key) into a fresh minimal config.yaml and delete the exotic one.
  4. If unfixable, note the code copies the config verbatim only on parse failure — a file that parses but cannot re-marshal must be simplified by hand.
Defensive patterns

Strategy: try-catch

Try / catch

if err := marshalYAMLToFile(&doc, dstConfig); err != nil {
	if strings.Contains(err.Error(), "marshal hermes config") {
		// encoder rejected exotic nodes: fall back to verbatim copy of the source
		return writeFileAtomic(dstConfig, srcData, 0o600)
	}
	return err
}

Prevention

When it happens

Trigger: A config.yaml that parsed into nodes the v3 encoder rejects on re-marshal — unusual anchors/aliases or tags produced by other YAML tooling, or corrupted scalar values with invalid UTF-8.

Common situations: Exotic hand-crafted YAML in ~/.hermes/config.yaml (custom local tags like !!python/object, binary scalars) written by Python tooling; extremely old or nonstandard YAML generators.

Related errors


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