translate claude skills into chatgpt or codex skills only after auditing purpose, target host, capability parity, resource portability, tool assumptions, and unrealizable behavior. use when the user uploads or points to a claude skill, asks to port a claude skill to chatgpt, codex, or openai skills, or wants a compatibility review before translation. requires a compatibility report, risk score, capability matrix, and user approval before packaging when behavior cannot be preserved faithfully.
83
90%
Does it follow best practices?
Impact
89%
2.34xAverage score across 2 eval scenarios
Passed
No known issues
#!/usr/bin/env python3
"""Inspect a Claude skill folder or zip before translation.
This script is intentionally conservative. It reports probable host assumptions,
file inventory, target-sensitive findings, and a rough translation risk score.
The assistant must still make the final compatibility call and capability matrix.
"""
from __future__ import annotations
import argparse
import json
import re
import tempfile
import zipfile
from pathlib import Path
from typing import Iterable
HOST_PATTERNS = {
"claude_runtime": [r"\bClaude Desktop\b", r"\bClaude Code\b", r"\bAnthropic\b", r"\bclaude\.ai\b", r"\bArtifacts\b"],
"mcp": [r"\bMCP\b", r"model context protocol", r"mcpServers", r"mcp_server", r"mcp\.json"],
"connectors": [r"Google Drive", r"Notion", r"Slack", r"GitHub", r"Linear", r"Dropbox", r"SharePoint", r"email", r"Gmail"],
"credentials": [r"API[_ -]?KEY", r"TOKEN", r"SECRET", r"OAuth", r"cookie", r"credential", r"login", r"session"],
"filesystem": [r"/Users/", r"~/", r"C:\\\\", r"/home/", r"local file", r"filesystem"],
"shell": [r"\bbash\b", r"\bzsh\b", r"\bnpm\b", r"\bpnpm\b", r"\bpip\b", r"\bbrew\b", r"\bdocker\b", r"\bmake\b"],
"background_work": [r"background", r"later", r"scheduled", r"cron", r"daemon", r"watcher", r"monitor"],
}
RISK_BY_CATEGORY = {
"claude_runtime": "medium",
"mcp": "high",
"connectors": "high",
"credentials": "high",
"filesystem": "medium",
"shell": "medium",
"background_work": "high",
}
TEXT_EXTS = {".md", ".txt", ".yaml", ".yml", ".json", ".py", ".js", ".ts", ".sh", ".toml", ".xml", ".html", ".css"}
BINARY_EXTS = {".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"}
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8", errors="replace")
except Exception:
return ""
def extract_frontmatter(text: str) -> dict[str, str]:
if not text.startswith("---"):
return {}
end = text.find("\n---", 3)
if end == -1:
return {}
raw = text[3:end].strip()
data: dict[str, str] = {}
for line in raw.splitlines():
if ":" in line:
k, v = line.split(":", 1)
data[k.strip()] = v.strip().strip('"\'')
return data
def max_risk(risks: Iterable[str]) -> str:
order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
best = "low"
for risk in risks:
if order[risk] > order[best]:
best = risk
return best
def normalize_kebab(value: str) -> str:
"""Normalize a skill/display name to lowercase kebab-case."""
value = value.strip()
value = re.sub(r"['’]", "", value)
value = re.sub(r"[^A-Za-z0-9]+", "-", value)
value = re.sub(r"-{2,}", "-", value).strip("-")
return value.lower()
def humanize_kebab(value: str) -> str:
"""Produce a readable display name from a kebab-case skill name."""
return " ".join(part.capitalize() for part in value.split("-") if part)
def inspect_dir(root: Path) -> dict:
skill_files = [p for p in root.rglob("SKILL.md") if p.is_file()]
files = []
findings = []
total_size = 0
risks = []
possible_capabilities = []
for p in sorted(root.rglob("*")):
if not p.is_file():
continue
rel = str(p.relative_to(root))
size = p.stat().st_size
total_size += size
suffix = p.suffix.lower()
file_entry = {"path": rel, "size": size, "kind": "text" if suffix in TEXT_EXTS else "binary" if suffix in BINARY_EXTS else "unknown"}
files.append(file_entry)
if rel.endswith("SKILL.md"):
possible_capabilities.append("core instructions")
elif rel.startswith("references/") and suffix in TEXT_EXTS:
possible_capabilities.append(f"reference guidance: {rel}")
elif rel.startswith("scripts/"):
possible_capabilities.append(f"scripted operation: {rel}")
elif rel.startswith("assets/"):
possible_capabilities.append(f"asset-dependent output: {rel}")
if suffix in TEXT_EXTS:
text = read_text(p)
for category, patterns in HOST_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, text, flags=re.IGNORECASE):
findings.append({"file": rel, "category": category, "pattern": pattern, "risk": RISK_BY_CATEGORY[category]})
risks.append(RISK_BY_CATEGORY[category])
frontmatter = {}
source_name = root.name
if len(skill_files) == 1:
frontmatter = extract_frontmatter(read_text(skill_files[0]))
source_name = frontmatter.get("name") or source_name
elif len(skill_files) == 0:
risks.append("blocked")
else:
risks.append("blocked")
suggested_name = normalize_kebab(source_name) if source_name else "translated-skill"
if not suggested_name:
suggested_name = "translated-skill"
if total_size > 25 * 1024 * 1024:
risks.append("blocked")
findings.append({"file": "<package>", "category": "package_size", "pattern": ">25MB", "risk": "blocked"})
return {
"root": str(root),
"skill_md_count": len(skill_files),
"skill_md_paths": [str(p.relative_to(root)) for p in skill_files],
"frontmatter": frontmatter,
"source_name_for_default_translation": source_name,
"suggested_translated_skill_name": suggested_name,
"suggested_display_name": humanize_kebab(suggested_name),
"file_count": len(files),
"total_size_bytes": total_size,
"files": files,
"probable_host_assumptions": findings,
"rough_translation_risk": max_risk(risks),
"possible_capabilities_to_review": sorted(set(possible_capabilities)),
"next_step": "Build the capability matrix and confirm user approval before packaging if risk is high or blocked.",
}
def main() -> int:
parser = argparse.ArgumentParser(description="Inspect a Claude skill folder or zip before translation.")
parser.add_argument("source", help="Path to skill folder or .zip archive")
args = parser.parse_args()
source = Path(args.source).expanduser().resolve()
if not source.exists():
raise SystemExit(f"source does not exist: {source}")
if source.is_file() and source.suffix.lower() == ".zip":
with tempfile.TemporaryDirectory() as tmp:
with zipfile.ZipFile(source) as zf:
zf.extractall(tmp)
report = inspect_dir(Path(tmp))
elif source.is_dir():
report = inspect_dir(source)
else:
raise SystemExit("source must be a directory or .zip file")
print(json.dumps(report, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())