本文定义 ClawManager / OpenClaw / Hermes 之间统一使用的 content_md5 计算方式。Hermes agent 的 inventory 上报、collect_skill_package 上传、install_skill 安装后校验,都必须使用同一套算法。
content_md5 不是 zip 文件本身的 MD5,也不包含 zip entry 顺序、压缩等级、mtime、权限等元数据。它是 skill 目录内容的规范化 MD5。
上传 zip 时,zip 必须包含且只包含一个顶层 skill 目录。ClawManager 会先剥掉这个顶层 skill 目录,再对目录内部内容计算 content_md5。
例如上传包结构为:
weather/
skill.json
src/
main.py
实际参与 MD5 的路径是:
skill.json
src
src/main.py
注意:只剥掉 zip 的顶层 skill 目录 weather/ 一次,不要再剥掉 skill 内部的 src/、lib/、dist/ 等目录。
- 以 skill 根目录作为基准,收集所有普通文件。
- 路径统一使用 POSIX
/分隔符。 - 去掉路径开头的
./,并做 clean 处理。 - 跳过空路径、
.、..、包含..越界语义的路径。 - 跳过任意路径段以
.开头的文件和目录,例如.git/config、.cache/a、.DS_Store。 - 目录项不直接从文件系统读取,而是由文件路径的父目录推导出来。
- 将目录项和文件项放在同一个列表中,按规范化路径字典序升序排序。
- 对每个目录项写入以下字节:
{relative_path}\n
dir\n
- 对每个文件项写入以下字节:
{relative_path}\n
file\n
{raw_file_bytes}
\n
- 对上述连续字节流计算 MD5,输出 32 位小写 hex 字符串。
不要改写文件内容。不要转换换行符,不要格式化 JSON,不要忽略空文件,不要把文件权限、mtime、owner、zip 压缩参数写入 digest。
Hermes agent 需要检查下面几个点:
- inventory 上报的
content_md5应该对/config/.hermes/skills/{skill_name}目录内部内容计算。 - 上传
collect_skill_packagezip 时,zip 内应该有一个顶层目录{skill_name}/。 - inventory 阶段和上传阶段必须使用同一份 skill 目录内容计算 MD5。
- 如果本地目录是
/config/.hermes/skills/weather/src/main.py,参与 MD5 的路径必须是src/main.py,不是weather/src/main.py,也不是main.py。 - 如果
content_md5和 ClawManager 返回的 expected 不一致,先检查是否多剥或少剥了顶层目录,其次检查是否把隐藏目录、文件元数据或 zip bytes 算进去了。
下面实现可直接给 Hermes agent 端对齐算法:
import hashlib
from pathlib import Path
def _is_hidden_relative_path(rel: str) -> bool:
return any(part.startswith(".") for part in rel.split("/"))
def skill_content_md5(skill_dir: str | Path) -> str:
root = Path(skill_dir).resolve()
files: dict[str, bytes] = {}
dirs: set[str] = set()
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
rel = path.relative_to(root).as_posix()
if rel.startswith("./"):
rel = rel[2:]
if not rel or rel == "." or rel.startswith("../") or _is_hidden_relative_path(rel):
continue
files[rel] = path.read_bytes()
parts = rel.split("/")
for i in range(1, len(parts)):
parent = "/".join(parts[:i])
if parent and not _is_hidden_relative_path(parent):
dirs.add(parent)
entries: dict[str, str] = {rel: "file" for rel in files}
for rel in dirs:
entries[rel] = "dir"
digest = hashlib.md5()
for rel in sorted(entries):
digest.update(rel.encode("utf-8"))
digest.update(b"\n")
if entries[rel] == "dir":
digest.update(b"dir\n")
else:
digest.update(b"file\n")
digest.update(files[rel])
digest.update(b"\n")
return digest.hexdigest()上传给 ClawManager 的 zip 应保持一个顶层目录:
weather/
skill.json
src/main.py
Hermes agent 在本地计算 MD5 时应对目录 /config/.hermes/skills/weather 调用 skill_content_md5()。不要对 zip 文件调用 MD5。
如果 agent 需要在上传前自检,可以先把 zip 解开,确认去掉 weather/ 后得到的文件列表与本地计算使用的相对路径一致。