conductor-oss/conductor · error · IllegalArgumentException

Skill file must be UTF-8 text: {path}

Error message

Skill file must be UTF-8 text: {path}

What it means

Thrown by decodeUtf8 when a file inside an uploaded skill package cannot be decoded as UTF-8. The decoder is configured with CodingErrorAction.REPORT for both malformed and unmappable sequences, so any byte sequence that is not valid UTF-8 (e.g. a Windows-1252 smart quote, a Latin-1 byte, or a binary asset mislabeled as text) causes the failure.

Source

Thrown at agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/SkillRegistryService.java:597

            return MAPPER.convertValue(map, MAP_TYPE);
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Invalid SKILL.md frontmatter: " + e.getMessage(), e);
        }
    }

    private String decodeUtf8(String path, byte[] data) {
        try {
            return StandardCharsets.UTF_8
                    .newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(data))
                    .toString();
        } catch (CharacterCodingException e) {
            throw new IllegalArgumentException("Skill file must be UTF-8 text: " + path, e);
        }
    }

    private boolean isRootAgentFile(String path) {
        return !path.contains("/") && path.endsWith("-agent.md");
    }

    private boolean isScriptFile(String path) {
        if (!path.startsWith("scripts/")) {
            return false;
        }
        String filename = path.substring("scripts/".length());
        return !filename.isBlank() && !filename.contains("/");
    }

    private boolean isResourceFile(String path) {
        if (!path.contains("/")) {
            return true;

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Re-save the offending file as UTF-8 without BOM (`iconv -f CP1252 -t UTF-8 file.md`).
  2. Double-check the file is actually text — if it is binary, give it a binary extension so it is not routed through decodeUtf8.
  3. Strip BOM bytes (`sed -i '1s/^\xEF\xBB\xBF//' file.md`).

Example fix

// before: SKILL.md saved as Windows-1252 with smart quotes
// convert on the command line
iconv -f CP1252 -t UTF-8 SKILL.md -o SKILL.md.utf8 && mv SKILL.md.utf8 SKILL.md
Defensive patterns

Strategy: validation

Validate before calling

// Reject non-UTF-8 text entries before packaging.
private static void assertUtf8(String path, byte[] bytes) {
    try {
        StandardCharsets.UTF_8.newDecoder()
            .onMalformedInput(CodingErrorAction.REPORT)
            .onUnmappableCharacter(CodingErrorAction.REPORT)
            .decode(ByteBuffer.wrap(bytes));
    } catch (CharacterCodingException e) {
        throw new IllegalArgumentException(path + " is not UTF-8", e);
    }
}

Type guard

boolean isUtf8(byte[] bytes) {
    try {
        StandardCharsets.UTF_8.newDecoder()
            .onMalformedInput(CodingErrorAction.REPORT)
            .onUnmappableCharacter(CodingErrorAction.REPORT)
            .decode(ByteBuffer.wrap(bytes));
        return true;
    } catch (CharacterCodingException e) { return false; }
}

Try / catch

try {
    return skillRegistry.readFile(skillId, path);
} catch (IllegalArgumentException e) {
    if (e.getMessage().contains("UTF-8")) return unsupportedMediaType(e.getMessage());
    throw e;
}

Prevention

When it happens

Trigger: A package entry whose extension is in TEXT_EXTENSIONS (.md/.txt/.json/.yaml/.yml/...) contains non-UTF-8 bytes, or a binary file was renamed to a text extension. decodeUtf8 is invoked on it and the CharsetDecoder throws CharacterCodingException.

Common situations: Markdown authored on Windows saved as CP1252; a PNG or PDF slipped in with a `.txt` extension; a JSON file saved with a BOM or mojibake from a previous encoding round-trip.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/284502e1d9ca1119. Report an issue: GitHub.