gastownhall/beads · error

agents file must have .md extension, got %q

Error message

agents file must have .md extension, got %q

What it means

ValidateAgentsFile enforces that agents files use the .md extension (case-insensitive), since they are markdown documents consumed by agents tooling. Any other extension is rejected with the actual extension quoted via %q.

Source

Thrown at internal/config/config.go:1180

}

// ValidateAgentsFile checks that filename is safe to use as an agents file path.
// It rejects absolute paths, path separators, names longer than 255 characters,
// and non-markdown extensions. This is a pure string validation function — I/O
// checks (e.g. symlink detection) are deferred to the file write layer.
func ValidateAgentsFile(filename string) error {
	if filename == "" {
		return fmt.Errorf("agents file name must not be empty")
	}
	if len(filename) > 255 {
		return fmt.Errorf("agents file name exceeds 255 characters")
	}
	if strings.ContainsAny(filename, "/\\") {
		return fmt.Errorf("agents file must be a simple filename without path separators, got %q", filename)
	}
	ext := strings.ToLower(filepath.Ext(filename))
	if ext != ".md" {
		return fmt.Errorf("agents file must have .md extension, got %q", ext)
	}
	return nil
}

// getConfigList retrieves a list-typed configuration value from config.yaml,
// accepting either the YAML list form (e.g. `types: { custom: [step, wisp] }`)
// or the legacy comma-separated string form (e.g.
// `types.custom = "step,wisp"`). Entries are trimmed; empty entries are
// dropped. The dual-form support is required for project-extension
// types/statuses declared in .beads/config.yaml — see gastownhall/beads#4024.
func getConfigList(key string) []string {
	if v == nil {
		debug.Logf("config: viper not initialized, returning nil for key %q", key)
		return nil
	}

	// Try the YAML-list form first. Viper's GetStringSlice returns:
	//   * []string for a YAML sequence value,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Rename the file to end in .md (extensions are compared case-insensitively, so .MD works).
  2. Convert the content to Markdown if it was plain text.
  3. Update the config value to a name ending in .md.

Example fix

// before
config.SafeAgentsFile("AGENTS.txt")
// after
config.SafeAgentsFile("AGENTS.md")
Defensive patterns

Strategy: validation

Validate before calling

if strings.ToLower(filepath.Ext(name)) != ".md" {
    return fmt.Errorf("agents file must be .md, got %q", filepath.Ext(name))
}

Try / catch

if err := config.SafeAgentsFile(name); err != nil {
    if strings.Contains(err.Error(), ".md extension") {
        fixed := strings.TrimSuffix(name, filepath.Ext(name)) + ".md"
        return config.SafeAgentsFile(fixed)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ValidateAgentsFile or SafeAgentsFile with names like "AGENTS.txt", "AGENTS.markdown", "AGENTS" (no extension), or "AGENTS.MD" handled by a call site that checks case-sensitively elsewhere (validation itself lowercases, so .MD passes).

Common situations: A user configures their existing notes file (.txt) as the agents file; tooling defaults to .txt; a template writes a different extension.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/0eb151ad1e4f0cd2. Report an issue: GitHub.