Hmbown/CodeWhale · error · ConversionError
Configuration and skill entrypoints must be UTF-8.
Error message
Configuration and skill entrypoints must be UTF-8.
What it means
Guard in the plugin converter's text_file() helper: a configuration or skill entrypoint source file failed UTF-8 decoding (invalid byte sequences). The converter requires these files to be valid UTF-8 so plugin/skill content is safe to embed and hash; a binary or wrong-encoding file is the input at fault.
Solutions
- Convert the file to UTF-8 (e.g. iconv -f UTF-16 -t UTF-8 file > file)
- Re-save the file in the editor with UTF-8 encoding
- Verify the entrypoint is actually a text Markdown/config file, not a binary
Example fix
# before iconv detect: file is UTF-16LE // after iconv -f UTF-16LE -t UTF-8 SKILL.md > SKILL.md
Defensive patterns
Strategy: validation
Validate before calling
raw = path.read_bytes()
raw.decode("utf-8") # raises UnicodeDecodeError before calling the converter Type guard
def is_utf8_file(p) -> bool:
try:
p.read_bytes().decode("utf-8")
return True
except UnicodeDecodeError:
return False Prevention
- Save all config and Markdown files as UTF-8
- Never point the converter at binary files
- Normalize encodings once in CI with a lint step
When it happens
Trigger: Calling text_file() (via skill_files or mcp_config) on a SKILL.md, plugin config, or MCP config file that is not UTF-8, e.g. saved as UTF-16, Latin-1, or containing binary bytes.
Common situations: Files edited on Windows with UTF-16 default encoding; files downloaded with wrong charset; images or binaries accidentally used as skill entrypoints.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- artifact name is not portable UTF-8
- child stdout was not valid UTF-8
- current Codewhale executable path is not valid UTF-8
- {err}
- {err}
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/33f9d25b9032e8e7.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/convert-plugin.py:127
path = plain_path(path)
info = path.stat()
require(stat.S_ISREG(info.st_mode) and info.st_nlink == 1, "Only regular, non-linked source files are supported.")
require(info.st_size <= limit, "Source file exceeds the conversion size limit.")
# O_NOFOLLOW protects the final component against replacement after lstat.
fd = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
with os.fdopen(fd, "rb") as source:
opened = os.fstat(source.fileno())
require((info.st_dev, info.st_ino) == (opened.st_dev, opened.st_ino), "Source changed during conversion.")
content = source.read(limit + 1)
require(len(content) <= limit, "Source file exceeds the conversion size limit.")
return content
def text_file(path):
try:
return read_file(path).decode("utf-8")
except UnicodeError:
raise ConversionError("Configuration and skill entrypoints must be UTF-8.") from None
def skill_files(path, max_files=MAX_FILES, max_bytes=MAX_BYTES):
source = plain_path(path)
entry = source / "SKILL.md" if source.is_dir() else source
require(entry.name == "SKILL.md" or entry.suffix == ".md", "Select a skill directory or Markdown skill file.")
text = text_file(entry)
parts = re.split(r"^---\s*$", text, maxsplit=2, flags=re.MULTILINE)
require(len(parts) == 3 and not parts[0].strip(), "Skills need YAML frontmatter with name and description.")
meta = mapping(data(parts[1]), {"name", "description", "license", "compatibility", "metadata",
"disable-model-invocation", "user-invocable"})
name = meta.get("name")
require(isinstance(name, str) and re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name)
and len(name) <= 64, "Skill name must be a kebab-case identifier of at most 64 characters.")
description = meta.get("description")
require(isinstance(description, str) and description.strip(), "Skills need a non-empty description.")
require("---" not in description, "Skill description contains a delimiter the native reader cannot preserve.")
require(type(meta.get("user-invocable", True)) is bool and meta.get("user-invocable", True),View on GitHub (pinned to 433685b202)