Canonical Skill Hash Specification
Status: Stable Version: 1
Auditor: moltguard 1.2.0
Published: September 2026
Specification URL: https://moltrust.ch/skill-hash-spec.html
Why this exists
MolTrust issues a signed credential over a skill, and the credential names a
hash rather than a file. Whoever holds the same SKILL.md can derive
the same hash and check the credential without asking us anything.
That is the whole point of publishing this page. A track record only its issuer can compute is a reputation service; one any party can recompute is evidence. Without a written rule for the hash, the second kind is not available — so the rule is written here, in enough detail to reimplement.
The rule
Given the raw bytes of a SKILL.md, decoded as UTF-8, apply these
steps in order:
- Strip a leading byte-order mark. If the first character is U+FEFF, drop it. Editors add it invisibly, and a file that looks identical on screen would otherwise hash differently.
- Normalise line endings. Replace every
CRLFwithLF, then every remaining loneCRwithLF. A checkout on Windows must produce the same hash as one on Linux. - Strip trailing whitespace on every line. Remove spaces and tabs at the end of each line. These are invisible and frequently rewritten by editors that had nothing to do with the content.
- Collapse runs of blank lines. Three or more consecutive newlines become exactly two, so one blank line is the maximum.
- Trim leading and trailing newlines from the document as a whole.
- Apply Unicode NFC. Composed and decomposed forms of the same character must not produce different hashes.
- Hash. SHA-256 over the UTF-8 encoding of the result.
The value is the lowercase hex digest, prefixed with the algorithm:
sha256:<64 lowercase hex characters>
Reference implementation
This is the code that runs. It is reproduced rather than paraphrased, because a paraphrase is one edit away from disagreeing with the thing it describes.
import { createHash } from 'node:crypto';
export function canonicalSkillHash(raw) {
let s = raw;
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
s = s.split('\n').map(line => line.replace(/[\t ]+$/, '')).join('\n');
s = s.replace(/\n{3,}/g, '\n\n');
s = s.replace(/^\n+/, '').replace(/\n+$/, '');
s = s.normalize('NFC');
const hash = createHash('sha256').update(s, 'utf8').digest('hex');
return `sha256:${hash}`;
}
The same rule in Python:
import hashlib, re, unicodedata
def canonical_skill_hash(raw: str) -> str:
s = raw.lstrip("\ufeff")
s = s.replace("\r\n", "\n").replace("\r", "\n")
s = "\n".join(line.rstrip(" \t") for line in s.split("\n"))
s = re.sub(r"\n{3,}", "\n\n", s)
s = s.strip("\n")
s = unicodedata.normalize("NFC", s)
return "sha256:" + hashlib.sha256(s.encode("utf-8")).hexdigest()
What is not normalised
Everything else is content and changes the hash:
- Indentation, including a change from tabs to spaces at the start of a line.
- Letter case anywhere in the document.
- Markdown structure — reordering sections, renaming a heading, adding a list item.
- YAML front matter, including key order.
- A single blank line where there was none.
This is deliberate. The normalisation covers what an editor changes without being asked; it does not attempt to decide which edits are meaningful. A reformatting that touches indentation produces a new hash and needs a new credential, which is the honest outcome — we cannot certify a file we did not read.
Verifying a credential
- Fetch the
SKILL.mdthe credential refers to. - Compute the canonical hash with the rule above.
- Compare it with
credentialSubject.skillHash. - Verify the credential signature against the MolTrust issuer key published at /.well-known/did.json.
A mismatch in step 3 means the file changed after the audit. It does not by itself mean anything is wrong — it means the credential describes different bytes than the ones in your hand, and a fresh audit is the way to settle it.
Stability
A change to any step above changes every hash and invalidates every issued credential, so this rule is versioned and will not change silently. A future revision would be published as version 2 alongside this page, and existing credentials would continue to name version 1.
Endpoints
GET /guard/skill/audit?url=<github-url>— audit a skill, free, no key. Answers 400 for a URL it cannot parse, 404 when the repository has noSKILL.md, 413 when the file exceeds 100 KB.GET /guard/skill/verify/<skillHash>— look up an issued credential by canonical hash.
MolTrust / CryptoKRI GmbH, Zurich | hello@moltrust.ch | moltrust.ch Released under Creative Commons Attribution 4.0 International (CC BY 4.0)