feat: consolidate RE-AI and heretek-skills into unified Claude Code Plugin Marketplace

This commit is contained in:
Claude
2026-08-13 12:53:27 -04:00
parent ae57d581c6
commit 2250c79062
3065 changed files with 787286 additions and 0 deletions
+860
View File
@@ -0,0 +1,860 @@
{
"mcpServers": {
"re-android-dynamic": {
"command": "uv",
"args": [
"--directory",
"/servers/re-android-dynamic",
"run",
"re-android-dynamic"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-angr": {
"command": "uv",
"args": [
"--directory",
"/servers/re-angr",
"run",
"re-angr"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-analysis": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-analysis",
"run",
"re-anti-analysis"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-debug-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-debug-patch",
"run",
"re-anti-debug-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-vm-spoof": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-vm-spoof",
"run",
"re-anti-vm-spoof"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-apktool": {
"command": "uv",
"args": [
"--directory",
"/servers/re-apktool",
"run",
"re-apktool"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-binary-diff": {
"command": "uv",
"args": [
"--directory",
"/servers/re-binary-diff",
"run",
"re-binary-diff"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-c-injection-build": {
"command": "uv",
"args": [
"--directory",
"/servers/re-c-injection-build",
"run",
"re-c-injection-build"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-capa": {
"command": "uv",
"args": [
"--directory",
"/servers/re-capa",
"run",
"re-capa"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-catalog-match": {
"command": "uv",
"args": [
"--directory",
"/servers/re-catalog-match",
"run",
"re-catalog-match"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-cinematic-skip": {
"command": "uv",
"args": [
"--directory",
"/servers/re-cinematic-skip",
"run",
"re-cinematic-skip"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet",
"run",
"re-dotnet"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet-patch",
"run",
"re-dotnet-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-encrypted-vm-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-encrypted-vm-bypass",
"run",
"re-encrypted-vm-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-entitlement-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-entitlement-bypass",
"run",
"re-entitlement-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida",
"run",
"re-frida"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-runtime",
"run",
"re-frida-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-wine-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-wine-runtime",
"run",
"re-frida-wine-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-fuzz-replay": {
"command": "uv",
"args": [
"--directory",
"/servers/re-fuzz-replay",
"run",
"re-fuzz-replay"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-game-ac-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-game-ac-bypass",
"run",
"re-game-ac-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-gdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-gdb",
"run",
"re-gdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ghidra-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ghidra-remote",
"run",
"re-ghidra-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-hypervisor-detect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-hypervisor-detect",
"run",
"re-hypervisor-detect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ida-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ida-remote",
"run",
"re-ida-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp",
"run",
"re-il2cpp"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp-triage",
"run",
"re-il2cpp-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-injection-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-injection-runtime",
"run",
"re-injection-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ios": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ios",
"run",
"re-ios"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-kaitai": {
"command": "uv",
"args": [
"--directory",
"/servers/re-kaitai",
"run",
"re-kaitai"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-launch-and-observe": {
"command": "uv",
"args": [
"--directory",
"/servers/re-launch-and-observe",
"run",
"re-launch-and-observe"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-leak-scan": {
"command": "uv",
"args": [
"--directory",
"/servers/re-leak-scan",
"run",
"re-leak-scan"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-lief": {
"command": "uv",
"args": [
"--directory",
"/servers/re-lief",
"run",
"re-lief"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-llm-decompile": {
"command": "uv",
"args": [
"--directory",
"/servers/re-llm-decompile",
"run",
"re-llm-decompile"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-mitm2swagger": {
"command": "uv",
"args": [
"--directory",
"/servers/re-mitm2swagger",
"run",
"re-mitm2swagger"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-orchestrator": {
"command": "uv",
"args": [
"--directory",
"/servers/re-orchestrator",
"run",
"re-orchestrator"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch",
"run",
"re-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch-apply": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch-apply",
"run",
"re-patch-apply"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pcap": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pcap",
"run",
"re-pcap"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pdb",
"run",
"re-pdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-qemu-antidetect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-qemu-antidetect",
"run",
"re-qemu-antidetect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-report-write": {
"command": "uv",
"args": [
"--directory",
"/servers/re-report-write",
"run",
"re-report-write"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-rizin": {
"command": "uv",
"args": [
"--directory",
"/servers/re-rizin",
"run",
"re-rizin"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-runtime-dump": {
"command": "uv",
"args": [
"--directory",
"/servers/re-runtime-dump",
"run",
"re-runtime-dump"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-speakeasy": {
"command": "uv",
"args": [
"--directory",
"/servers/re-speakeasy",
"run",
"re-speakeasy"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-target-fingerprint": {
"command": "uv",
"args": [
"--directory",
"/servers/re-target-fingerprint",
"run",
"re-target-fingerprint"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-traffic-capture": {
"command": "uv",
"args": [
"--directory",
"/servers/re-traffic-capture",
"run",
"re-traffic-capture"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triage",
"run",
"re-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triton": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triton",
"run",
"re-triton"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vendor-anti-tamper": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vendor-anti-tamper",
"run",
"re-vendor-anti-tamper"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-bridge": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-bridge",
"run",
"re-vm-bridge"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-control": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-control",
"run",
"re-vm-control"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-debug": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-debug",
"run",
"re-vm-debug"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-decrypt": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-decrypt",
"run",
"re-vm-decrypt"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-launch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-launch",
"run",
"re-vm-launch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-memory": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-memory",
"run",
"re-vm-memory"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-ssh": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-ssh",
"run",
"re-vm-ssh"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vtil": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vtil",
"run",
"re-vtil"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-winedbg": {
"command": "uv",
"args": [
"--directory",
"/servers/re-winedbg",
"run",
"re-winedbg"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-x64dbg-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-x64dbg-remote",
"run",
"re-x64dbg-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara",
"run",
"re-yara"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara-author": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara-author",
"run",
"re-yara-author"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"ghidra-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ghidra-mcp",
"run",
"ghidra-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ida-pro-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ida-pro-mcp",
"run",
"ida-pro-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"x64DbgMCPServer": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/x64DbgMCPServer",
"run",
"x64dbg-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ssh-mcp": {
"command": "/vendored/ssh-mcp/ssh-mcp",
"args": [
"--storage",
"/data/ssh-mcp/storage.db"
],
"env": {}
},
"re-ui-automate": {
"command": "/vendored/touchpoint/.venv/bin/touchpoint-mcp",
"args": [],
"env": {
"PYTHONUNBUFFERED": "1",
"TOUCHPOINT_FALLBACK_INPUT": "true"
}
},
"re-persistproc": {
"command": "/vendored/persistproc/.venv/bin/persistproc",
"args": [
"serve",
"--port",
"8947"
],
"env": {
"PERSISTPROC_PORT": "8947"
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "Heretek Claude Code Plugin Marketplace",
"description": "Unified marketplace hosting Heretek reverse-engineering toolchains, Claude Code skills, MCP servers, and quality guardrails.",
"version": "1.0.0",
"plugins": [
{
"name": "re-ai",
"path": "plugins/re-ai",
"description": "Reverse-engineering toolchain, 31 MCP tools, 34 skill workflows, RE-BREAKER toolkit, and Morphe patch creation skills."
},
{
"name": "core-skills",
"path": "plugins/core-skills",
"description": "General Heretek skills, maintenance skill workflows, and Morphe patch creation."
},
{
"name": "guardrails",
"path": "plugins/guardrails",
"description": "Fast quality guardrails including ruff, pyright, biome, and clippy."
}
]
}
@@ -0,0 +1,384 @@
---
name: brainstorming-research-ideas
description: Guides researchers through structured ideation frameworks to discover high-impact research directions. Use when exploring new problem spaces, pivoting between projects, or seeking novel angles on existing work.
version: 1.0.0
author: Orchestra Research
license: MIT
tags: [Research Ideation, Brainstorming, Problem Discovery, Creative Thinking, Research Strategy]
dependencies: []
---
# Research Idea Brainstorming
Structured frameworks for discovering the next research idea. This skill provides ten complementary ideation lenses that help researchers move from vague curiosity to concrete, defensible research proposals. Each framework targets a different cognitive mode—use them individually or combine them for comprehensive exploration.
## When to Use This Skill
- Starting a new research direction and need structured exploration
- Feeling stuck on a current project and want fresh angles
- Evaluating whether a half-formed idea has real potential
- Preparing for a brainstorming session with collaborators
- Transitioning between research areas and seeking high-leverage entry points
- Reviewing a field and looking for underexplored gaps
**Do NOT use this skill when**:
- You already have a well-defined research question and need execution guidance
- You need help with experimental design or methodology (use domain-specific skills)
- You want a literature review (use `scientific-skills:literature-review`)
---
## Core Ideation Frameworks
### 1. Problem-First vs. Solution-First Thinking
Research ideas originate from two distinct modes. Knowing which mode you are in prevents a common failure: building solutions that lack real problems, or chasing problems without feasible approaches.
**Problem-First** (pain point → method):
- Start with a concrete failure, bottleneck, or unmet need
- Naturally yields impactful work because the motivation is intrinsic
- Risk: may converge on incremental fixes rather than paradigm shifts
**Solution-First** (new capability → application):
- Start with a new tool, insight, or technique seeking application
- Often drives breakthroughs by unlocking previously impossible approaches
- Risk: "hammer looking for a nail"—solution may lack genuine demand
**Workflow**:
1. Write down your idea in one sentence
2. Classify it: Is this problem-first or solution-first?
3. If problem-first → verify the problem matters (who suffers? how much?)
4. If solution-first → identify at least two genuine problems it addresses
5. For either mode, articulate the gap: what cannot be done today that this enables?
**Self-Check**:
- [ ] Can I name a specific person or community who needs this?
- [ ] Is the problem I am solving actually unsolved (not just under-marketed)?
- [ ] If solution-first, does the solution create new capability or just replicate existing ones?
---
### 2. The Abstraction Ladder
Every research problem sits at a particular level of abstraction. Deliberately moving up or down the ladder reveals ideas invisible at your current level.
| Direction | Action | Outcome |
|-----------|--------|---------|
| **Move Up** (generalize) | Turn a specific result into a broader principle | Framework papers, theoretical contributions |
| **Move Down** (instantiate) | Test a general paradigm under concrete constraints | Empirical papers, surprising failure analyses |
| **Move Sideways** (analogize) | Apply same abstraction level to adjacent domain | Cross-pollination, transfer papers |
**Workflow**:
1. State your current research focus in one sentence
2. Move UP: What is the general principle behind this? What class of problems does this belong to?
3. Move DOWN: What is the most specific, constrained instance of this? What happens at the extreme?
4. Move SIDEWAYS: Where else does this pattern appear in a different field?
5. For each new level, ask: Is this a publishable contribution on its own?
**Example**:
- **Current**: "Improving retrieval accuracy for RAG systems"
- **Up**: "What makes context selection effective for any augmented generation system?"
- **Down**: "How does retrieval accuracy degrade when documents are adversarially perturbed?"
- **Sideways**: "Database query optimization uses similar relevance ranking—what can we borrow?"
---
### 3. Tension and Contradiction Hunting
Breakthroughs often come from resolving tensions between widely accepted but seemingly conflicting goals. These contradictions are not bugs—they are the research opportunity.
**Common Research Tensions**:
| Tension Pair | Research Opportunity |
|-------------|---------------------|
| Performance ↔ Efficiency | Can we match SOTA with 10x less compute? |
| Privacy ↔ Utility | Can federated/encrypted methods close the accuracy gap? |
| Generality ↔ Specialization | When does fine-tuning beat prompting, and why? |
| Safety ↔ Capability | Can alignment improve rather than tax capability? |
| Interpretability ↔ Performance | Do mechanistic insights enable better architectures? |
| Scale ↔ Accessibility | Can small models replicate emergent behaviors? |
**Workflow**:
1. Pick your research area
2. List the top 3-5 desiderata (things everyone wants)
3. Identify pairs that are commonly treated as trade-offs
4. For each pair, ask: Is this trade-off fundamental or an artifact of current methods?
5. If artifact → the reconciliation IS your research contribution
6. If fundamental → characterizing the Pareto frontier is itself valuable
**Self-Check**:
- [ ] Have I confirmed this tension is real (not just assumed)?
- [ ] Can I point to papers that optimize for each side independently?
- [ ] Is my proposed reconciliation technically plausible, not just aspirational?
---
### 4. Cross-Pollination (Analogy Transfer)
Borrowing structural ideas from other disciplines is one of the most generative research heuristics. Many foundational techniques emerged this way—attention mechanisms draw from cognitive science, genetic algorithms from biology, adversarial training from game theory.
**Requirements for a Valid Analogy**:
- **Structural fidelity**: The mapping must hold at the level of underlying mechanisms, not just surface similarity
- **Non-obvious connection**: If the link is well-known, the novelty is gone
- **Testable predictions**: The analogy should generate concrete hypotheses
**High-Yield Source Fields for ML Research**:
| Source Field | Transferable Concepts |
|-------------|----------------------|
| Neuroscience | Attention, memory consolidation, hierarchical processing |
| Physics | Energy-based models, phase transitions, renormalization |
| Economics | Mechanism design, auction theory, incentive alignment |
| Ecology | Population dynamics, niche competition, co-evolution |
| Linguistics | Compositionality, pragmatics, grammatical induction |
| Control Theory | Feedback loops, stability, adaptive regulation |
**Workflow**:
1. Describe your problem in domain-agnostic language (strip the jargon)
2. Ask: What other field solves a structurally similar problem?
3. Study that field's solution at the mechanism level
4. Map the solution back to your domain, preserving structural relationships
5. Generate testable predictions from the analogy
6. Validate: Does the borrowed idea actually improve outcomes?
---
### 5. The "What Changed?" Principle
Strong ideas often come from revisiting old problems under new conditions. Advances in hardware, scale, data availability, or regulations can invalidate prior assumptions and make previously impractical approaches viable.
**Categories of Change to Monitor**:
| Change Type | Example | Research Implication |
|------------|---------|---------------------|
| **Compute** | GPUs 10x faster | Methods dismissed as too expensive become feasible |
| **Scale** | Trillion-token datasets | Statistical arguments that failed at small scale may now hold |
| **Regulation** | EU AI Act, GDPR | Creates demand for compliant alternatives |
| **Tooling** | New frameworks, APIs | Reduces implementation barrier for complex methods |
| **Failure** | High-profile system failures | Exposes gaps in existing approaches |
| **Cultural** | New user behaviors | Shifts what problems matter most |
**Workflow**:
1. Pick a well-known negative result or abandoned approach (3-10 years old)
2. List the assumptions that led to its rejection
3. For each assumption, ask: Is this still true today?
4. If any assumption has been invalidated → re-run the idea under new conditions
5. Frame the contribution: "X was previously impractical because Y, but Z has changed"
---
### 6. Failure Analysis and Boundary Probing
Understanding where a method breaks is often as valuable as showing where it works. Boundary probing systematically exposes the conditions under which accepted techniques fail.
**Types of Boundaries to Probe**:
- **Distributional**: What happens with out-of-distribution inputs?
- **Scale**: Does the method degrade at 10x or 0.1x the typical scale?
- **Adversarial**: Can the method be deliberately broken?
- **Compositional**: Does performance hold when combining multiple capabilities?
- **Temporal**: Does the method degrade over time (concept drift)?
**Workflow**:
1. Select a widely-used method with strong reported results
2. Identify the implicit assumptions in its evaluation (dataset, scale, domain)
3. Systematically violate each assumption
4. Document where and how the method breaks
5. Diagnose the root cause of each failure
6. Propose a fix or explain why the failure is fundamental
**Self-Check**:
- [ ] Am I probing genuine boundaries, not just confirming known limitations?
- [ ] Can I explain WHY the method fails, not just THAT it fails?
- [ ] Does my analysis suggest a constructive path forward?
---
### 7. The Simplicity Test
Before accepting complexity, ask whether a simpler approach suffices. Fields sometimes over-index on elaborate solutions when a streamlined baseline performs competitively.
**Warning Signs of Unnecessary Complexity**:
- The method has many hyperparameters with narrow optimal ranges
- Ablations show most components contribute marginally
- A simple baseline was never properly tuned or evaluated
- The improvement over baselines is within noise on most benchmarks
**Workflow**:
1. Identify the current SOTA method for your problem
2. Strip it to its simplest possible core (what is the one key idea?)
3. Build that minimal version with careful engineering
4. Compare fairly: same compute budget, same tuning effort
5. If the gap is small → the contribution is the simplicity itself
6. If the gap is large → you now understand what the complexity buys
**Contribution Framing**:
- "We show that [simple method] with [one modification] matches [complex SOTA]"
- "We identify [specific component] as the critical driver, not [other components]"
---
### 8. Stakeholder Rotation
Viewing a system from multiple perspectives reveals distinct classes of research questions. Each stakeholder sees different friction, risk, and opportunity.
**Stakeholder Perspectives**:
| Stakeholder | Key Questions |
|-------------|---------------|
| **End User** | Is this usable? What errors are unacceptable? What is the latency tolerance? |
| **Developer** | Is this debuggable? What is the maintenance burden? How does it compose? |
| **Theorist** | Why does this work? What are the formal guarantees? Where are the gaps? |
| **Adversary** | How can this be exploited? What are the attack surfaces? |
| **Ethicist** | Who is harmed? What biases are embedded? Who is excluded? |
| **Regulator** | Is this auditable? Can decisions be explained? Is there accountability? |
| **Operator** | What is the cost? How does it scale? What is the failure mode? |
**Workflow**:
1. Describe your system or method in one paragraph
2. Assume each stakeholder perspective in turn (spend 5 minutes per role)
3. For each perspective, list the top 3 concerns or questions
4. Identify which concerns are unaddressed by existing work
5. The unaddressed concern with the broadest impact is your research question
---
### 9. Composition and Decomposition
Novelty often emerges from recombination or modularization. Innovation frequently lies not in new primitives, but in how components are arranged or separated.
**Composition** (combining existing techniques):
- Identify two methods that solve complementary subproblems
- Ask: What emergent capability arises from combining them?
- Example: RAG + Chain-of-Thought → retrieval-augmented reasoning
**Decomposition** (breaking apart monolithic systems):
- Identify a complex system with entangled components
- Ask: Which component is the actual bottleneck?
- Example: Decomposing "fine-tuning" into data selection, optimization, and regularization reveals that data selection often matters most
**Workflow**:
1. List the 5-10 key components or techniques in your area
2. **Compose**: Pick pairs and ask what happens when you combine them
3. **Decompose**: Pick a complex method and isolate each component's contribution
4. For compositions: Does the combination create emergent capabilities?
5. For decompositions: Does isolation reveal a dominant or redundant component?
---
### 10. The "Explain It to Someone" Test
A strong research idea should be defensible in two sentences to a smart non-expert. This test enforces clarity of purpose and sharpens the value proposition.
**The Two-Sentence Template**:
> **Sentence 1** (Problem): "[Domain] currently struggles with [specific problem], which matters because [concrete consequence]."
> **Sentence 2** (Insight): "We [approach] by [key mechanism], which works because [reason]."
**If You Cannot Fill This Template**:
- The problem may not be well-defined yet → return to Framework 1
- The insight may not be clear yet → return to Framework 7 (simplify)
- The significance may not be established → return to Framework 3 (find the tension)
**Calibration Questions**:
- Would a smart colleague outside your subfield understand why this matters?
- Does the explanation stand without jargon?
- Can you predict what a skeptic's first objection would be?
---
## Integrated Brainstorming Workflow
Use this end-to-end workflow to go from blank page to ranked research ideas.
### Phase 1: Diverge (Generate Candidates)
**Goal**: Produce 10-20 candidate ideas without filtering.
1. **Scan for tensions** (Framework 3): List 5 trade-offs in your field
2. **Check what changed** (Framework 5): List 3 recent shifts (compute, data, regulation)
3. **Probe boundaries** (Framework 6): Pick 2 popular methods and find where they break
4. **Cross-pollinate** (Framework 4): Pick 1 idea from an adjacent field
5. **Compose/decompose** (Framework 9): Combine 2 existing techniques or split 1 apart
6. **Climb the abstraction ladder** (Framework 2): For each candidate, generate up/down/sideways variants
### Phase 2: Converge (Filter and Rank)
**Goal**: Narrow to 3-5 strongest ideas.
Apply these filters to each candidate:
| Filter | Question | Kill Criterion |
|--------|----------|----------------|
| **Explain-It Test** (F10) | Can I state this in two sentences? | If no → idea is not yet clear |
| **Problem-First Check** (F1) | Is the problem genuine and important? | If no one suffers from this → drop it |
| **Simplicity Test** (F7) | Is the complexity justified? | If a simpler approach works → simplify or drop |
| **Stakeholder Check** (F8) | Who benefits? Who might object? | If no clear beneficiary → drop it |
| **Feasibility** | Can I execute this with available resources? | If clearly infeasible → park it for later |
### Phase 3: Refine (Sharpen the Winner)
**Goal**: Turn the top idea into a concrete research plan.
1. Write the two-sentence pitch (Framework 10)
2. Identify the core tension being resolved (Framework 3)
3. Specify the abstraction level (Framework 2)
4. List 3 concrete experiments that would validate the idea
5. Anticipate the strongest objection and prepare a response
6. Define a 2-week pilot that would provide signal on feasibility
**Completion Checklist**:
- [ ] Two-sentence pitch is clear and compelling
- [ ] Problem is genuine (problem-first check passed)
- [ ] Approach is justified (simplicity test passed)
- [ ] At least one stakeholder clearly benefits
- [ ] Core experiments are specified
- [ ] Feasibility pilot is defined
- [ ] Strongest objection has a response
---
## Framework Selection Guide
Not sure which framework to start with? Use this decision guide:
| Your Situation | Start With |
|---------------|------------|
| "I don't know what area to work in" | Tension Hunting (F3) → What Changed (F5) |
| "I have a vague area but no specific idea" | Abstraction Ladder (F2) → Failure Analysis (F6) |
| "I have an idea but I'm not sure it's good" | Explain-It Test (F10) → Simplicity Test (F7) |
| "I have a good idea but need a fresh angle" | Cross-Pollination (F4) → Stakeholder Rotation (F8) |
| "I want to combine existing work into something new" | Composition/Decomposition (F9) |
| "I found a cool technique and want to apply it" | Problem-First Check (F1) → Stakeholder Rotation (F8) |
| "I want to challenge conventional wisdom" | Failure Analysis (F6) → Simplicity Test (F7) |
---
## Common Pitfalls in Research Ideation
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| **Novelty without impact** | "No one has done X" but no one needs X | Apply Problem-First Check (F1) |
| **Incremental by default** | Idea is +2% on a benchmark | Climb the Abstraction Ladder (F2) |
| **Complexity worship** | Method has 8 components, each helping marginally | Apply Simplicity Test (F7) |
| **Echo chamber** | All ideas come from reading the same 10 papers | Use Cross-Pollination (F4) |
| **Stale assumptions** | "This was tried and didn't work" (5 years ago) | Apply What Changed (F5) |
| **Single-perspective bias** | Only considering the ML engineer's view | Use Stakeholder Rotation (F8) |
| **Premature convergence** | Committed to first idea without exploring alternatives | Run full Diverge phase |
---
## Usage Instructions for Agents
When a researcher asks for help brainstorming research ideas:
1. **Identify their starting point**: Are they exploring a new area, stuck on a current project, or evaluating an existing idea?
2. **Select appropriate frameworks**: Use the Framework Selection Guide to pick 2-3 relevant lenses
3. **Walk through frameworks interactively**: Apply each framework step-by-step, asking the researcher for domain-specific inputs
4. **Generate candidates**: Aim for 10-20 raw ideas across frameworks
5. **Filter and rank**: Apply the Converge phase filters to narrow to top 3-5
6. **Refine the winner**: Help articulate the two-sentence pitch and define concrete next steps
**Key Principles**:
- Push for specificity—vague ideas ("improve efficiency") are not actionable
- Challenge assumptions—ask "why?" at least three times
- Maintain a written list of all candidates, even rejected ones (they may recombine later)
- The researcher makes the final call on which ideas to pursue; the agent facilitates structured thinking
@@ -0,0 +1,179 @@
---
name: compatibility-testing
description: "Cross-browser, cross-platform, and cross-device compatibility testing ensuring consistent experience across environments. Use when validating browser support, testing responsive design, or ensuring platform compatibility."
category: specialized-testing
priority: medium
tokenEstimate: 800
agents: [qe-visual-tester, qe-test-executor, qe-performance-tester]
implementation_status: optimized
optimization_version: 1.0
last_optimized: 2025-12-02
dependencies: []
quick_reference_card: true
tags: [compatibility, cross-browser, responsive, browserstack, playwright, devices]
trust_tier: 3
validation:
schema_path: schemas/output.json
validator_path: scripts/validate-config.json
---
# Compatibility Testing
## Browser engine
Browser-driven checks (viewport emulation, responsive validation, cross-browser screenshots) should go through the **qe-browser** fleet skill (`.claude/skills/qe-browser/`). Vibium is installed by `aqe init`. Quick reference:
```bash
# Viewport emulation per breakpoint
vibium viewport 375 667 # mobile
vibium viewport 768 1024 # tablet
vibium viewport 1920 1080 # desktop
# Full device emulation (user-agent, DPR, touch)
vibium emulate-device "iPhone 15"
# Visual diff per viewport
for vp in "375 667 mobile" "768 1024 tablet" "1920 1080 desktop"; do
read w h name <<< "$vp"
vibium viewport $w $h
node .claude/skills/qe-browser/scripts/visual-diff.js --name "homepage-$name"
done
```
Cross-browser (Firefox/Safari) still requires Playwright or a cloud device farm today — Vibium's BiDi backend is Chrome-only at v26.3.x.
<default_to_action>
When validating cross-browser/platform compatibility:
1. DEFINE browser matrix (cover 95%+ of users)
2. TEST responsive breakpoints (mobile, tablet, desktop)
3. RUN in parallel across browsers/devices
4. USE cloud services for device coverage (BrowserStack, Sauce Labs)
5. COMPARE visual screenshots across platforms
**Quick Compatibility Checklist:**
- Chrome, Firefox, Safari, Edge (latest + N-1)
- Mobile Safari (iOS), Mobile Chrome (Android)
- Screen sizes: 320px, 768px, 1920px
- Test on actual target devices for critical flows
**Critical Success Factors:**
- Users access from 100+ browser/device combinations
- Test where users are, not where you develop
- Cloud testing reduces 10 hours to 15 minutes
</default_to_action>
## Quick Reference Card
### When to Use
- Before release
- After CSS/layout changes
- Launching in new markets
- Responsive design validation
---
## Cross-Browser with Playwright
```javascript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'] } }
]
});
// Run: npx playwright test --project=chromium --project=firefox
```
---
## Cloud Testing Integration
```javascript
// BrowserStack configuration
const capabilities = {
'browserName': 'Chrome',
'browser_version': '118.0',
'os': 'Windows',
'os_version': '11',
'browserstack.user': process.env.BROWSERSTACK_USER,
'browserstack.key': process.env.BROWSERSTACK_KEY
};
// Parallel execution across devices
const deviceMatrix = [
{ os: 'Windows', browser: 'Chrome' },
{ os: 'OS X', browser: 'Safari' },
{ os: 'Android', device: 'Samsung Galaxy S24' },
{ os: 'iOS', device: 'iPhone 15' }
];
```
---
## Agent-Driven Compatibility Testing
```typescript
// Cross-platform visual comparison
await Task("Compatibility Testing", {
url: 'https://example.com',
browsers: ['chrome', 'firefox', 'safari', 'edge'],
devices: ['desktop', 'tablet', 'mobile'],
platform: 'browserstack',
parallel: true
}, "qe-visual-tester");
// Returns:
// {
// combinations: 12, // 4 browsers × 3 devices
// passed: 11,
// differences: [{ browser: 'safari', device: 'mobile', diff: 0.02 }]
// }
```
---
## Agent Coordination Hints
### Memory Namespace
```
aqe/compatibility-testing/
├── browser-matrix/* - Browser/version configurations
├── device-matrix/* - Device configurations
├── visual-diffs/* - Cross-browser visual differences
└── reports/* - Compatibility reports
```
### Fleet Coordination
```typescript
const compatFleet = await FleetManager.coordinate({
strategy: 'compatibility-testing',
agents: [
'qe-visual-tester', // Visual comparison
'qe-test-executor', // Cross-browser execution
'qe-performance-tester' // Performance by platform
],
topology: 'parallel'
});
```
---
## Related Skills
- [mobile-testing](../mobile-testing/) - Mobile-specific testing
- [visual-testing-advanced](../visual-testing-advanced/) - Visual regression
- [accessibility-testing](../accessibility-testing/) - Cross-platform a11y
---
## Remember
**Cover 95%+ of your user base.** Use analytics to identify actual browser/device usage. Don't waste time on browsers nobody uses.
**With Agents:** Agents orchestrate parallel cross-browser testing across cloud platforms. `qe-visual-tester` catches visual inconsistencies across platforms automatically.
@@ -0,0 +1,410 @@
# =============================================================================
# AQE Compatibility Testing Skill Evaluation Suite v1.0.0
# Cross-browser, cross-platform, cross-device compatibility testing evaluation
# =============================================================================
skill: compatibility-testing
version: 1.0.0
description: >
Comprehensive evaluation suite for cross-browser and cross-platform
compatibility testing. Tests browser matrix coverage, device tier validation,
responsive breakpoints, and visual difference detection.
# =============================================================================
# Multi-Model Configuration
# =============================================================================
models_to_test:
- claude-sonnet-4-6 # Primary (high accuracy expected)
- claude-haiku-4-5 # Fast model (minimum quality floor)
# =============================================================================
# MCP Integration Configuration
# =============================================================================
mcp_integration:
enabled: true
namespace: skill-validation
query_patterns: true
track_outcomes: true
store_patterns: true
share_learning: true
update_quality_gate: true
target_agents:
- qe-learning-coordinator
- qe-queen-coordinator
- qe-visual-tester
# =============================================================================
# Learning Configuration
# =============================================================================
learning:
store_success_patterns: true
store_failure_patterns: true
pattern_ttl_days: 90
min_confidence_to_store: 0.7
cross_model_comparison: true
# =============================================================================
# Result Format Configuration
# =============================================================================
result_format:
json_output: true
markdown_report: true
include_raw_output: false
include_timing: true
include_token_usage: true
# =============================================================================
# Environment Setup
# =============================================================================
setup:
required_tools:
- jq
environment_variables:
BROWSER_COVERAGE_TARGET: "95"
fixtures:
- name: responsive_layout
content: |
<div class="container">
<nav class="main-nav">Navigation</nav>
<main class="content">Main content</main>
<aside class="sidebar">Sidebar</aside>
</div>
# =============================================================================
# Test Cases
# =============================================================================
test_cases:
# -------------------------------------------------------------------------
# Browser Matrix Tests
# -------------------------------------------------------------------------
- id: tc001_browser_matrix_detection
description: "Detect and report browser compatibility issues"
category: browser_matrix
priority: critical
input:
code: |
// Uses CSS Grid without fallback
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
// Uses CSS custom properties
:root {
--primary-color: #007bff;
}
.btn { background: var(--primary-color); }
context:
language: css
target_browsers: ["chrome", "firefox", "safari", "edge", "ie11"]
expected_output:
must_contain:
- "browser"
- "compatibility"
- "grid"
must_not_contain:
- "all browsers supported"
finding_count:
min: 1
max: 3
validation:
schema_check: true
keyword_match_threshold: 0.8
reasoning_quality_min: 0.7
- id: tc002_webkit_specific_issues
description: "Detect WebKit/Safari-specific compatibility issues"
category: browser_matrix
priority: high
input:
code: |
.flex-container {
display: flex;
gap: 10px; /* Gap not supported in older Safari */
}
.gradient {
background: linear-gradient(to right, red, blue);
}
input[type="date"] {
/* Date input styling varies by browser */
-webkit-appearance: none;
}
context:
language: css
target_browsers: ["safari", "mobile-safari"]
expected_output:
must_contain:
- "Safari"
- "WebKit"
finding_count:
min: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# Device Coverage Tests
# -------------------------------------------------------------------------
- id: tc003_device_tier_coverage
description: "Validate device coverage across tiers"
category: device_coverage
priority: critical
input:
prompt: |
Analyze compatibility for the following device matrix:
Tier 1 (60% users): iPhone 15, Galaxy S24, iPad Pro
Tier 2 (30% users): iPhone 13, Pixel 7, Galaxy A54
Tier 3 (10% users): iPhone 11, older Android devices
context:
test_type: device_coverage
minimum_coverage: 95
expected_output:
must_contain:
- "Tier 1"
- "Tier 2"
- "coverage"
- "device"
validation:
schema_check: true
keyword_match_threshold: 0.8
- id: tc004_mobile_specific_issues
description: "Detect mobile-specific compatibility issues"
category: device_coverage
priority: high
input:
code: |
// Touch events without fallback
element.addEventListener('touchstart', handler);
// Fixed positioning issues on iOS
.modal {
position: fixed;
bottom: 0;
}
// Viewport units with address bar
.full-height {
height: 100vh;
}
context:
language: javascript
platforms: ["ios", "android"]
expected_output:
must_contain:
- "mobile"
- "touch"
- "viewport"
validation:
schema_check: true
# -------------------------------------------------------------------------
# Responsive Breakpoint Tests
# -------------------------------------------------------------------------
- id: tc005_responsive_breakpoints
description: "Validate responsive design across breakpoints"
category: responsive
priority: high
input:
code: |
@media (max-width: 320px) {
.nav { display: none; }
}
@media (min-width: 768px) {
.sidebar { display: block; }
}
@media (min-width: 1200px) {
.container { max-width: 1140px; }
}
context:
language: css
breakpoints: [320, 480, 768, 1024, 1200, 1920]
expected_output:
must_contain:
- "breakpoint"
- "mobile"
- "responsive"
must_not_contain:
- "all breakpoints pass"
validation:
schema_check: true
keyword_match_threshold: 0.8
# -------------------------------------------------------------------------
# Visual Consistency Tests
# -------------------------------------------------------------------------
- id: tc006_visual_differences
description: "Detect visual differences across platforms"
category: visual
priority: medium
input:
prompt: |
Compare visual rendering of the following component across browsers:
- Chrome (Windows): Button renders with 4px border-radius
- Firefox (Windows): Button renders with 4px border-radius
- Safari (macOS): Button renders with 6px border-radius (native styling)
- Edge (Windows): Button renders with 4px border-radius
context:
test_type: visual_comparison
threshold: 0.02
expected_output:
must_contain:
- "visual"
- "difference"
- "Safari"
finding_count:
min: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# JavaScript Compatibility Tests
# -------------------------------------------------------------------------
- id: tc007_javascript_features
description: "Detect JavaScript feature compatibility issues"
category: javascript
priority: high
input:
code: |
// Optional chaining - not supported in older browsers
const value = obj?.nested?.property;
// Nullish coalescing
const result = value ?? 'default';
// Array.flat() - ES2019
const flat = nested.flat(2);
// BigInt - limited support
const big = 9007199254740991n;
context:
language: javascript
target_browsers: ["chrome-80", "firefox-75", "safari-13", "ie11"]
expected_output:
must_contain:
- "JavaScript"
- "compatibility"
- "ES"
finding_count:
min: 2
validation:
schema_check: true
keyword_match_threshold: 0.8
# -------------------------------------------------------------------------
# Negative Tests
# -------------------------------------------------------------------------
- id: tc010_no_issues_modern_code
description: "Modern browsers should pass with standard code"
category: negative
priority: high
input:
code: |
.container {
display: flex;
justify-content: center;
align-items: center;
}
.button {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
context:
language: css
target_browsers: ["chrome-latest", "firefox-latest", "safari-latest"]
expected_output:
must_contain:
- "compatible"
must_not_contain:
- "critical"
- "breaking"
finding_count:
max: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# Edge Cases
# -------------------------------------------------------------------------
- id: tc011_mixed_platform_issues
description: "Handle issues affecting multiple but not all platforms"
category: edge_cases
priority: medium
input:
prompt: |
Analyze this scenario:
- Feature works on Chrome, Firefox, Edge
- Feature partially works on Safari desktop
- Feature fails on Safari iOS
- Feature not tested on Opera
context:
test_type: cross_platform
expected_output:
must_contain:
- "Safari"
- "partial"
- "platform"
validation:
schema_check: true
allow_partial: true
# =============================================================================
# Success Criteria
# =============================================================================
success_criteria:
pass_rate: 0.90
critical_pass_rate: 1.0
avg_reasoning_quality: 0.70
max_execution_time_ms: 300000
cross_model_variance: 0.15
# =============================================================================
# Metadata
# =============================================================================
metadata:
author: "@aqe-team"
created: "2026-02-02"
last_updated: "2026-02-02"
coverage_target: "Browser matrix, device tiers, responsive breakpoints, visual consistency"
@@ -0,0 +1,551 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://agentic-qe.dev/schemas/compatibility-testing-output.json",
"title": "AQE Compatibility Testing Skill Output Schema",
"description": "Schema for cross-browser, cross-platform, and cross-device compatibility testing output. Extends base skill output template with browser matrix, device coverage, and visual consistency fields.",
"type": "object",
"required": ["skillName", "version", "timestamp", "status", "trustTier", "output"],
"properties": {
"skillName": {
"type": "string",
"const": "compatibility-testing",
"description": "Skill identifier"
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$",
"description": "Semantic version of the skill"
},
"timestamp": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$",
"description": "ISO 8601 timestamp of test execution"
},
"status": {
"type": "string",
"enum": ["success", "partial", "failed", "skipped"],
"description": "Overall execution status"
},
"trustTier": {
"type": "integer",
"const": 3,
"description": "Trust tier 3: has schema, validator, and eval suite"
},
"output": {
"type": "object",
"required": ["summary", "browserMatrix", "deviceCoverage", "metrics"],
"properties": {
"summary": {
"type": "string",
"minLength": 10,
"maxLength": 2000,
"description": "Human-readable summary of compatibility test results"
},
"score": {
"$ref": "#/$defs/compatibilityScore",
"description": "Overall compatibility score"
},
"browserMatrix": {
"$ref": "#/$defs/browserMatrix",
"description": "Browser coverage and results matrix"
},
"deviceCoverage": {
"$ref": "#/$defs/deviceCoverage",
"description": "Device coverage breakdown by tier"
},
"platformResults": {
"type": "array",
"items": {
"$ref": "#/$defs/platformResult"
},
"maxItems": 100,
"description": "Results by platform/browser/device combination"
},
"findings": {
"type": "array",
"items": {
"$ref": "#/$defs/compatibilityFinding"
},
"maxItems": 500,
"description": "Compatibility issues discovered"
},
"recommendations": {
"type": "array",
"items": {
"$ref": "#/$defs/recommendation"
},
"maxItems": 100,
"description": "Recommendations for improving compatibility"
},
"metrics": {
"$ref": "#/$defs/compatibilityMetrics",
"description": "Quantitative compatibility metrics"
},
"artifacts": {
"type": "array",
"items": {
"$ref": "#/$defs/artifact"
},
"maxItems": 50,
"description": "Generated artifacts (screenshots, reports)"
},
"responsiveBreakpoints": {
"type": "array",
"items": {
"$ref": "#/$defs/breakpointResult"
},
"description": "Results by responsive breakpoint"
},
"visualDifferences": {
"type": "array",
"items": {
"$ref": "#/$defs/visualDifference"
},
"description": "Cross-platform visual differences detected"
}
}
},
"metadata": {
"type": "object",
"properties": {
"executionTimeMs": {
"type": "integer",
"minimum": 0,
"description": "Execution time in milliseconds"
},
"toolsUsed": {
"type": "array",
"items": {
"type": "string",
"enum": ["playwright", "browserstack", "saucelabs", "lambdatest", "percy", "cypress", "selenium"]
},
"description": "Testing tools used"
},
"agentId": {
"type": "string",
"pattern": "^qe-[a-z][a-z0-9-]*$",
"description": "Agent that executed the test"
},
"targetUrl": {
"type": "string",
"pattern": "^https?://",
"description": "URL tested"
},
"totalCombinations": {
"type": "integer",
"minimum": 1,
"description": "Total browser/device combinations tested"
}
}
},
"validation": {
"type": "object",
"properties": {
"schemaValid": { "type": "boolean" },
"contentValid": { "type": "boolean" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"warnings": { "type": "array", "items": { "type": "string" } },
"errors": { "type": "array", "items": { "type": "string" } }
}
},
"learning": {
"type": "object",
"properties": {
"patternsDetected": {
"type": "array",
"items": { "type": "string" },
"description": "Compatibility patterns detected"
},
"reward": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Learning reward signal"
}
}
}
},
"$defs": {
"compatibilityScore": {
"type": "object",
"required": ["value", "max"],
"properties": {
"value": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Compatibility score (0-100)"
},
"max": {
"type": "number",
"const": 100
},
"grade": {
"type": "string",
"pattern": "^[A-F][+-]?$",
"description": "Letter grade"
},
"userCoveragePercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Percentage of user base covered by passing platforms"
},
"trend": {
"type": "string",
"enum": ["improving", "stable", "declining", "unknown"]
}
}
},
"browserMatrix": {
"type": "object",
"required": ["browsers"],
"properties": {
"browsers": {
"type": "array",
"items": {
"$ref": "#/$defs/browserEntry"
},
"minItems": 1,
"description": "Browser test results"
},
"coveragePercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Browser market share covered"
}
}
},
"browserEntry": {
"type": "object",
"required": ["browser", "status"],
"properties": {
"browser": {
"type": "string",
"enum": ["chrome", "firefox", "safari", "edge", "opera", "mobile-chrome", "mobile-safari", "samsung-internet"],
"description": "Browser name"
},
"version": {
"type": "string",
"description": "Browser version (e.g., '118.0', 'latest')"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial", "skipped"],
"description": "Test status"
},
"issueCount": {
"type": "integer",
"minimum": 0,
"description": "Number of issues found"
},
"marketShare": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Browser market share percentage"
}
}
},
"deviceCoverage": {
"type": "object",
"properties": {
"tier1": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 1 devices (60% of users)"
},
"tier2": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 2 devices (30% of users)"
},
"tier3": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 3 devices (10% of users)"
},
"totalDevicesTested": {
"type": "integer",
"minimum": 0,
"description": "Total unique devices tested"
}
}
},
"tierCoverage": {
"type": "object",
"properties": {
"devices": {
"type": "array",
"items": { "type": "string" },
"description": "Device names in this tier"
},
"passRate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Pass rate percentage"
},
"tested": {
"type": "integer",
"minimum": 0
},
"passed": {
"type": "integer",
"minimum": 0
}
}
},
"platformResult": {
"type": "object",
"required": ["platform", "browser", "status"],
"properties": {
"platform": {
"type": "string",
"enum": ["windows", "macos", "linux", "ios", "android"],
"description": "Operating system"
},
"platformVersion": {
"type": "string",
"description": "OS version"
},
"browser": {
"type": "string",
"description": "Browser name"
},
"browserVersion": {
"type": "string",
"description": "Browser version"
},
"device": {
"type": "string",
"description": "Device name (for mobile)"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial", "skipped"]
},
"executionTimeMs": {
"type": "integer",
"minimum": 0
},
"screenshotPath": {
"type": "string",
"description": "Path to screenshot"
},
"issues": {
"type": "array",
"items": { "type": "string" },
"description": "Issue IDs found on this platform"
}
}
},
"compatibilityFinding": {
"type": "object",
"required": ["id", "title", "severity", "category"],
"properties": {
"id": {
"type": "string",
"pattern": "^COMPAT-\\d{3,6}$",
"description": "Unique finding identifier"
},
"title": {
"type": "string",
"minLength": 5,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 2000
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"]
},
"category": {
"type": "string",
"enum": ["layout", "rendering", "functionality", "performance", "visual", "javascript", "css", "responsive"],
"description": "Issue category"
},
"affectedPlatforms": {
"type": "array",
"items": { "type": "string" },
"description": "Platforms where issue occurs"
},
"workingPlatforms": {
"type": "array",
"items": { "type": "string" },
"description": "Platforms where it works correctly"
},
"remediation": {
"type": "string",
"description": "How to fix this issue"
},
"evidence": {
"type": "string",
"description": "Screenshot path or code snippet"
},
"userImpactPercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Percentage of users affected"
}
}
},
"recommendation": {
"type": "object",
"required": ["id", "title", "priority"],
"properties": {
"id": {
"type": "string",
"pattern": "^REC-\\d{3,6}$"
},
"title": {
"type": "string",
"minLength": 5,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 2000
},
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"effort": {
"type": "string",
"enum": ["trivial", "low", "medium", "high", "major"]
},
"relatedFindings": {
"type": "array",
"items": { "type": "string" }
}
}
},
"compatibilityMetrics": {
"type": "object",
"properties": {
"totalCombinations": {
"type": "integer",
"minimum": 0,
"description": "Total platform combinations tested"
},
"passed": {
"type": "integer",
"minimum": 0
},
"failed": {
"type": "integer",
"minimum": 0
},
"skipped": {
"type": "integer",
"minimum": 0
},
"passRate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Overall pass rate percentage"
},
"userCoverage": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "User base coverage percentage"
},
"browsersCovered": {
"type": "integer",
"minimum": 0
},
"devicesCovered": {
"type": "integer",
"minimum": 0
},
"criticalIssues": {
"type": "integer",
"minimum": 0
},
"duration": {
"type": "integer",
"minimum": 0,
"description": "Total test duration in milliseconds"
}
}
},
"breakpointResult": {
"type": "object",
"required": ["breakpoint", "width", "status"],
"properties": {
"breakpoint": {
"type": "string",
"enum": ["mobile", "tablet", "desktop", "large-desktop"],
"description": "Breakpoint category"
},
"width": {
"type": "integer",
"minimum": 1,
"description": "Screen width in pixels"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial"]
},
"issues": {
"type": "array",
"items": { "type": "string" }
}
}
},
"visualDifference": {
"type": "object",
"required": ["platform1", "platform2", "diffPercent"],
"properties": {
"platform1": {
"type": "string",
"description": "First platform"
},
"platform2": {
"type": "string",
"description": "Second platform"
},
"diffPercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Visual difference percentage"
},
"diffImagePath": {
"type": "string",
"description": "Path to diff image"
},
"significant": {
"type": "boolean",
"description": "Whether difference is significant"
}
}
},
"artifact": {
"type": "object",
"required": ["type", "path"],
"properties": {
"type": {
"type": "string",
"enum": ["report", "screenshot", "diff", "video", "log", "data"]
},
"path": {
"type": "string",
"maxLength": 500
},
"format": {
"type": "string",
"enum": ["json", "html", "md", "png", "jpg", "webm", "mp4", "txt", "csv"]
},
"description": {
"type": "string",
"maxLength": 500
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"skillName": "compatibility-testing",
"skillVersion": "1.0.0",
"requiredTools": [
"jq"
],
"optionalTools": [
"playwright",
"browserstack",
"ajv",
"jsonschema",
"python3"
],
"schemaPath": "schemas/output.json",
"requiredFields": [
"skillName",
"status",
"output",
"output.browserMatrix",
"output.deviceCoverage",
"output.metrics"
],
"requiredNonEmptyFields": [
"output.summary",
"output.browserMatrix"
],
"mustContainTerms": [
"browser",
"compatibility"
],
"mustNotContainTerms": [
"TODO",
"placeholder",
"FIXME"
],
"enumValidations": {
".status": [
"success",
"partial",
"failed",
"skipped"
]
}
}
@@ -0,0 +1,99 @@
---
name: skill-authoring
license: Apache-2.0
description: Author, audit, and improve Grafana SKILL.md files against Anthropic's published Agent Skills guidance and the four-dimension rubric the grafana/skills CI gate uses (conciseness, actionability, workflow clarity, progressive disclosure). Applies the canonical SKILL.md structure (YAML frontmatter + body + references/ + scripts/ + assets/), the "pushy description" trigger pattern, the three-level progressive-disclosure model, and the validate-fix-rerun feedback loop. Use when creating a new skill in this repo, when reviewing a skill PR, when a skill's Tessl review score is below 75 (the merge gate), when a skill's description isn't getting picked up by agents, when restructuring a long SKILL.md into a bundle, or when the user asks how to write, improve, optimize, audit, or fix a skill - even if they don't say "skill" explicitly (e.g. "this isn't triggering", "Tessl scored this 72", "split this doc").
---
# Authoring & Improving Grafana Skills
How to write, review, and improve SKILL.md files so they pass the repo's CI gate and score well against the Anthropic-aligned rubric Tessl uses.
## Critical rules (always)
1. **Description is the primary trigger** — third-person, ≤1024 chars, must include explicit "Use when..." phrasing AND list concrete trigger terms users naturally say. See [references/descriptions.md](references/descriptions.md) for the pushy-description pattern that combats undertriggering.
2. **Body under 500 lines** — split into `references/*.md` if approaching the limit. SKILL.md is the routing layer, not the entire knowledge base.
3. **One level of nesting for references** — link from SKILL.md directly, never `SKILL.md → a.md → b.md`. Claude may use `head -100` previews on nested chains and miss content.
4. **Imperative voice** — "Run X" not "You should run X" not "It is important to run X". Explain *why* over heavy-handed `MUST` markers.
5. **Concrete examples beat prose** — copy-paste-ready commands, real config snippets. Tessl's `actionability` dimension scores this directly.
6. **No reserved words in `name`** — `anthropic` and `claude` are forbidden in skill names.
7. **No time-sensitive language in the body** — "after August 2025…" rots. Use an `<details>` "Old patterns" section for legacy info instead.
8. **Validate before committing** — `./scripts/lint-skills.sh skills/<plugin>/<your-skill>` clean + Tessl score ≥75 (run `tessl skill review --json <dir>`).
## The rubric
CI fails any PR where a touched SKILL.md scores below **75** on four 0-3 dimensions: **conciseness**, **actionability**, **workflow clarity**, **progressive disclosure**. Full per-dimension scoring + Anthropic-doc mapping in [references/rubric.md](references/rubric.md).
## Score variance
The judge is an LLM and swings 7-10 points run-to-run. Local 94 commonly lands at CI 85. **Ship only on three consecutive local 100s.**
## Decision tree for a new skill
1. **What product / domain does this skill belong to?**
Pick the right plugin folder: `grafana-core/`, `grafana-cloud/`, `grafana-lgtm/`, `grafana-app-sdk/`, `grafana-k6/`, `grafana-plugins/`. If none fits cleanly, ask the user before creating a new plugin group (a new group requires updating three `marketplace.json` files).
2. **Estimate body length.**
- <200 lines of substance → single `SKILL.md`, no bundle
- 200-500 lines → `SKILL.md` + `references/<topic>.md` for the long-form material
- >500 lines → mandatory bundle split; see [references/anatomy.md § Splitting strategies](references/anatomy.md#splitting-strategies)
3. **Write a "pushy" description first.**
The description is the only thing always loaded into context. If agents don't trigger the skill, nothing else matters. See [references/descriptions.md](references/descriptions.md) for the pattern.
4. **Draft body with the four-dimension rubric in mind.**
- Cut every sentence Claude already knows (Conciseness)
- Replace prose explanations with code blocks (Actionability)
- Number every multi-step procedure + add a validation step at the end (Workflow clarity)
- If you reach for `<details>`, consider whether that content belongs in `references/` instead (Progressive disclosure)
5. **Register in marketplace manifests.**
Add the skill path to the `skills` array in all three:
- `.claude-plugin/marketplace.json`
- `.cursor-plugin/marketplace.json`
- `.agents-plugin/marketplace.json`
6. **Validate locally.**
```bash
# 1. Lint clean (0 errors)
./scripts/lint-skills.sh skills/<plugin>/<your-skill>
# 2. Tessl reviewScore ≥75 (the CI gate)
tessl skill review --json skills/<plugin>/<your-skill> | jq '.review.reviewScore'
# 3. If below 75 or you want ≥85: run --optimize (requires auth)
tessl skill review --optimize --yes --max-iterations 3 skills/<plugin>/<your-skill>
```
If the run fails: read the lint error / Tessl suggestion, fix, re-run. Don't open the PR until both checks pass cleanly. The feedback-loop pattern beats one-shot writing.
## Fixing a low-scoring existing skill
1. Read the judge's verbatim Suggestions text (non-JSON output):
```bash
tessl skill review skills/<plugin>/<name>
```
The `Suggestions:` block under each dimension names the exact sentences/sections to cut. **Copy the suggestion** — don't guess. Then verify the lowest dimension matches your read.
2. Apply the fix pattern from [references/rubric.md](references/rubric.md):
- **Conciseness 1-2** → cut intros, definitions, multi-line tables that mostly point to refs
- **Actionability 1-2** → replace prose with code blocks and CLI commands
- **Workflow clarity 1-2** → add numbered steps + validation checkpoints
- **Progressive disclosure 1-2** → split into `references/*.md`
3. If the skill is **intentionally a routing document** (like `grafana-k6/k6-docs`), don't let `--optimize` inline the bundle back into SKILL.md. Hand-craft a minimal copy-paste "validation loop" inline so SKILL.md is independently actionable, while preserving the bundle.
4. Re-score five times locally. **Don't stop until all five runs hit 100** — see "Score variance" above for why.
## Anti-patterns
See [references/anti-patterns.md](references/anti-patterns.md).
## References
- [`references/descriptions.md`](references/descriptions.md) — the pushy-description pattern + trigger-term checklist
- [`references/rubric.md`](references/rubric.md) — per-dimension scoring with Anthropic-doc citations and concrete fix patterns
- [`references/anatomy.md`](references/anatomy.md) — three-level progressive disclosure, bundle layout, splitting strategies
- [`references/anti-patterns.md`](references/anti-patterns.md) — what NOT to do, with examples
- [Anthropic — Agent Skills best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
- [anthropics/skills — skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md)
- [The Complete Guide to Building Skills for Claude (PDF)](https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf)
@@ -0,0 +1,139 @@
# Anatomy of a skill bundle
## The canonical layout
```
skills/<plugin>/<skill-name>/
├── SKILL.md (required: YAML frontmatter + body)
├── references/ (markdown loaded only when needed)
│ ├── topic-a.md
│ └── topic-b.md
├── scripts/ (executable code; not loaded into context)
│ └── helper.py
└── assets/ (templates, fonts, images used as output)
└── template.json
```
Source: [anthropics/skills — skill-creator SKILL.md § Anatomy of a Skill](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md).
## The three-level loading model
| Level | What's loaded | When | Token cost |
|---|---|---|---|
| 1. Metadata | `name` + `description` | Always (at agent startup) | ~50-200 tokens per skill |
| 2. SKILL.md body | Full markdown body | When the agent decides the skill triggers | ~500-3000 tokens once loaded |
| 3. Bundle | `references/*.md`, `assets/*`, `scripts/*` (via bash, not context) | On demand, only the specific file the body links to | Zero until accessed |
Implication: **the cheapest way to lift Conciseness is to move heavy content from level 2 to level 3.**
## SKILL.md size guidance
- **Aim:** ≤200 lines for the body
- **Hard guideline:** ≤500 lines (Anthropic's documented limit)
- **Soft threshold:** If you're past 250 lines, ask whether some sections should be `references/*.md`
## Splitting strategies
### By domain (when a skill spans multiple platforms/frameworks)
```
cloud-integrations/
├── SKILL.md (overview + selection logic: "if AWS, see references/aws.md…")
└── references/
├── aws.md
├── gcp.md
├── azure.md
└── confluent.md
```
Claude reads SKILL.md, sees the user mentioned AWS, reads only `references/aws.md`. The other three never load → zero token cost.
### By depth (when overview vs. detail is clear)
```
react-19-plugin-migration/
├── SKILL.md (decision tree + critical rules + happy path)
└── references/
├── eslint-9-migration.md (deep dive for the ESLint sub-step)
└── troubleshooting.md (failure modes + fixes)
```
### By format (when reference material doesn't fit the prose flow)
```
admin/
├── SKILL.md
└── references/
├── sso.md (full OAuth/SAML/GitHub OAuth configs)
├── terraform.md (Terraform provider examples)
└── api-reference.md (Cloud API + Admin endpoints + audit logs)
```
## Reference-file conventions
- **Filename**: short, descriptive, lowercase-with-hyphens (`troubleshooting.md`, not `Troubleshooting.md` or `Docs.md`)
- **Top of file**: if >100 lines, include a `## Contents` table of contents so Claude can navigate during a partial read
- **Linked one level deep from SKILL.md** — never a chain (`SKILL.md → a.md → b.md`)
- **Inline relative links** — `[full setup guide](references/sso.md)` from SKILL.md, not absolute URLs
## When NOT to inline
Tessl's rubric penalizes routing documents (a thin SKILL.md that defers to references/) by scoring them down for "not independently actionable". The naive response is to inline content from references back into SKILL.md, which undoes the deliberate progressive disclosure.
**Better fix for routing skills:** add a minimal copy-paste-ready "happy path" loop inline so SKILL.md is independently actionable for the most common task, while preserving the bundle for everything else.
Concrete example — `grafana-k6/k6-docs` (33 lines in its SKILL.md):
````markdown
## Workflow
Pick the workflow that matches the user's intent:
- **Write documentation:** follow [references/workflows/write.md](references/workflows/write.md)
- **Review documentation:** follow [references/workflows/review.md](references/workflows/review.md)
## Validating examples (always run before committing)
Every code example in k6 documentation and release notes must execute cleanly against `k6@master`:
```bash
cd ~/path/to/k6
git checkout master
git pull
go run . run /path/to/script.js
```
If the run fails: read the error, fix the source, re-run. Don't commit a doc edit whose
example doesn't run clean. For the full multi-example workflow with parallel subagents,
see [references/testing-workflow.md](references/testing-workflow.md).
````
This pattern (the file lifted its score from 72 → 100):
1. Keeps the routing layer + bundle architecture
2. Adds enough inline content that Claude can do the common task without loading any bundle file
3. Still defers full procedural detail to the references/
## scripts/ directory
Use when the skill includes deterministic operations Claude shouldn't reinvent:
- File transformations (PDF extraction, image resizing)
- Validation harnesses
- Output formatters
Scripts are executed via bash; **their contents do not consume context tokens**, only their output does. Big performance win for skills that run the same code repeatedly.
Make scripts executable (`chmod +x`) — the linter warns otherwise.
## assets/ directory
Use for template files, fonts, images, or other static resources the skill produces as output. Like scripts, assets don't consume tokens until referenced.
## Marketplace registration
After creating the skill directory, add it to the `skills` array in all three marketplace files:
- `.claude-plugin/marketplace.json`
- `.cursor-plugin/marketplace.json`
- `.agents-plugin/marketplace.json`
The three must stay in sync. CI's `validate` job will fail any PR where they diverge.
@@ -0,0 +1,243 @@
# Anti-patterns: what NOT to do in a skill
Distilled from [Anthropic's Agent Skills best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices), [skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md), and the Tessl rubric this repo's CI gate uses.
## Description anti-patterns
### First-person / second-person voice
```yaml
# Wrong
description: I can help you process Excel files and generate reports.
description: You can use this to process Excel files.
# Right (third-person)
description: Processes Excel files and generates reports. Use when analyzing spreadsheets, building pivot tables, or generating charts from .xlsx data.
```
### Vague "what"
```yaml
# Wrong
description: Helps with metrics queries.
# Right (specific actions + concrete outputs)
description: Write PromQL queries for Prometheus and Grafana Mimir. Calculates rates, builds histogram quantiles, debugs cardinality, and optimizes slow queries. Use when writing metric queries, building Grafana panels, working with recording rules, or troubleshooting Prometheus performance.
```
### Missing "Use when" trigger
```yaml
# Wrong (no triggers — agent can't tell when to load it)
description: PromQL query writing for Prometheus.
# Right (explicit triggers)
description: PromQL query writing for Prometheus and Grafana Mimir. Use when writing metric queries, building Grafana panels, calculating rates, working with histograms, or optimizing slow queries.
```
### No trigger variations / no pushy clause
```yaml
# Wrong (only the canonical term — agent skips this when user says synonyms)
description: Use when writing k6 documentation.
# Right (variations + pushy clause)
description: ... Use when working on k6 documentation, the k6 changelog, the k6 release notes, k6 API reference, k6 TypeScript types, load testing docs - even if the user doesn't explicitly say "documentation".
```
### Reserved words in `name`
```yaml
# Wrong
name: anthropic-helper
name: claude-tools
# Right
name: skill-authoring
name: prompt-helper
```
## Body anti-patterns
### Time-sensitive language
```markdown
# Wrong (rots — "after August 2025" is meaningless in 2027)
If you're doing this before August 2025, use the old API.
After August 2025, use the new API.
# Right (`<details>` for legacy info, current path inline)
## Current method
Use the v2 API endpoint: `api.example.com/v2/messages`
<details>
<summary>Legacy v1 API (deprecated 2025-08)</summary>
The v1 API used: `api.example.com/v1/messages`. No longer supported.
</details>
```
### Windows-style paths
```markdown
# Wrong (breaks on Unix)
Run `scripts\helper.py` to process the input.
# Right (forward slashes everywhere)
Run `scripts/helper.py` to process the input.
```
### Inconsistent terminology
Pick one term per concept and stick with it. Mixing breaks Claude's pattern-matching:
```markdown
# Wrong (same concept, three names)
The API endpoint is /messages. Send a POST to the URL. The API route accepts JSON.
# Right (one term)
The API endpoint is /messages. Send a POST to the endpoint. The endpoint accepts JSON.
```
### Heavy-handed `MUST` markers without justification
```markdown
# Wrong (no explanation, feels like noise)
You MUST validate the input. You MUST handle errors. You MUST log the result.
# Right (explain why, reserve MUST for genuine hard constraints)
Validate the input before submitting — the API returns a 422 for malformed payloads
without telling you which field failed, which is hard to debug after the fact.
Always handle the 429 response with exponential backoff; the rate limiter resets
on a sliding window, not a fixed window.
```
### "There are many ways to do this" hedging
```markdown
# Wrong (forces Claude to pick — wastes time on irrelevant alternatives)
You can use pypdf, or pdfplumber, or PyMuPDF, or pdf2image. Each has trade-offs...
# Right (pick a default, mention the escape hatch)
Use pdfplumber for text extraction. For scanned PDFs requiring OCR, use pdf2image
with pytesseract instead.
```
### Dangling references
```markdown
# Wrong (file doesn't exist)
For full setup, see [references/setup-guide.md](references/setup-guide.md)
```
The linter doesn't catch this. Reviewers should. Either create the file or remove the link.
### Too-deep reference chains
```markdown
# Wrong: SKILL.md → advanced.md → details.md
# (Claude may preview with `head -100` and miss content past level 2)
# Right: keep all references one level deep from SKILL.md
SKILL.md
├── references/advanced.md
└── references/details.md
```
## Workflow anti-patterns
### Implicit ordering
```markdown
# Wrong (ordering is in the prose; easy to miss)
Configure X. Make sure to do Y before X actually. Don't forget Z either.
# Right (explicit numbered steps)
1. Do Y first
2. Then configure X
3. Then run Z to verify
```
### No validation checkpoints
```markdown
# Wrong (no way to know if step 1 worked before doing step 2)
1. Create the contact point
2. Add the notification policy
3. Write the alert rule
# Right (validate after each step)
1. Create the contact point
- **Verify**: `curl .../api/v1/provisioning/contact-points` returns it
2. Add the notification policy
- **Verify**: send a test alert; output should show the contact point name
3. Write the alert rule
- **Verify**: alert fires correctly under simulated conditions
```
### "Punting to Claude" in scripts
```python
# Wrong (script lets Claude figure out failure)
def process_file(path):
return open(path).read() # fails on FileNotFoundError, no fallback
# Right (handle the error explicitly)
def process_file(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"File {path} not found, creating default")
with open(path, "w") as f:
f.write("")
return ""
```
### Voodoo constants
```python
# Wrong (no justification for the values)
TIMEOUT = 47
RETRIES = 5
# Right (self-documenting)
# HTTP requests typically complete within 30 seconds; longer timeout for slow networks
REQUEST_TIMEOUT = 30
# Three retries balances reliability vs speed; most flakes resolve by the second retry
MAX_RETRIES = 3
```
## Structural anti-patterns
### Skill body acts as a reference dump
If the skill has no workflow guidance and is just a list of API endpoints / config snippets / capabilities, the user has to do all the orchestration themselves. Tessl scores this 1-2/3 on workflow_clarity.
Fix: pick the 2-3 most common user tasks, write numbered workflows for them at the top, and move the reference-dump material to `references/<topic>.md`.
### One mega-SKILL.md when domains are clearly separable
If the skill covers AWS + GCP + Azure in one file, Claude loads all three even when the user only needs one. Split per domain:
```
cloud-deploy/
├── SKILL.md (overview + selection)
└── references/
├── aws.md
├── gcp.md
└── azure.md
```
### Skipping marketplace registration
The skill is functionally created on disk but not installable from any marketplace. CI's `validate` job catches inter-manifest divergence but doesn't flag a skill that's missing from all three.
After creating the skill directory:
1. Add the path to `.claude-plugin/marketplace.json` → relevant plugin's `skills` array
2. Same for `.cursor-plugin/marketplace.json`
3. Same for `.agents-plugin/marketplace.json`
All three must list the same skill path. Keep them in sync.
@@ -0,0 +1,86 @@
# Writing skill descriptions that actually trigger
The `description` field is the only part of a skill always loaded into the agent's context. Everything else — the SKILL.md body, the bundle files — is only read after the agent decides this skill applies. **If the description is weak, none of the rest of your work matters.**
## The four scoring dimensions (Tessl `descriptionJudge`)
Each rated 0-3, normalized to a 0-1 final score:
| Dimension | What it checks |
|---|---|
| **Specificity** | Lists concrete actions the skill performs (not just "helps with X") |
| **Trigger term quality** | Includes natural terms users actually say (variations, synonyms) |
| **Completeness** | Has both *what it does* AND *when to use* — usually via "Use when..." |
| **Distinctiveness / conflict risk** | Clearly scoped; won't fight other skills for activation |
## The pushy pattern (Anthropic-recommended)
From [Anthropic's skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md):
> Claude has a tendency to "undertrigger" skills — to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy". So for instance, instead of "How to build a simple fast dashboard to display internal Anthropic data.", you might write "How to build a simple fast dashboard to display internal Anthropic data. **Make sure to use this skill whenever the user mentions dashboards, data visualization, internal metrics, or wants to display any kind of company data, even if they don't explicitly ask for a 'dashboard.'**"
The pushy clause forces the agent to consider the skill across phrasings users actually say.
## Anatomy of a strong description
```yaml
description: <concrete-actions-the-skill-performs>. Applies <specific-conventions/methodologies>, generates <specific-outputs>, and validates <specific-checks>. Use when <task-A>, <task-B>, <task-C>, or when the user asks to <verb-1>, <verb-2>, <verb-3> - even if they don't explicitly say "<the-narrow-canonical-term>".
```
Three parts:
1. **What** — concrete actions, not vague "helps with". List specific outputs the skill produces.
2. **Use when** — explicit triggers. Multiple scenarios, ideally including domain-specific terms (repo names, CLI names, product names).
3. **Pushy clause** — the "even if they don't explicitly say…" part. This catches the user phrasings that miss the canonical term.
## Strong vs weak examples
### Weak
```yaml
# Vague "what", no "when", no trigger variations
description: Helps with k6 documentation.
# First-person, no "when"
description: I help you write k6 docs and TypeScript types.
# "When" is too narrow; missing trigger variations and pushy clause
description: Use when writing or reviewing k6 documentation across TypeScript types, user docs, and release notes.
```
### Strong (k6-docs from this repo, score 1.0/1.0 after rewrite)
```yaml
description: >
Write or review k6 documentation across the three k6 repositories - k6-DefinitelyTyped (TypeScript types),
k6-docs (user documentation), and k6 (release notes / changelog). Applies k6 doc style conventions,
generates TypeScript type definitions, drafts release notes, and validates examples by running them
against k6@master. Use when working on k6 documentation, the k6 changelog, the k6 release notes,
k6 API reference, k6 TypeScript types, load testing docs, or when the user asks to write, edit, or
review anything in the k6, k6-docs, or k6-DefinitelyTyped repos - even if they don't explicitly
say "documentation".
```
Why it works:
- Specific outputs: TypeScript type definitions, release notes, example validation
- Trigger variations: changelog, release notes, API reference, repo names, "load testing docs"
- Pushy clause: catches users editing the underlying repos without saying "documentation"
## Hard limits
| Limit | Value | Source |
|---|---|---|
| Max length | 1024 chars | Agent Skills spec |
| Voice | Third-person | Anthropic best-practices doc |
| XML tags | Not allowed | Agent Skills spec |
| Reserved words in `name` (NOT description) | `anthropic`, `claude` | Anthropic best-practices doc |
## Checklist before opening a PR
- [ ] Third-person ("Processes X" not "I process X" or "You can process X")
- [ ] Explicit "Use when..." clause
- [ ] Pushy clause at the end ("even if they don't say…")
- [ ] At least 4-5 trigger-term variations users naturally say
- [ ] ≤1024 chars
- [ ] No reserved words in `name`
- [ ] Concrete actions, not "helps with"
@@ -0,0 +1,242 @@
# The four-dimension rubric: per-dimension scoring + fix patterns
Tessl's `contentJudge` (the body-scoring half of the review) uses these four dimensions, each 0-3, weighted into the overall `reviewScore` shown in CI. The rubric is aligned with Anthropic's published best-practices doc.
## Contents
- [Conciseness](#conciseness)
- [Actionability](#actionability)
- [Workflow clarity](#workflow-clarity)
- [Progressive disclosure](#progressive-disclosure)
- [Combined heuristic: how to read a low score](#combined-heuristic)
## Conciseness
**What it scores:** Does every sentence justify its token cost? Or does the skill spend tokens explaining things Claude already knows?
**Anthropic's principle** ([best-practices doc § *Concise is key*](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)):
> Default assumption: Claude is already very smart. Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" "Can I assume Claude knows this?" "Does this paragraph justify its token cost?"
**Common 2/3 patterns to cut:**
- Intro paragraphs explaining what the technology is ("PromQL is a functional query language…", "Loki is a log aggregation system…") — Claude knows
- "First, install X, then run Y, then verify Z works" prose where each step could be a one-line code block
- Repeated cautions ("never drop a distinguishing label" said three times)
- "Why this matters" paragraphs longer than the actual rule
- Verbose `<details><summary>What is X?</summary>` blocks
**Fix pattern:**
```diff
- ## Querying metrics
-
- PromQL is a functional query language that lets you query time-series data. Each query
- consists of selectors (which series), operators (what to compute), and functions (how to
- aggregate). For example, to get the request rate for an API endpoint, you'd write:
-
- ```promql
- rate(http_requests_total{job="api"}[5m])
- ```
+ ## Querying metrics
+
+ ```promql
+ rate(http_requests_total{job="api"}[5m])
+ ```
+ Standard PromQL — selector, function, range vector.
```
The code is the explanation. The prose around it was tax.
### Trim multi-example lists to 1-2 inline + defer the rest
When a section enumerates N variants of the same shape (receiver types / cloud providers / language SDKs / panel types / etc.), the judge consistently scores conciseness 2/3 if all N are inline. Reliable lift to 3/3:
- Keep the **1-2 most-common** examples inline, in full
- Move the rest to `references/<topic>.md` under a "Other X types" section, often with brief stub code + a list of remaining types
- Link from the inline section: *"For email, webhook, Teams, … see [references/X.md § Other types](references/x.md#other-x-types)"*
Concrete: in this repo, `alerting-irm` inlined four contact-point receiver types (PagerDuty, Slack, email, webhook). Conciseness stuck at 2/3. Trimming to PagerDuty + Slack inline and moving email + webhook to `references/alerting.md § Contact point receiver types` (with a list of the other 10+ supported types) lifted the score from 94 → 100 across 3 consecutive local runs.
### Other 2/3 traps observed across the catalog
The 85→100 sweep on `cue-kind-definition`, `private-connectivity`, and `skill-authoring` surfaced these recurring conciseness traps. The judge's verbatim suggestion text is in italics — quoting it because the wording is consistent enough to grep for in future reviews.
- **"When to use this skill" bulleted lists** — overlap with the description's "Use when…" clause. *"the 'when to use this skill' list overlaps heavily with what should be in the description"*. Fix: delete the section entirely; the description is the trigger.
- **Multi-row tables that delegate to refs** — each cell is a one-line summary + ref link. *"the rubric table could be leaner since it mostly points to references"*. Fix: replace with a one-sentence summary + single ref link.
- **Cost / savings / business-value tangents** — dollar amounts, ROI calculations, "this saves N% on…". *"includes some unnecessary content like the cost savings section with specific dollar calculations"*. Fix: move to `references/<topic>.md § Cost`.
- **Bare decision-table rows** — single-line entries with no actionable content ("On-premises with no cloud provider — use Agent over internet"). *"the 'On-premises with no cloud provider' row"*. Fix: drop the row.
- **Multi-paragraph "Why X" explanations** — explaining the rationale at length. *"the 'Score variance' section spends many lines explaining LLM judge variance which could be condensed to 2-3 lines"*. Fix: collapse to one sentence stating the rule + one stating the reason.
- **Anti-patterns lists that duplicate critical rules** — *"the anti-patterns section partially duplicates the critical rules"*. Fix: either fold uniques into the rules, or replace the section with a one-line pointer to `references/anti-patterns.md`.
- **Intro paragraphs of the form "X drives the entire Y pipeline; each X describes…"** — orientation prose Claude doesn't need. *"the opening paragraph explaining what kinds drive and what gets generated from them is context Claude can infer"*. Fix: delete and start with the first workflow.
### Read the judge's suggestions, don't guess
`tessl skill review` (without `--json`) prints an `Assessment:` paragraph and `Suggestions:` bullets per dimension. **The Suggestions block names the exact sentences/sections to cut.** Copy them directly. Across this repo's 85→100 fixes, *every* lift came from applying the verbatim suggestion — guessing what to trim consistently took longer and lifted less.
## Actionability
**What it scores:** Could Claude, reading this once, do the task copy-paste? Or does Claude have to fill gaps from its own knowledge?
**Strong patterns:**
- Copy-paste-ready CLI commands with concrete arguments (not placeholders like `<your-token>` everywhere)
- Complete config snippets, not "configure X like Y"
- Templates with explicit `[Title]` / `[Section]` placeholders the user can fill
- Explicit `Input:` / `Output:` pairs for output-shape teaching
**Weak patterns (the 2/3 trap):**
- "Use the appropriate library to do X" without naming the library
- Placeholders that hide ambiguity: "Set the value to <appropriate>"
- "There are many ways to do this, here are some…" — pick one, ship it
- Discussing trade-offs before showing the solution
**Fix pattern:**
```diff
- ### Setup
-
- You'll need to configure Alloy with the appropriate scrape targets. Most users will want
- to start with a basic configuration and extend it.
+ ### Setup
+
+ ```alloy
+ prometheus.scrape "node_exporter" {
+ targets = [{"__address__" = "localhost:9100"}]
+ forward_to = [prometheus.remote_write.default.receiver]
+ }
+
+ prometheus.remote_write "default" {
+ endpoint {
+ url = sys.env("PROMETHEUS_REMOTE_WRITE_URL")
+ }
+ }
+ ```
```
## Workflow clarity
**What it scores:** Multi-step procedures with explicit ordering, decision points, and validation checkpoints.
**Strong patterns:**
- Numbered steps where order matters
- Explicit `if validation fails, do X; else continue` branches
- A copy-able checklist Claude can paste into its response
- Validate → fix → re-run feedback loops written out
**Weak patterns:**
- Bullet list of capabilities with no ordering
- "Configure X, then Y, then Z" without per-step validation
- No "what to do if it fails" guidance
- Implicit ordering ("Make sure to do A. By the way, you also need B before A")
**Fix pattern:**
```diff
- ## Setting up alerts
-
- Create contact points, add notification policies, write the alert rules. Make sure to
- test routing before going live.
+ ## Setting up alerts
+
+ 1. **Create contact points** (where notifications go):
+ ```bash
+ curl -X POST .../api/v1/provisioning/contact-points -d @contact-points.json
+ # Verify:
+ curl .../api/v1/provisioning/contact-points | jq '.[].name'
+ ```
+
+ 2. **Add notification policies** (which alerts go where):
+ ```bash
+ curl -X POST .../api/v1/provisioning/policies -d @policies.json
+ ```
+
+ 3. **Write alert rules**:
+ ```bash
+ curl -X POST .../api/v1/provisioning/alert-rules -d @rules.json
+ ```
+
+ 4. **Verify routing** before going live:
+ ```bash
+ curl -X POST .../api/v1/provisioning/policies/test -d @test-alert.json
+ # If output doesn't show the expected contact point, re-check matchers in step 2.
+ ```
```
The "verify" + "what to do if it fails" steps are what lift a workflow_clarity score from 2 to 3.
## Progressive disclosure
**What it scores:** Are you using the three-level loading model correctly? Or is everything dumped into one 600-line SKILL.md?
**The three levels** ([skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md)):
1. **Metadata** (name + description) — always loaded, ~100 words
2. **SKILL.md body** — loaded when the skill triggers, <500 lines
3. **Bundled resources** (scripts/, references/, assets/) — loaded only when needed
**Strong patterns:**
- SKILL.md under 200 lines for the routing layer
- `references/*.md` for detailed material, linked one level deep
- Domain-organized refs: `references/aws.md`, `references/gcp.md`, `references/azure.md` (Claude reads only the relevant one)
- Reference files >100 lines have a table of contents at the top
**Weak patterns:**
- One 800-line SKILL.md with sections that should be separate files
- Nested references: `SKILL.md → a.md → b.md` (Claude may truncate at level 2)
- Dangling references — `[see X](references/x.md)` where the file doesn't exist
- All content inline even when there are clear domain boundaries
**Fix pattern (split):**
Before:
```
skills/grafana-cloud/admin/
└── SKILL.md (244 lines, includes SSO config, Terraform examples, full API endpoint reference)
```
After:
```
skills/grafana-cloud/admin/
├── SKILL.md (under 100 lines: overview, workflows, RBAC, service accounts)
└── references/
├── sso.md (OAuth + SAML + GitHub OAuth configs)
├── terraform.md (Terraform examples)
└── api-reference.md (Cloud API + Admin endpoints + audit logs)
```
Then SKILL.md links to each `references/*.md` one level deep.
### When NOT to split
Some skills are intentionally short routing documents (e.g. `grafana-k6/k6-docs` — 33 lines of overview pointing at a deep `references/workflows/*.md` bundle). The four-dimension rubric penalizes these for "not independently actionable", BUT the architecture is correct.
For routing documents: add a minimal copy-paste-ready "validation loop" inline so SKILL.md is independently actionable for the most common task, while preserving the bundle for everything else. See `skills/grafana-k6/k6-docs/SKILL.md` for the pattern that lifted its score from 72 → 100 without losing the bundle.
### The "bundle files weren't provided" 2/3 ceiling
`tessl skill review` (and CI) only reads SKILL.md — not files under `references/`. The LLM judge sees the link in SKILL.md but cannot verify the target exists, so it caps progressive_disclosure at 2/3 with the line *"no bundle files were provided, so the references are dangling"*. Two responses:
- **Login locally**: `tessl login`, then `tessl skill review` includes bundle files in the judgment (output note: *"Only SKILL.md was reviewed. Log in with tessl login to include all supporting files in the review."*). CI doesn't login, so this only helps local-only sanity checks.
- **Don't write your way into the trap**: a skill that *itself* names "dangling references" as a danger triggers the judge's caution explicitly. If your SKILL.md uses references heavily, keep the dangling-references warning in `references/anti-patterns.md` rather than in the body.
## Combined heuristic
If you see a 2/3 across all four dimensions, the skill is doing too much narrative and not enough structure. If just one dimension is low:
| Low dimension | First fix to try |
|---|---|
| Conciseness | Cut the introduction. Cut explanations of what known technologies are. |
| Actionability | Replace one prose paragraph per section with a code block. |
| Workflow clarity | Add numbered steps + one validation step per workflow. |
| Progressive disclosure | Split sections >50 lines into `references/*.md`. |
Then re-run `tessl skill review --json` and iterate. Score lift per fix is typically 5-15 points.
@@ -0,0 +1,384 @@
---
name: brainstorming-research-ideas
description: Guides researchers through structured ideation frameworks to discover high-impact research directions. Use when exploring new problem spaces, pivoting between projects, or seeking novel angles on existing work.
version: 1.0.0
author: Orchestra Research
license: MIT
tags: [Research Ideation, Brainstorming, Problem Discovery, Creative Thinking, Research Strategy]
dependencies: []
---
# Research Idea Brainstorming
Structured frameworks for discovering the next research idea. This skill provides ten complementary ideation lenses that help researchers move from vague curiosity to concrete, defensible research proposals. Each framework targets a different cognitive mode—use them individually or combine them for comprehensive exploration.
## When to Use This Skill
- Starting a new research direction and need structured exploration
- Feeling stuck on a current project and want fresh angles
- Evaluating whether a half-formed idea has real potential
- Preparing for a brainstorming session with collaborators
- Transitioning between research areas and seeking high-leverage entry points
- Reviewing a field and looking for underexplored gaps
**Do NOT use this skill when**:
- You already have a well-defined research question and need execution guidance
- You need help with experimental design or methodology (use domain-specific skills)
- You want a literature review (use `scientific-skills:literature-review`)
---
## Core Ideation Frameworks
### 1. Problem-First vs. Solution-First Thinking
Research ideas originate from two distinct modes. Knowing which mode you are in prevents a common failure: building solutions that lack real problems, or chasing problems without feasible approaches.
**Problem-First** (pain point → method):
- Start with a concrete failure, bottleneck, or unmet need
- Naturally yields impactful work because the motivation is intrinsic
- Risk: may converge on incremental fixes rather than paradigm shifts
**Solution-First** (new capability → application):
- Start with a new tool, insight, or technique seeking application
- Often drives breakthroughs by unlocking previously impossible approaches
- Risk: "hammer looking for a nail"—solution may lack genuine demand
**Workflow**:
1. Write down your idea in one sentence
2. Classify it: Is this problem-first or solution-first?
3. If problem-first → verify the problem matters (who suffers? how much?)
4. If solution-first → identify at least two genuine problems it addresses
5. For either mode, articulate the gap: what cannot be done today that this enables?
**Self-Check**:
- [ ] Can I name a specific person or community who needs this?
- [ ] Is the problem I am solving actually unsolved (not just under-marketed)?
- [ ] If solution-first, does the solution create new capability or just replicate existing ones?
---
### 2. The Abstraction Ladder
Every research problem sits at a particular level of abstraction. Deliberately moving up or down the ladder reveals ideas invisible at your current level.
| Direction | Action | Outcome |
|-----------|--------|---------|
| **Move Up** (generalize) | Turn a specific result into a broader principle | Framework papers, theoretical contributions |
| **Move Down** (instantiate) | Test a general paradigm under concrete constraints | Empirical papers, surprising failure analyses |
| **Move Sideways** (analogize) | Apply same abstraction level to adjacent domain | Cross-pollination, transfer papers |
**Workflow**:
1. State your current research focus in one sentence
2. Move UP: What is the general principle behind this? What class of problems does this belong to?
3. Move DOWN: What is the most specific, constrained instance of this? What happens at the extreme?
4. Move SIDEWAYS: Where else does this pattern appear in a different field?
5. For each new level, ask: Is this a publishable contribution on its own?
**Example**:
- **Current**: "Improving retrieval accuracy for RAG systems"
- **Up**: "What makes context selection effective for any augmented generation system?"
- **Down**: "How does retrieval accuracy degrade when documents are adversarially perturbed?"
- **Sideways**: "Database query optimization uses similar relevance ranking—what can we borrow?"
---
### 3. Tension and Contradiction Hunting
Breakthroughs often come from resolving tensions between widely accepted but seemingly conflicting goals. These contradictions are not bugs—they are the research opportunity.
**Common Research Tensions**:
| Tension Pair | Research Opportunity |
|-------------|---------------------|
| Performance ↔ Efficiency | Can we match SOTA with 10x less compute? |
| Privacy ↔ Utility | Can federated/encrypted methods close the accuracy gap? |
| Generality ↔ Specialization | When does fine-tuning beat prompting, and why? |
| Safety ↔ Capability | Can alignment improve rather than tax capability? |
| Interpretability ↔ Performance | Do mechanistic insights enable better architectures? |
| Scale ↔ Accessibility | Can small models replicate emergent behaviors? |
**Workflow**:
1. Pick your research area
2. List the top 3-5 desiderata (things everyone wants)
3. Identify pairs that are commonly treated as trade-offs
4. For each pair, ask: Is this trade-off fundamental or an artifact of current methods?
5. If artifact → the reconciliation IS your research contribution
6. If fundamental → characterizing the Pareto frontier is itself valuable
**Self-Check**:
- [ ] Have I confirmed this tension is real (not just assumed)?
- [ ] Can I point to papers that optimize for each side independently?
- [ ] Is my proposed reconciliation technically plausible, not just aspirational?
---
### 4. Cross-Pollination (Analogy Transfer)
Borrowing structural ideas from other disciplines is one of the most generative research heuristics. Many foundational techniques emerged this way—attention mechanisms draw from cognitive science, genetic algorithms from biology, adversarial training from game theory.
**Requirements for a Valid Analogy**:
- **Structural fidelity**: The mapping must hold at the level of underlying mechanisms, not just surface similarity
- **Non-obvious connection**: If the link is well-known, the novelty is gone
- **Testable predictions**: The analogy should generate concrete hypotheses
**High-Yield Source Fields for ML Research**:
| Source Field | Transferable Concepts |
|-------------|----------------------|
| Neuroscience | Attention, memory consolidation, hierarchical processing |
| Physics | Energy-based models, phase transitions, renormalization |
| Economics | Mechanism design, auction theory, incentive alignment |
| Ecology | Population dynamics, niche competition, co-evolution |
| Linguistics | Compositionality, pragmatics, grammatical induction |
| Control Theory | Feedback loops, stability, adaptive regulation |
**Workflow**:
1. Describe your problem in domain-agnostic language (strip the jargon)
2. Ask: What other field solves a structurally similar problem?
3. Study that field's solution at the mechanism level
4. Map the solution back to your domain, preserving structural relationships
5. Generate testable predictions from the analogy
6. Validate: Does the borrowed idea actually improve outcomes?
---
### 5. The "What Changed?" Principle
Strong ideas often come from revisiting old problems under new conditions. Advances in hardware, scale, data availability, or regulations can invalidate prior assumptions and make previously impractical approaches viable.
**Categories of Change to Monitor**:
| Change Type | Example | Research Implication |
|------------|---------|---------------------|
| **Compute** | GPUs 10x faster | Methods dismissed as too expensive become feasible |
| **Scale** | Trillion-token datasets | Statistical arguments that failed at small scale may now hold |
| **Regulation** | EU AI Act, GDPR | Creates demand for compliant alternatives |
| **Tooling** | New frameworks, APIs | Reduces implementation barrier for complex methods |
| **Failure** | High-profile system failures | Exposes gaps in existing approaches |
| **Cultural** | New user behaviors | Shifts what problems matter most |
**Workflow**:
1. Pick a well-known negative result or abandoned approach (3-10 years old)
2. List the assumptions that led to its rejection
3. For each assumption, ask: Is this still true today?
4. If any assumption has been invalidated → re-run the idea under new conditions
5. Frame the contribution: "X was previously impractical because Y, but Z has changed"
---
### 6. Failure Analysis and Boundary Probing
Understanding where a method breaks is often as valuable as showing where it works. Boundary probing systematically exposes the conditions under which accepted techniques fail.
**Types of Boundaries to Probe**:
- **Distributional**: What happens with out-of-distribution inputs?
- **Scale**: Does the method degrade at 10x or 0.1x the typical scale?
- **Adversarial**: Can the method be deliberately broken?
- **Compositional**: Does performance hold when combining multiple capabilities?
- **Temporal**: Does the method degrade over time (concept drift)?
**Workflow**:
1. Select a widely-used method with strong reported results
2. Identify the implicit assumptions in its evaluation (dataset, scale, domain)
3. Systematically violate each assumption
4. Document where and how the method breaks
5. Diagnose the root cause of each failure
6. Propose a fix or explain why the failure is fundamental
**Self-Check**:
- [ ] Am I probing genuine boundaries, not just confirming known limitations?
- [ ] Can I explain WHY the method fails, not just THAT it fails?
- [ ] Does my analysis suggest a constructive path forward?
---
### 7. The Simplicity Test
Before accepting complexity, ask whether a simpler approach suffices. Fields sometimes over-index on elaborate solutions when a streamlined baseline performs competitively.
**Warning Signs of Unnecessary Complexity**:
- The method has many hyperparameters with narrow optimal ranges
- Ablations show most components contribute marginally
- A simple baseline was never properly tuned or evaluated
- The improvement over baselines is within noise on most benchmarks
**Workflow**:
1. Identify the current SOTA method for your problem
2. Strip it to its simplest possible core (what is the one key idea?)
3. Build that minimal version with careful engineering
4. Compare fairly: same compute budget, same tuning effort
5. If the gap is small → the contribution is the simplicity itself
6. If the gap is large → you now understand what the complexity buys
**Contribution Framing**:
- "We show that [simple method] with [one modification] matches [complex SOTA]"
- "We identify [specific component] as the critical driver, not [other components]"
---
### 8. Stakeholder Rotation
Viewing a system from multiple perspectives reveals distinct classes of research questions. Each stakeholder sees different friction, risk, and opportunity.
**Stakeholder Perspectives**:
| Stakeholder | Key Questions |
|-------------|---------------|
| **End User** | Is this usable? What errors are unacceptable? What is the latency tolerance? |
| **Developer** | Is this debuggable? What is the maintenance burden? How does it compose? |
| **Theorist** | Why does this work? What are the formal guarantees? Where are the gaps? |
| **Adversary** | How can this be exploited? What are the attack surfaces? |
| **Ethicist** | Who is harmed? What biases are embedded? Who is excluded? |
| **Regulator** | Is this auditable? Can decisions be explained? Is there accountability? |
| **Operator** | What is the cost? How does it scale? What is the failure mode? |
**Workflow**:
1. Describe your system or method in one paragraph
2. Assume each stakeholder perspective in turn (spend 5 minutes per role)
3. For each perspective, list the top 3 concerns or questions
4. Identify which concerns are unaddressed by existing work
5. The unaddressed concern with the broadest impact is your research question
---
### 9. Composition and Decomposition
Novelty often emerges from recombination or modularization. Innovation frequently lies not in new primitives, but in how components are arranged or separated.
**Composition** (combining existing techniques):
- Identify two methods that solve complementary subproblems
- Ask: What emergent capability arises from combining them?
- Example: RAG + Chain-of-Thought → retrieval-augmented reasoning
**Decomposition** (breaking apart monolithic systems):
- Identify a complex system with entangled components
- Ask: Which component is the actual bottleneck?
- Example: Decomposing "fine-tuning" into data selection, optimization, and regularization reveals that data selection often matters most
**Workflow**:
1. List the 5-10 key components or techniques in your area
2. **Compose**: Pick pairs and ask what happens when you combine them
3. **Decompose**: Pick a complex method and isolate each component's contribution
4. For compositions: Does the combination create emergent capabilities?
5. For decompositions: Does isolation reveal a dominant or redundant component?
---
### 10. The "Explain It to Someone" Test
A strong research idea should be defensible in two sentences to a smart non-expert. This test enforces clarity of purpose and sharpens the value proposition.
**The Two-Sentence Template**:
> **Sentence 1** (Problem): "[Domain] currently struggles with [specific problem], which matters because [concrete consequence]."
> **Sentence 2** (Insight): "We [approach] by [key mechanism], which works because [reason]."
**If You Cannot Fill This Template**:
- The problem may not be well-defined yet → return to Framework 1
- The insight may not be clear yet → return to Framework 7 (simplify)
- The significance may not be established → return to Framework 3 (find the tension)
**Calibration Questions**:
- Would a smart colleague outside your subfield understand why this matters?
- Does the explanation stand without jargon?
- Can you predict what a skeptic's first objection would be?
---
## Integrated Brainstorming Workflow
Use this end-to-end workflow to go from blank page to ranked research ideas.
### Phase 1: Diverge (Generate Candidates)
**Goal**: Produce 10-20 candidate ideas without filtering.
1. **Scan for tensions** (Framework 3): List 5 trade-offs in your field
2. **Check what changed** (Framework 5): List 3 recent shifts (compute, data, regulation)
3. **Probe boundaries** (Framework 6): Pick 2 popular methods and find where they break
4. **Cross-pollinate** (Framework 4): Pick 1 idea from an adjacent field
5. **Compose/decompose** (Framework 9): Combine 2 existing techniques or split 1 apart
6. **Climb the abstraction ladder** (Framework 2): For each candidate, generate up/down/sideways variants
### Phase 2: Converge (Filter and Rank)
**Goal**: Narrow to 3-5 strongest ideas.
Apply these filters to each candidate:
| Filter | Question | Kill Criterion |
|--------|----------|----------------|
| **Explain-It Test** (F10) | Can I state this in two sentences? | If no → idea is not yet clear |
| **Problem-First Check** (F1) | Is the problem genuine and important? | If no one suffers from this → drop it |
| **Simplicity Test** (F7) | Is the complexity justified? | If a simpler approach works → simplify or drop |
| **Stakeholder Check** (F8) | Who benefits? Who might object? | If no clear beneficiary → drop it |
| **Feasibility** | Can I execute this with available resources? | If clearly infeasible → park it for later |
### Phase 3: Refine (Sharpen the Winner)
**Goal**: Turn the top idea into a concrete research plan.
1. Write the two-sentence pitch (Framework 10)
2. Identify the core tension being resolved (Framework 3)
3. Specify the abstraction level (Framework 2)
4. List 3 concrete experiments that would validate the idea
5. Anticipate the strongest objection and prepare a response
6. Define a 2-week pilot that would provide signal on feasibility
**Completion Checklist**:
- [ ] Two-sentence pitch is clear and compelling
- [ ] Problem is genuine (problem-first check passed)
- [ ] Approach is justified (simplicity test passed)
- [ ] At least one stakeholder clearly benefits
- [ ] Core experiments are specified
- [ ] Feasibility pilot is defined
- [ ] Strongest objection has a response
---
## Framework Selection Guide
Not sure which framework to start with? Use this decision guide:
| Your Situation | Start With |
|---------------|------------|
| "I don't know what area to work in" | Tension Hunting (F3) → What Changed (F5) |
| "I have a vague area but no specific idea" | Abstraction Ladder (F2) → Failure Analysis (F6) |
| "I have an idea but I'm not sure it's good" | Explain-It Test (F10) → Simplicity Test (F7) |
| "I have a good idea but need a fresh angle" | Cross-Pollination (F4) → Stakeholder Rotation (F8) |
| "I want to combine existing work into something new" | Composition/Decomposition (F9) |
| "I found a cool technique and want to apply it" | Problem-First Check (F1) → Stakeholder Rotation (F8) |
| "I want to challenge conventional wisdom" | Failure Analysis (F6) → Simplicity Test (F7) |
---
## Common Pitfalls in Research Ideation
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| **Novelty without impact** | "No one has done X" but no one needs X | Apply Problem-First Check (F1) |
| **Incremental by default** | Idea is +2% on a benchmark | Climb the Abstraction Ladder (F2) |
| **Complexity worship** | Method has 8 components, each helping marginally | Apply Simplicity Test (F7) |
| **Echo chamber** | All ideas come from reading the same 10 papers | Use Cross-Pollination (F4) |
| **Stale assumptions** | "This was tried and didn't work" (5 years ago) | Apply What Changed (F5) |
| **Single-perspective bias** | Only considering the ML engineer's view | Use Stakeholder Rotation (F8) |
| **Premature convergence** | Committed to first idea without exploring alternatives | Run full Diverge phase |
---
## Usage Instructions for Agents
When a researcher asks for help brainstorming research ideas:
1. **Identify their starting point**: Are they exploring a new area, stuck on a current project, or evaluating an existing idea?
2. **Select appropriate frameworks**: Use the Framework Selection Guide to pick 2-3 relevant lenses
3. **Walk through frameworks interactively**: Apply each framework step-by-step, asking the researcher for domain-specific inputs
4. **Generate candidates**: Aim for 10-20 raw ideas across frameworks
5. **Filter and rank**: Apply the Converge phase filters to narrow to top 3-5
6. **Refine the winner**: Help articulate the two-sentence pitch and define concrete next steps
**Key Principles**:
- Push for specificity—vague ideas ("improve efficiency") are not actionable
- Challenge assumptions—ask "why?" at least three times
- Maintain a written list of all candidates, even rejected ones (they may recombine later)
- The researcher makes the final call on which ideas to pursue; the agent facilitates structured thinking
@@ -0,0 +1,179 @@
---
name: compatibility-testing
description: "Cross-browser, cross-platform, and cross-device compatibility testing ensuring consistent experience across environments. Use when validating browser support, testing responsive design, or ensuring platform compatibility."
category: specialized-testing
priority: medium
tokenEstimate: 800
agents: [qe-visual-tester, qe-test-executor, qe-performance-tester]
implementation_status: optimized
optimization_version: 1.0
last_optimized: 2025-12-02
dependencies: []
quick_reference_card: true
tags: [compatibility, cross-browser, responsive, browserstack, playwright, devices]
trust_tier: 3
validation:
schema_path: schemas/output.json
validator_path: scripts/validate-config.json
---
# Compatibility Testing
## Browser engine
Browser-driven checks (viewport emulation, responsive validation, cross-browser screenshots) should go through the **qe-browser** fleet skill (`.claude/skills/qe-browser/`). Vibium is installed by `aqe init`. Quick reference:
```bash
# Viewport emulation per breakpoint
vibium viewport 375 667 # mobile
vibium viewport 768 1024 # tablet
vibium viewport 1920 1080 # desktop
# Full device emulation (user-agent, DPR, touch)
vibium emulate-device "iPhone 15"
# Visual diff per viewport
for vp in "375 667 mobile" "768 1024 tablet" "1920 1080 desktop"; do
read w h name <<< "$vp"
vibium viewport $w $h
node .claude/skills/qe-browser/scripts/visual-diff.js --name "homepage-$name"
done
```
Cross-browser (Firefox/Safari) still requires Playwright or a cloud device farm today — Vibium's BiDi backend is Chrome-only at v26.3.x.
<default_to_action>
When validating cross-browser/platform compatibility:
1. DEFINE browser matrix (cover 95%+ of users)
2. TEST responsive breakpoints (mobile, tablet, desktop)
3. RUN in parallel across browsers/devices
4. USE cloud services for device coverage (BrowserStack, Sauce Labs)
5. COMPARE visual screenshots across platforms
**Quick Compatibility Checklist:**
- Chrome, Firefox, Safari, Edge (latest + N-1)
- Mobile Safari (iOS), Mobile Chrome (Android)
- Screen sizes: 320px, 768px, 1920px
- Test on actual target devices for critical flows
**Critical Success Factors:**
- Users access from 100+ browser/device combinations
- Test where users are, not where you develop
- Cloud testing reduces 10 hours to 15 minutes
</default_to_action>
## Quick Reference Card
### When to Use
- Before release
- After CSS/layout changes
- Launching in new markets
- Responsive design validation
---
## Cross-Browser with Playwright
```javascript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'] } }
]
});
// Run: npx playwright test --project=chromium --project=firefox
```
---
## Cloud Testing Integration
```javascript
// BrowserStack configuration
const capabilities = {
'browserName': 'Chrome',
'browser_version': '118.0',
'os': 'Windows',
'os_version': '11',
'browserstack.user': process.env.BROWSERSTACK_USER,
'browserstack.key': process.env.BROWSERSTACK_KEY
};
// Parallel execution across devices
const deviceMatrix = [
{ os: 'Windows', browser: 'Chrome' },
{ os: 'OS X', browser: 'Safari' },
{ os: 'Android', device: 'Samsung Galaxy S24' },
{ os: 'iOS', device: 'iPhone 15' }
];
```
---
## Agent-Driven Compatibility Testing
```typescript
// Cross-platform visual comparison
await Task("Compatibility Testing", {
url: 'https://example.com',
browsers: ['chrome', 'firefox', 'safari', 'edge'],
devices: ['desktop', 'tablet', 'mobile'],
platform: 'browserstack',
parallel: true
}, "qe-visual-tester");
// Returns:
// {
// combinations: 12, // 4 browsers × 3 devices
// passed: 11,
// differences: [{ browser: 'safari', device: 'mobile', diff: 0.02 }]
// }
```
---
## Agent Coordination Hints
### Memory Namespace
```
aqe/compatibility-testing/
├── browser-matrix/* - Browser/version configurations
├── device-matrix/* - Device configurations
├── visual-diffs/* - Cross-browser visual differences
└── reports/* - Compatibility reports
```
### Fleet Coordination
```typescript
const compatFleet = await FleetManager.coordinate({
strategy: 'compatibility-testing',
agents: [
'qe-visual-tester', // Visual comparison
'qe-test-executor', // Cross-browser execution
'qe-performance-tester' // Performance by platform
],
topology: 'parallel'
});
```
---
## Related Skills
- [mobile-testing](../mobile-testing/) - Mobile-specific testing
- [visual-testing-advanced](../visual-testing-advanced/) - Visual regression
- [accessibility-testing](../accessibility-testing/) - Cross-platform a11y
---
## Remember
**Cover 95%+ of your user base.** Use analytics to identify actual browser/device usage. Don't waste time on browsers nobody uses.
**With Agents:** Agents orchestrate parallel cross-browser testing across cloud platforms. `qe-visual-tester` catches visual inconsistencies across platforms automatically.
@@ -0,0 +1,410 @@
# =============================================================================
# AQE Compatibility Testing Skill Evaluation Suite v1.0.0
# Cross-browser, cross-platform, cross-device compatibility testing evaluation
# =============================================================================
skill: compatibility-testing
version: 1.0.0
description: >
Comprehensive evaluation suite for cross-browser and cross-platform
compatibility testing. Tests browser matrix coverage, device tier validation,
responsive breakpoints, and visual difference detection.
# =============================================================================
# Multi-Model Configuration
# =============================================================================
models_to_test:
- claude-sonnet-4-6 # Primary (high accuracy expected)
- claude-haiku-4-5 # Fast model (minimum quality floor)
# =============================================================================
# MCP Integration Configuration
# =============================================================================
mcp_integration:
enabled: true
namespace: skill-validation
query_patterns: true
track_outcomes: true
store_patterns: true
share_learning: true
update_quality_gate: true
target_agents:
- qe-learning-coordinator
- qe-queen-coordinator
- qe-visual-tester
# =============================================================================
# Learning Configuration
# =============================================================================
learning:
store_success_patterns: true
store_failure_patterns: true
pattern_ttl_days: 90
min_confidence_to_store: 0.7
cross_model_comparison: true
# =============================================================================
# Result Format Configuration
# =============================================================================
result_format:
json_output: true
markdown_report: true
include_raw_output: false
include_timing: true
include_token_usage: true
# =============================================================================
# Environment Setup
# =============================================================================
setup:
required_tools:
- jq
environment_variables:
BROWSER_COVERAGE_TARGET: "95"
fixtures:
- name: responsive_layout
content: |
<div class="container">
<nav class="main-nav">Navigation</nav>
<main class="content">Main content</main>
<aside class="sidebar">Sidebar</aside>
</div>
# =============================================================================
# Test Cases
# =============================================================================
test_cases:
# -------------------------------------------------------------------------
# Browser Matrix Tests
# -------------------------------------------------------------------------
- id: tc001_browser_matrix_detection
description: "Detect and report browser compatibility issues"
category: browser_matrix
priority: critical
input:
code: |
// Uses CSS Grid without fallback
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
// Uses CSS custom properties
:root {
--primary-color: #007bff;
}
.btn { background: var(--primary-color); }
context:
language: css
target_browsers: ["chrome", "firefox", "safari", "edge", "ie11"]
expected_output:
must_contain:
- "browser"
- "compatibility"
- "grid"
must_not_contain:
- "all browsers supported"
finding_count:
min: 1
max: 3
validation:
schema_check: true
keyword_match_threshold: 0.8
reasoning_quality_min: 0.7
- id: tc002_webkit_specific_issues
description: "Detect WebKit/Safari-specific compatibility issues"
category: browser_matrix
priority: high
input:
code: |
.flex-container {
display: flex;
gap: 10px; /* Gap not supported in older Safari */
}
.gradient {
background: linear-gradient(to right, red, blue);
}
input[type="date"] {
/* Date input styling varies by browser */
-webkit-appearance: none;
}
context:
language: css
target_browsers: ["safari", "mobile-safari"]
expected_output:
must_contain:
- "Safari"
- "WebKit"
finding_count:
min: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# Device Coverage Tests
# -------------------------------------------------------------------------
- id: tc003_device_tier_coverage
description: "Validate device coverage across tiers"
category: device_coverage
priority: critical
input:
prompt: |
Analyze compatibility for the following device matrix:
Tier 1 (60% users): iPhone 15, Galaxy S24, iPad Pro
Tier 2 (30% users): iPhone 13, Pixel 7, Galaxy A54
Tier 3 (10% users): iPhone 11, older Android devices
context:
test_type: device_coverage
minimum_coverage: 95
expected_output:
must_contain:
- "Tier 1"
- "Tier 2"
- "coverage"
- "device"
validation:
schema_check: true
keyword_match_threshold: 0.8
- id: tc004_mobile_specific_issues
description: "Detect mobile-specific compatibility issues"
category: device_coverage
priority: high
input:
code: |
// Touch events without fallback
element.addEventListener('touchstart', handler);
// Fixed positioning issues on iOS
.modal {
position: fixed;
bottom: 0;
}
// Viewport units with address bar
.full-height {
height: 100vh;
}
context:
language: javascript
platforms: ["ios", "android"]
expected_output:
must_contain:
- "mobile"
- "touch"
- "viewport"
validation:
schema_check: true
# -------------------------------------------------------------------------
# Responsive Breakpoint Tests
# -------------------------------------------------------------------------
- id: tc005_responsive_breakpoints
description: "Validate responsive design across breakpoints"
category: responsive
priority: high
input:
code: |
@media (max-width: 320px) {
.nav { display: none; }
}
@media (min-width: 768px) {
.sidebar { display: block; }
}
@media (min-width: 1200px) {
.container { max-width: 1140px; }
}
context:
language: css
breakpoints: [320, 480, 768, 1024, 1200, 1920]
expected_output:
must_contain:
- "breakpoint"
- "mobile"
- "responsive"
must_not_contain:
- "all breakpoints pass"
validation:
schema_check: true
keyword_match_threshold: 0.8
# -------------------------------------------------------------------------
# Visual Consistency Tests
# -------------------------------------------------------------------------
- id: tc006_visual_differences
description: "Detect visual differences across platforms"
category: visual
priority: medium
input:
prompt: |
Compare visual rendering of the following component across browsers:
- Chrome (Windows): Button renders with 4px border-radius
- Firefox (Windows): Button renders with 4px border-radius
- Safari (macOS): Button renders with 6px border-radius (native styling)
- Edge (Windows): Button renders with 4px border-radius
context:
test_type: visual_comparison
threshold: 0.02
expected_output:
must_contain:
- "visual"
- "difference"
- "Safari"
finding_count:
min: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# JavaScript Compatibility Tests
# -------------------------------------------------------------------------
- id: tc007_javascript_features
description: "Detect JavaScript feature compatibility issues"
category: javascript
priority: high
input:
code: |
// Optional chaining - not supported in older browsers
const value = obj?.nested?.property;
// Nullish coalescing
const result = value ?? 'default';
// Array.flat() - ES2019
const flat = nested.flat(2);
// BigInt - limited support
const big = 9007199254740991n;
context:
language: javascript
target_browsers: ["chrome-80", "firefox-75", "safari-13", "ie11"]
expected_output:
must_contain:
- "JavaScript"
- "compatibility"
- "ES"
finding_count:
min: 2
validation:
schema_check: true
keyword_match_threshold: 0.8
# -------------------------------------------------------------------------
# Negative Tests
# -------------------------------------------------------------------------
- id: tc010_no_issues_modern_code
description: "Modern browsers should pass with standard code"
category: negative
priority: high
input:
code: |
.container {
display: flex;
justify-content: center;
align-items: center;
}
.button {
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
context:
language: css
target_browsers: ["chrome-latest", "firefox-latest", "safari-latest"]
expected_output:
must_contain:
- "compatible"
must_not_contain:
- "critical"
- "breaking"
finding_count:
max: 1
validation:
schema_check: true
# -------------------------------------------------------------------------
# Edge Cases
# -------------------------------------------------------------------------
- id: tc011_mixed_platform_issues
description: "Handle issues affecting multiple but not all platforms"
category: edge_cases
priority: medium
input:
prompt: |
Analyze this scenario:
- Feature works on Chrome, Firefox, Edge
- Feature partially works on Safari desktop
- Feature fails on Safari iOS
- Feature not tested on Opera
context:
test_type: cross_platform
expected_output:
must_contain:
- "Safari"
- "partial"
- "platform"
validation:
schema_check: true
allow_partial: true
# =============================================================================
# Success Criteria
# =============================================================================
success_criteria:
pass_rate: 0.90
critical_pass_rate: 1.0
avg_reasoning_quality: 0.70
max_execution_time_ms: 300000
cross_model_variance: 0.15
# =============================================================================
# Metadata
# =============================================================================
metadata:
author: "@aqe-team"
created: "2026-02-02"
last_updated: "2026-02-02"
coverage_target: "Browser matrix, device tiers, responsive breakpoints, visual consistency"
@@ -0,0 +1,551 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://agentic-qe.dev/schemas/compatibility-testing-output.json",
"title": "AQE Compatibility Testing Skill Output Schema",
"description": "Schema for cross-browser, cross-platform, and cross-device compatibility testing output. Extends base skill output template with browser matrix, device coverage, and visual consistency fields.",
"type": "object",
"required": ["skillName", "version", "timestamp", "status", "trustTier", "output"],
"properties": {
"skillName": {
"type": "string",
"const": "compatibility-testing",
"description": "Skill identifier"
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$",
"description": "Semantic version of the skill"
},
"timestamp": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})?$",
"description": "ISO 8601 timestamp of test execution"
},
"status": {
"type": "string",
"enum": ["success", "partial", "failed", "skipped"],
"description": "Overall execution status"
},
"trustTier": {
"type": "integer",
"const": 3,
"description": "Trust tier 3: has schema, validator, and eval suite"
},
"output": {
"type": "object",
"required": ["summary", "browserMatrix", "deviceCoverage", "metrics"],
"properties": {
"summary": {
"type": "string",
"minLength": 10,
"maxLength": 2000,
"description": "Human-readable summary of compatibility test results"
},
"score": {
"$ref": "#/$defs/compatibilityScore",
"description": "Overall compatibility score"
},
"browserMatrix": {
"$ref": "#/$defs/browserMatrix",
"description": "Browser coverage and results matrix"
},
"deviceCoverage": {
"$ref": "#/$defs/deviceCoverage",
"description": "Device coverage breakdown by tier"
},
"platformResults": {
"type": "array",
"items": {
"$ref": "#/$defs/platformResult"
},
"maxItems": 100,
"description": "Results by platform/browser/device combination"
},
"findings": {
"type": "array",
"items": {
"$ref": "#/$defs/compatibilityFinding"
},
"maxItems": 500,
"description": "Compatibility issues discovered"
},
"recommendations": {
"type": "array",
"items": {
"$ref": "#/$defs/recommendation"
},
"maxItems": 100,
"description": "Recommendations for improving compatibility"
},
"metrics": {
"$ref": "#/$defs/compatibilityMetrics",
"description": "Quantitative compatibility metrics"
},
"artifacts": {
"type": "array",
"items": {
"$ref": "#/$defs/artifact"
},
"maxItems": 50,
"description": "Generated artifacts (screenshots, reports)"
},
"responsiveBreakpoints": {
"type": "array",
"items": {
"$ref": "#/$defs/breakpointResult"
},
"description": "Results by responsive breakpoint"
},
"visualDifferences": {
"type": "array",
"items": {
"$ref": "#/$defs/visualDifference"
},
"description": "Cross-platform visual differences detected"
}
}
},
"metadata": {
"type": "object",
"properties": {
"executionTimeMs": {
"type": "integer",
"minimum": 0,
"description": "Execution time in milliseconds"
},
"toolsUsed": {
"type": "array",
"items": {
"type": "string",
"enum": ["playwright", "browserstack", "saucelabs", "lambdatest", "percy", "cypress", "selenium"]
},
"description": "Testing tools used"
},
"agentId": {
"type": "string",
"pattern": "^qe-[a-z][a-z0-9-]*$",
"description": "Agent that executed the test"
},
"targetUrl": {
"type": "string",
"pattern": "^https?://",
"description": "URL tested"
},
"totalCombinations": {
"type": "integer",
"minimum": 1,
"description": "Total browser/device combinations tested"
}
}
},
"validation": {
"type": "object",
"properties": {
"schemaValid": { "type": "boolean" },
"contentValid": { "type": "boolean" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"warnings": { "type": "array", "items": { "type": "string" } },
"errors": { "type": "array", "items": { "type": "string" } }
}
},
"learning": {
"type": "object",
"properties": {
"patternsDetected": {
"type": "array",
"items": { "type": "string" },
"description": "Compatibility patterns detected"
},
"reward": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Learning reward signal"
}
}
}
},
"$defs": {
"compatibilityScore": {
"type": "object",
"required": ["value", "max"],
"properties": {
"value": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Compatibility score (0-100)"
},
"max": {
"type": "number",
"const": 100
},
"grade": {
"type": "string",
"pattern": "^[A-F][+-]?$",
"description": "Letter grade"
},
"userCoveragePercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Percentage of user base covered by passing platforms"
},
"trend": {
"type": "string",
"enum": ["improving", "stable", "declining", "unknown"]
}
}
},
"browserMatrix": {
"type": "object",
"required": ["browsers"],
"properties": {
"browsers": {
"type": "array",
"items": {
"$ref": "#/$defs/browserEntry"
},
"minItems": 1,
"description": "Browser test results"
},
"coveragePercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Browser market share covered"
}
}
},
"browserEntry": {
"type": "object",
"required": ["browser", "status"],
"properties": {
"browser": {
"type": "string",
"enum": ["chrome", "firefox", "safari", "edge", "opera", "mobile-chrome", "mobile-safari", "samsung-internet"],
"description": "Browser name"
},
"version": {
"type": "string",
"description": "Browser version (e.g., '118.0', 'latest')"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial", "skipped"],
"description": "Test status"
},
"issueCount": {
"type": "integer",
"minimum": 0,
"description": "Number of issues found"
},
"marketShare": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Browser market share percentage"
}
}
},
"deviceCoverage": {
"type": "object",
"properties": {
"tier1": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 1 devices (60% of users)"
},
"tier2": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 2 devices (30% of users)"
},
"tier3": {
"$ref": "#/$defs/tierCoverage",
"description": "Tier 3 devices (10% of users)"
},
"totalDevicesTested": {
"type": "integer",
"minimum": 0,
"description": "Total unique devices tested"
}
}
},
"tierCoverage": {
"type": "object",
"properties": {
"devices": {
"type": "array",
"items": { "type": "string" },
"description": "Device names in this tier"
},
"passRate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Pass rate percentage"
},
"tested": {
"type": "integer",
"minimum": 0
},
"passed": {
"type": "integer",
"minimum": 0
}
}
},
"platformResult": {
"type": "object",
"required": ["platform", "browser", "status"],
"properties": {
"platform": {
"type": "string",
"enum": ["windows", "macos", "linux", "ios", "android"],
"description": "Operating system"
},
"platformVersion": {
"type": "string",
"description": "OS version"
},
"browser": {
"type": "string",
"description": "Browser name"
},
"browserVersion": {
"type": "string",
"description": "Browser version"
},
"device": {
"type": "string",
"description": "Device name (for mobile)"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial", "skipped"]
},
"executionTimeMs": {
"type": "integer",
"minimum": 0
},
"screenshotPath": {
"type": "string",
"description": "Path to screenshot"
},
"issues": {
"type": "array",
"items": { "type": "string" },
"description": "Issue IDs found on this platform"
}
}
},
"compatibilityFinding": {
"type": "object",
"required": ["id", "title", "severity", "category"],
"properties": {
"id": {
"type": "string",
"pattern": "^COMPAT-\\d{3,6}$",
"description": "Unique finding identifier"
},
"title": {
"type": "string",
"minLength": 5,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 2000
},
"severity": {
"type": "string",
"enum": ["critical", "high", "medium", "low", "info"]
},
"category": {
"type": "string",
"enum": ["layout", "rendering", "functionality", "performance", "visual", "javascript", "css", "responsive"],
"description": "Issue category"
},
"affectedPlatforms": {
"type": "array",
"items": { "type": "string" },
"description": "Platforms where issue occurs"
},
"workingPlatforms": {
"type": "array",
"items": { "type": "string" },
"description": "Platforms where it works correctly"
},
"remediation": {
"type": "string",
"description": "How to fix this issue"
},
"evidence": {
"type": "string",
"description": "Screenshot path or code snippet"
},
"userImpactPercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Percentage of users affected"
}
}
},
"recommendation": {
"type": "object",
"required": ["id", "title", "priority"],
"properties": {
"id": {
"type": "string",
"pattern": "^REC-\\d{3,6}$"
},
"title": {
"type": "string",
"minLength": 5,
"maxLength": 200
},
"description": {
"type": "string",
"maxLength": 2000
},
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"]
},
"effort": {
"type": "string",
"enum": ["trivial", "low", "medium", "high", "major"]
},
"relatedFindings": {
"type": "array",
"items": { "type": "string" }
}
}
},
"compatibilityMetrics": {
"type": "object",
"properties": {
"totalCombinations": {
"type": "integer",
"minimum": 0,
"description": "Total platform combinations tested"
},
"passed": {
"type": "integer",
"minimum": 0
},
"failed": {
"type": "integer",
"minimum": 0
},
"skipped": {
"type": "integer",
"minimum": 0
},
"passRate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Overall pass rate percentage"
},
"userCoverage": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "User base coverage percentage"
},
"browsersCovered": {
"type": "integer",
"minimum": 0
},
"devicesCovered": {
"type": "integer",
"minimum": 0
},
"criticalIssues": {
"type": "integer",
"minimum": 0
},
"duration": {
"type": "integer",
"minimum": 0,
"description": "Total test duration in milliseconds"
}
}
},
"breakpointResult": {
"type": "object",
"required": ["breakpoint", "width", "status"],
"properties": {
"breakpoint": {
"type": "string",
"enum": ["mobile", "tablet", "desktop", "large-desktop"],
"description": "Breakpoint category"
},
"width": {
"type": "integer",
"minimum": 1,
"description": "Screen width in pixels"
},
"status": {
"type": "string",
"enum": ["passed", "failed", "partial"]
},
"issues": {
"type": "array",
"items": { "type": "string" }
}
}
},
"visualDifference": {
"type": "object",
"required": ["platform1", "platform2", "diffPercent"],
"properties": {
"platform1": {
"type": "string",
"description": "First platform"
},
"platform2": {
"type": "string",
"description": "Second platform"
},
"diffPercent": {
"type": "number",
"minimum": 0,
"maximum": 100,
"description": "Visual difference percentage"
},
"diffImagePath": {
"type": "string",
"description": "Path to diff image"
},
"significant": {
"type": "boolean",
"description": "Whether difference is significant"
}
}
},
"artifact": {
"type": "object",
"required": ["type", "path"],
"properties": {
"type": {
"type": "string",
"enum": ["report", "screenshot", "diff", "video", "log", "data"]
},
"path": {
"type": "string",
"maxLength": 500
},
"format": {
"type": "string",
"enum": ["json", "html", "md", "png", "jpg", "webm", "mp4", "txt", "csv"]
},
"description": {
"type": "string",
"maxLength": 500
}
}
}
}
}
@@ -0,0 +1,44 @@
{
"skillName": "compatibility-testing",
"skillVersion": "1.0.0",
"requiredTools": [
"jq"
],
"optionalTools": [
"playwright",
"browserstack",
"ajv",
"jsonschema",
"python3"
],
"schemaPath": "schemas/output.json",
"requiredFields": [
"skillName",
"status",
"output",
"output.browserMatrix",
"output.deviceCoverage",
"output.metrics"
],
"requiredNonEmptyFields": [
"output.summary",
"output.browserMatrix"
],
"mustContainTerms": [
"browser",
"compatibility"
],
"mustNotContainTerms": [
"TODO",
"placeholder",
"FIXME"
],
"enumValidations": {
".status": [
"success",
"partial",
"failed",
"skipped"
]
}
}
@@ -0,0 +1,99 @@
---
name: skill-authoring
license: Apache-2.0
description: Author, audit, and improve Grafana SKILL.md files against Anthropic's published Agent Skills guidance and the four-dimension rubric the grafana/skills CI gate uses (conciseness, actionability, workflow clarity, progressive disclosure). Applies the canonical SKILL.md structure (YAML frontmatter + body + references/ + scripts/ + assets/), the "pushy description" trigger pattern, the three-level progressive-disclosure model, and the validate-fix-rerun feedback loop. Use when creating a new skill in this repo, when reviewing a skill PR, when a skill's Tessl review score is below 75 (the merge gate), when a skill's description isn't getting picked up by agents, when restructuring a long SKILL.md into a bundle, or when the user asks how to write, improve, optimize, audit, or fix a skill - even if they don't say "skill" explicitly (e.g. "this isn't triggering", "Tessl scored this 72", "split this doc").
---
# Authoring & Improving Grafana Skills
How to write, review, and improve SKILL.md files so they pass the repo's CI gate and score well against the Anthropic-aligned rubric Tessl uses.
## Critical rules (always)
1. **Description is the primary trigger** — third-person, ≤1024 chars, must include explicit "Use when..." phrasing AND list concrete trigger terms users naturally say. See [references/descriptions.md](references/descriptions.md) for the pushy-description pattern that combats undertriggering.
2. **Body under 500 lines** — split into `references/*.md` if approaching the limit. SKILL.md is the routing layer, not the entire knowledge base.
3. **One level of nesting for references** — link from SKILL.md directly, never `SKILL.md → a.md → b.md`. Claude may use `head -100` previews on nested chains and miss content.
4. **Imperative voice** — "Run X" not "You should run X" not "It is important to run X". Explain *why* over heavy-handed `MUST` markers.
5. **Concrete examples beat prose** — copy-paste-ready commands, real config snippets. Tessl's `actionability` dimension scores this directly.
6. **No reserved words in `name`** — `anthropic` and `claude` are forbidden in skill names.
7. **No time-sensitive language in the body** — "after August 2025…" rots. Use an `<details>` "Old patterns" section for legacy info instead.
8. **Validate before committing** — `./scripts/lint-skills.sh skills/<plugin>/<your-skill>` clean + Tessl score ≥75 (run `tessl skill review --json <dir>`).
## The rubric
CI fails any PR where a touched SKILL.md scores below **75** on four 0-3 dimensions: **conciseness**, **actionability**, **workflow clarity**, **progressive disclosure**. Full per-dimension scoring + Anthropic-doc mapping in [references/rubric.md](references/rubric.md).
## Score variance
The judge is an LLM and swings 7-10 points run-to-run. Local 94 commonly lands at CI 85. **Ship only on three consecutive local 100s.**
## Decision tree for a new skill
1. **What product / domain does this skill belong to?**
Pick the right plugin folder: `grafana-core/`, `grafana-cloud/`, `grafana-lgtm/`, `grafana-app-sdk/`, `grafana-k6/`, `grafana-plugins/`. If none fits cleanly, ask the user before creating a new plugin group (a new group requires updating three `marketplace.json` files).
2. **Estimate body length.**
- <200 lines of substance → single `SKILL.md`, no bundle
- 200-500 lines → `SKILL.md` + `references/<topic>.md` for the long-form material
- >500 lines → mandatory bundle split; see [references/anatomy.md § Splitting strategies](references/anatomy.md#splitting-strategies)
3. **Write a "pushy" description first.**
The description is the only thing always loaded into context. If agents don't trigger the skill, nothing else matters. See [references/descriptions.md](references/descriptions.md) for the pattern.
4. **Draft body with the four-dimension rubric in mind.**
- Cut every sentence Claude already knows (Conciseness)
- Replace prose explanations with code blocks (Actionability)
- Number every multi-step procedure + add a validation step at the end (Workflow clarity)
- If you reach for `<details>`, consider whether that content belongs in `references/` instead (Progressive disclosure)
5. **Register in marketplace manifests.**
Add the skill path to the `skills` array in all three:
- `.claude-plugin/marketplace.json`
- `.cursor-plugin/marketplace.json`
- `.agents-plugin/marketplace.json`
6. **Validate locally.**
```bash
# 1. Lint clean (0 errors)
./scripts/lint-skills.sh skills/<plugin>/<your-skill>
# 2. Tessl reviewScore ≥75 (the CI gate)
tessl skill review --json skills/<plugin>/<your-skill> | jq '.review.reviewScore'
# 3. If below 75 or you want ≥85: run --optimize (requires auth)
tessl skill review --optimize --yes --max-iterations 3 skills/<plugin>/<your-skill>
```
If the run fails: read the lint error / Tessl suggestion, fix, re-run. Don't open the PR until both checks pass cleanly. The feedback-loop pattern beats one-shot writing.
## Fixing a low-scoring existing skill
1. Read the judge's verbatim Suggestions text (non-JSON output):
```bash
tessl skill review skills/<plugin>/<name>
```
The `Suggestions:` block under each dimension names the exact sentences/sections to cut. **Copy the suggestion** — don't guess. Then verify the lowest dimension matches your read.
2. Apply the fix pattern from [references/rubric.md](references/rubric.md):
- **Conciseness 1-2** → cut intros, definitions, multi-line tables that mostly point to refs
- **Actionability 1-2** → replace prose with code blocks and CLI commands
- **Workflow clarity 1-2** → add numbered steps + validation checkpoints
- **Progressive disclosure 1-2** → split into `references/*.md`
3. If the skill is **intentionally a routing document** (like `grafana-k6/k6-docs`), don't let `--optimize` inline the bundle back into SKILL.md. Hand-craft a minimal copy-paste "validation loop" inline so SKILL.md is independently actionable, while preserving the bundle.
4. Re-score five times locally. **Don't stop until all five runs hit 100** — see "Score variance" above for why.
## Anti-patterns
See [references/anti-patterns.md](references/anti-patterns.md).
## References
- [`references/descriptions.md`](references/descriptions.md) — the pushy-description pattern + trigger-term checklist
- [`references/rubric.md`](references/rubric.md) — per-dimension scoring with Anthropic-doc citations and concrete fix patterns
- [`references/anatomy.md`](references/anatomy.md) — three-level progressive disclosure, bundle layout, splitting strategies
- [`references/anti-patterns.md`](references/anti-patterns.md) — what NOT to do, with examples
- [Anthropic — Agent Skills best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)
- [anthropics/skills — skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md)
- [The Complete Guide to Building Skills for Claude (PDF)](https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf)
@@ -0,0 +1,139 @@
# Anatomy of a skill bundle
## The canonical layout
```
skills/<plugin>/<skill-name>/
├── SKILL.md (required: YAML frontmatter + body)
├── references/ (markdown loaded only when needed)
│ ├── topic-a.md
│ └── topic-b.md
├── scripts/ (executable code; not loaded into context)
│ └── helper.py
└── assets/ (templates, fonts, images used as output)
└── template.json
```
Source: [anthropics/skills — skill-creator SKILL.md § Anatomy of a Skill](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md).
## The three-level loading model
| Level | What's loaded | When | Token cost |
|---|---|---|---|
| 1. Metadata | `name` + `description` | Always (at agent startup) | ~50-200 tokens per skill |
| 2. SKILL.md body | Full markdown body | When the agent decides the skill triggers | ~500-3000 tokens once loaded |
| 3. Bundle | `references/*.md`, `assets/*`, `scripts/*` (via bash, not context) | On demand, only the specific file the body links to | Zero until accessed |
Implication: **the cheapest way to lift Conciseness is to move heavy content from level 2 to level 3.**
## SKILL.md size guidance
- **Aim:** ≤200 lines for the body
- **Hard guideline:** ≤500 lines (Anthropic's documented limit)
- **Soft threshold:** If you're past 250 lines, ask whether some sections should be `references/*.md`
## Splitting strategies
### By domain (when a skill spans multiple platforms/frameworks)
```
cloud-integrations/
├── SKILL.md (overview + selection logic: "if AWS, see references/aws.md…")
└── references/
├── aws.md
├── gcp.md
├── azure.md
└── confluent.md
```
Claude reads SKILL.md, sees the user mentioned AWS, reads only `references/aws.md`. The other three never load → zero token cost.
### By depth (when overview vs. detail is clear)
```
react-19-plugin-migration/
├── SKILL.md (decision tree + critical rules + happy path)
└── references/
├── eslint-9-migration.md (deep dive for the ESLint sub-step)
└── troubleshooting.md (failure modes + fixes)
```
### By format (when reference material doesn't fit the prose flow)
```
admin/
├── SKILL.md
└── references/
├── sso.md (full OAuth/SAML/GitHub OAuth configs)
├── terraform.md (Terraform provider examples)
└── api-reference.md (Cloud API + Admin endpoints + audit logs)
```
## Reference-file conventions
- **Filename**: short, descriptive, lowercase-with-hyphens (`troubleshooting.md`, not `Troubleshooting.md` or `Docs.md`)
- **Top of file**: if >100 lines, include a `## Contents` table of contents so Claude can navigate during a partial read
- **Linked one level deep from SKILL.md** — never a chain (`SKILL.md → a.md → b.md`)
- **Inline relative links** — `[full setup guide](references/sso.md)` from SKILL.md, not absolute URLs
## When NOT to inline
Tessl's rubric penalizes routing documents (a thin SKILL.md that defers to references/) by scoring them down for "not independently actionable". The naive response is to inline content from references back into SKILL.md, which undoes the deliberate progressive disclosure.
**Better fix for routing skills:** add a minimal copy-paste-ready "happy path" loop inline so SKILL.md is independently actionable for the most common task, while preserving the bundle for everything else.
Concrete example — `grafana-k6/k6-docs` (33 lines in its SKILL.md):
````markdown
## Workflow
Pick the workflow that matches the user's intent:
- **Write documentation:** follow [references/workflows/write.md](references/workflows/write.md)
- **Review documentation:** follow [references/workflows/review.md](references/workflows/review.md)
## Validating examples (always run before committing)
Every code example in k6 documentation and release notes must execute cleanly against `k6@master`:
```bash
cd ~/path/to/k6
git checkout master
git pull
go run . run /path/to/script.js
```
If the run fails: read the error, fix the source, re-run. Don't commit a doc edit whose
example doesn't run clean. For the full multi-example workflow with parallel subagents,
see [references/testing-workflow.md](references/testing-workflow.md).
````
This pattern (the file lifted its score from 72 → 100):
1. Keeps the routing layer + bundle architecture
2. Adds enough inline content that Claude can do the common task without loading any bundle file
3. Still defers full procedural detail to the references/
## scripts/ directory
Use when the skill includes deterministic operations Claude shouldn't reinvent:
- File transformations (PDF extraction, image resizing)
- Validation harnesses
- Output formatters
Scripts are executed via bash; **their contents do not consume context tokens**, only their output does. Big performance win for skills that run the same code repeatedly.
Make scripts executable (`chmod +x`) — the linter warns otherwise.
## assets/ directory
Use for template files, fonts, images, or other static resources the skill produces as output. Like scripts, assets don't consume tokens until referenced.
## Marketplace registration
After creating the skill directory, add it to the `skills` array in all three marketplace files:
- `.claude-plugin/marketplace.json`
- `.cursor-plugin/marketplace.json`
- `.agents-plugin/marketplace.json`
The three must stay in sync. CI's `validate` job will fail any PR where they diverge.
@@ -0,0 +1,243 @@
# Anti-patterns: what NOT to do in a skill
Distilled from [Anthropic's Agent Skills best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices), [skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md), and the Tessl rubric this repo's CI gate uses.
## Description anti-patterns
### First-person / second-person voice
```yaml
# Wrong
description: I can help you process Excel files and generate reports.
description: You can use this to process Excel files.
# Right (third-person)
description: Processes Excel files and generates reports. Use when analyzing spreadsheets, building pivot tables, or generating charts from .xlsx data.
```
### Vague "what"
```yaml
# Wrong
description: Helps with metrics queries.
# Right (specific actions + concrete outputs)
description: Write PromQL queries for Prometheus and Grafana Mimir. Calculates rates, builds histogram quantiles, debugs cardinality, and optimizes slow queries. Use when writing metric queries, building Grafana panels, working with recording rules, or troubleshooting Prometheus performance.
```
### Missing "Use when" trigger
```yaml
# Wrong (no triggers — agent can't tell when to load it)
description: PromQL query writing for Prometheus.
# Right (explicit triggers)
description: PromQL query writing for Prometheus and Grafana Mimir. Use when writing metric queries, building Grafana panels, calculating rates, working with histograms, or optimizing slow queries.
```
### No trigger variations / no pushy clause
```yaml
# Wrong (only the canonical term — agent skips this when user says synonyms)
description: Use when writing k6 documentation.
# Right (variations + pushy clause)
description: ... Use when working on k6 documentation, the k6 changelog, the k6 release notes, k6 API reference, k6 TypeScript types, load testing docs - even if the user doesn't explicitly say "documentation".
```
### Reserved words in `name`
```yaml
# Wrong
name: anthropic-helper
name: claude-tools
# Right
name: skill-authoring
name: prompt-helper
```
## Body anti-patterns
### Time-sensitive language
```markdown
# Wrong (rots — "after August 2025" is meaningless in 2027)
If you're doing this before August 2025, use the old API.
After August 2025, use the new API.
# Right (`<details>` for legacy info, current path inline)
## Current method
Use the v2 API endpoint: `api.example.com/v2/messages`
<details>
<summary>Legacy v1 API (deprecated 2025-08)</summary>
The v1 API used: `api.example.com/v1/messages`. No longer supported.
</details>
```
### Windows-style paths
```markdown
# Wrong (breaks on Unix)
Run `scripts\helper.py` to process the input.
# Right (forward slashes everywhere)
Run `scripts/helper.py` to process the input.
```
### Inconsistent terminology
Pick one term per concept and stick with it. Mixing breaks Claude's pattern-matching:
```markdown
# Wrong (same concept, three names)
The API endpoint is /messages. Send a POST to the URL. The API route accepts JSON.
# Right (one term)
The API endpoint is /messages. Send a POST to the endpoint. The endpoint accepts JSON.
```
### Heavy-handed `MUST` markers without justification
```markdown
# Wrong (no explanation, feels like noise)
You MUST validate the input. You MUST handle errors. You MUST log the result.
# Right (explain why, reserve MUST for genuine hard constraints)
Validate the input before submitting — the API returns a 422 for malformed payloads
without telling you which field failed, which is hard to debug after the fact.
Always handle the 429 response with exponential backoff; the rate limiter resets
on a sliding window, not a fixed window.
```
### "There are many ways to do this" hedging
```markdown
# Wrong (forces Claude to pick — wastes time on irrelevant alternatives)
You can use pypdf, or pdfplumber, or PyMuPDF, or pdf2image. Each has trade-offs...
# Right (pick a default, mention the escape hatch)
Use pdfplumber for text extraction. For scanned PDFs requiring OCR, use pdf2image
with pytesseract instead.
```
### Dangling references
```markdown
# Wrong (file doesn't exist)
For full setup, see [references/setup-guide.md](references/setup-guide.md)
```
The linter doesn't catch this. Reviewers should. Either create the file or remove the link.
### Too-deep reference chains
```markdown
# Wrong: SKILL.md → advanced.md → details.md
# (Claude may preview with `head -100` and miss content past level 2)
# Right: keep all references one level deep from SKILL.md
SKILL.md
├── references/advanced.md
└── references/details.md
```
## Workflow anti-patterns
### Implicit ordering
```markdown
# Wrong (ordering is in the prose; easy to miss)
Configure X. Make sure to do Y before X actually. Don't forget Z either.
# Right (explicit numbered steps)
1. Do Y first
2. Then configure X
3. Then run Z to verify
```
### No validation checkpoints
```markdown
# Wrong (no way to know if step 1 worked before doing step 2)
1. Create the contact point
2. Add the notification policy
3. Write the alert rule
# Right (validate after each step)
1. Create the contact point
- **Verify**: `curl .../api/v1/provisioning/contact-points` returns it
2. Add the notification policy
- **Verify**: send a test alert; output should show the contact point name
3. Write the alert rule
- **Verify**: alert fires correctly under simulated conditions
```
### "Punting to Claude" in scripts
```python
# Wrong (script lets Claude figure out failure)
def process_file(path):
return open(path).read() # fails on FileNotFoundError, no fallback
# Right (handle the error explicitly)
def process_file(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"File {path} not found, creating default")
with open(path, "w") as f:
f.write("")
return ""
```
### Voodoo constants
```python
# Wrong (no justification for the values)
TIMEOUT = 47
RETRIES = 5
# Right (self-documenting)
# HTTP requests typically complete within 30 seconds; longer timeout for slow networks
REQUEST_TIMEOUT = 30
# Three retries balances reliability vs speed; most flakes resolve by the second retry
MAX_RETRIES = 3
```
## Structural anti-patterns
### Skill body acts as a reference dump
If the skill has no workflow guidance and is just a list of API endpoints / config snippets / capabilities, the user has to do all the orchestration themselves. Tessl scores this 1-2/3 on workflow_clarity.
Fix: pick the 2-3 most common user tasks, write numbered workflows for them at the top, and move the reference-dump material to `references/<topic>.md`.
### One mega-SKILL.md when domains are clearly separable
If the skill covers AWS + GCP + Azure in one file, Claude loads all three even when the user only needs one. Split per domain:
```
cloud-deploy/
├── SKILL.md (overview + selection)
└── references/
├── aws.md
├── gcp.md
└── azure.md
```
### Skipping marketplace registration
The skill is functionally created on disk but not installable from any marketplace. CI's `validate` job catches inter-manifest divergence but doesn't flag a skill that's missing from all three.
After creating the skill directory:
1. Add the path to `.claude-plugin/marketplace.json` → relevant plugin's `skills` array
2. Same for `.cursor-plugin/marketplace.json`
3. Same for `.agents-plugin/marketplace.json`
All three must list the same skill path. Keep them in sync.
@@ -0,0 +1,86 @@
# Writing skill descriptions that actually trigger
The `description` field is the only part of a skill always loaded into the agent's context. Everything else — the SKILL.md body, the bundle files — is only read after the agent decides this skill applies. **If the description is weak, none of the rest of your work matters.**
## The four scoring dimensions (Tessl `descriptionJudge`)
Each rated 0-3, normalized to a 0-1 final score:
| Dimension | What it checks |
|---|---|
| **Specificity** | Lists concrete actions the skill performs (not just "helps with X") |
| **Trigger term quality** | Includes natural terms users actually say (variations, synonyms) |
| **Completeness** | Has both *what it does* AND *when to use* — usually via "Use when..." |
| **Distinctiveness / conflict risk** | Clearly scoped; won't fight other skills for activation |
## The pushy pattern (Anthropic-recommended)
From [Anthropic's skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md):
> Claude has a tendency to "undertrigger" skills — to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy". So for instance, instead of "How to build a simple fast dashboard to display internal Anthropic data.", you might write "How to build a simple fast dashboard to display internal Anthropic data. **Make sure to use this skill whenever the user mentions dashboards, data visualization, internal metrics, or wants to display any kind of company data, even if they don't explicitly ask for a 'dashboard.'**"
The pushy clause forces the agent to consider the skill across phrasings users actually say.
## Anatomy of a strong description
```yaml
description: <concrete-actions-the-skill-performs>. Applies <specific-conventions/methodologies>, generates <specific-outputs>, and validates <specific-checks>. Use when <task-A>, <task-B>, <task-C>, or when the user asks to <verb-1>, <verb-2>, <verb-3> - even if they don't explicitly say "<the-narrow-canonical-term>".
```
Three parts:
1. **What** — concrete actions, not vague "helps with". List specific outputs the skill produces.
2. **Use when** — explicit triggers. Multiple scenarios, ideally including domain-specific terms (repo names, CLI names, product names).
3. **Pushy clause** — the "even if they don't explicitly say…" part. This catches the user phrasings that miss the canonical term.
## Strong vs weak examples
### Weak
```yaml
# Vague "what", no "when", no trigger variations
description: Helps with k6 documentation.
# First-person, no "when"
description: I help you write k6 docs and TypeScript types.
# "When" is too narrow; missing trigger variations and pushy clause
description: Use when writing or reviewing k6 documentation across TypeScript types, user docs, and release notes.
```
### Strong (k6-docs from this repo, score 1.0/1.0 after rewrite)
```yaml
description: >
Write or review k6 documentation across the three k6 repositories - k6-DefinitelyTyped (TypeScript types),
k6-docs (user documentation), and k6 (release notes / changelog). Applies k6 doc style conventions,
generates TypeScript type definitions, drafts release notes, and validates examples by running them
against k6@master. Use when working on k6 documentation, the k6 changelog, the k6 release notes,
k6 API reference, k6 TypeScript types, load testing docs, or when the user asks to write, edit, or
review anything in the k6, k6-docs, or k6-DefinitelyTyped repos - even if they don't explicitly
say "documentation".
```
Why it works:
- Specific outputs: TypeScript type definitions, release notes, example validation
- Trigger variations: changelog, release notes, API reference, repo names, "load testing docs"
- Pushy clause: catches users editing the underlying repos without saying "documentation"
## Hard limits
| Limit | Value | Source |
|---|---|---|
| Max length | 1024 chars | Agent Skills spec |
| Voice | Third-person | Anthropic best-practices doc |
| XML tags | Not allowed | Agent Skills spec |
| Reserved words in `name` (NOT description) | `anthropic`, `claude` | Anthropic best-practices doc |
## Checklist before opening a PR
- [ ] Third-person ("Processes X" not "I process X" or "You can process X")
- [ ] Explicit "Use when..." clause
- [ ] Pushy clause at the end ("even if they don't say…")
- [ ] At least 4-5 trigger-term variations users naturally say
- [ ] ≤1024 chars
- [ ] No reserved words in `name`
- [ ] Concrete actions, not "helps with"
@@ -0,0 +1,242 @@
# The four-dimension rubric: per-dimension scoring + fix patterns
Tessl's `contentJudge` (the body-scoring half of the review) uses these four dimensions, each 0-3, weighted into the overall `reviewScore` shown in CI. The rubric is aligned with Anthropic's published best-practices doc.
## Contents
- [Conciseness](#conciseness)
- [Actionability](#actionability)
- [Workflow clarity](#workflow-clarity)
- [Progressive disclosure](#progressive-disclosure)
- [Combined heuristic: how to read a low score](#combined-heuristic)
## Conciseness
**What it scores:** Does every sentence justify its token cost? Or does the skill spend tokens explaining things Claude already knows?
**Anthropic's principle** ([best-practices doc § *Concise is key*](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices)):
> Default assumption: Claude is already very smart. Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" "Can I assume Claude knows this?" "Does this paragraph justify its token cost?"
**Common 2/3 patterns to cut:**
- Intro paragraphs explaining what the technology is ("PromQL is a functional query language…", "Loki is a log aggregation system…") — Claude knows
- "First, install X, then run Y, then verify Z works" prose where each step could be a one-line code block
- Repeated cautions ("never drop a distinguishing label" said three times)
- "Why this matters" paragraphs longer than the actual rule
- Verbose `<details><summary>What is X?</summary>` blocks
**Fix pattern:**
```diff
- ## Querying metrics
-
- PromQL is a functional query language that lets you query time-series data. Each query
- consists of selectors (which series), operators (what to compute), and functions (how to
- aggregate). For example, to get the request rate for an API endpoint, you'd write:
-
- ```promql
- rate(http_requests_total{job="api"}[5m])
- ```
+ ## Querying metrics
+
+ ```promql
+ rate(http_requests_total{job="api"}[5m])
+ ```
+ Standard PromQL — selector, function, range vector.
```
The code is the explanation. The prose around it was tax.
### Trim multi-example lists to 1-2 inline + defer the rest
When a section enumerates N variants of the same shape (receiver types / cloud providers / language SDKs / panel types / etc.), the judge consistently scores conciseness 2/3 if all N are inline. Reliable lift to 3/3:
- Keep the **1-2 most-common** examples inline, in full
- Move the rest to `references/<topic>.md` under a "Other X types" section, often with brief stub code + a list of remaining types
- Link from the inline section: *"For email, webhook, Teams, … see [references/X.md § Other types](references/x.md#other-x-types)"*
Concrete: in this repo, `alerting-irm` inlined four contact-point receiver types (PagerDuty, Slack, email, webhook). Conciseness stuck at 2/3. Trimming to PagerDuty + Slack inline and moving email + webhook to `references/alerting.md § Contact point receiver types` (with a list of the other 10+ supported types) lifted the score from 94 → 100 across 3 consecutive local runs.
### Other 2/3 traps observed across the catalog
The 85→100 sweep on `cue-kind-definition`, `private-connectivity`, and `skill-authoring` surfaced these recurring conciseness traps. The judge's verbatim suggestion text is in italics — quoting it because the wording is consistent enough to grep for in future reviews.
- **"When to use this skill" bulleted lists** — overlap with the description's "Use when…" clause. *"the 'when to use this skill' list overlaps heavily with what should be in the description"*. Fix: delete the section entirely; the description is the trigger.
- **Multi-row tables that delegate to refs** — each cell is a one-line summary + ref link. *"the rubric table could be leaner since it mostly points to references"*. Fix: replace with a one-sentence summary + single ref link.
- **Cost / savings / business-value tangents** — dollar amounts, ROI calculations, "this saves N% on…". *"includes some unnecessary content like the cost savings section with specific dollar calculations"*. Fix: move to `references/<topic>.md § Cost`.
- **Bare decision-table rows** — single-line entries with no actionable content ("On-premises with no cloud provider — use Agent over internet"). *"the 'On-premises with no cloud provider' row"*. Fix: drop the row.
- **Multi-paragraph "Why X" explanations** — explaining the rationale at length. *"the 'Score variance' section spends many lines explaining LLM judge variance which could be condensed to 2-3 lines"*. Fix: collapse to one sentence stating the rule + one stating the reason.
- **Anti-patterns lists that duplicate critical rules** — *"the anti-patterns section partially duplicates the critical rules"*. Fix: either fold uniques into the rules, or replace the section with a one-line pointer to `references/anti-patterns.md`.
- **Intro paragraphs of the form "X drives the entire Y pipeline; each X describes…"** — orientation prose Claude doesn't need. *"the opening paragraph explaining what kinds drive and what gets generated from them is context Claude can infer"*. Fix: delete and start with the first workflow.
### Read the judge's suggestions, don't guess
`tessl skill review` (without `--json`) prints an `Assessment:` paragraph and `Suggestions:` bullets per dimension. **The Suggestions block names the exact sentences/sections to cut.** Copy them directly. Across this repo's 85→100 fixes, *every* lift came from applying the verbatim suggestion — guessing what to trim consistently took longer and lifted less.
## Actionability
**What it scores:** Could Claude, reading this once, do the task copy-paste? Or does Claude have to fill gaps from its own knowledge?
**Strong patterns:**
- Copy-paste-ready CLI commands with concrete arguments (not placeholders like `<your-token>` everywhere)
- Complete config snippets, not "configure X like Y"
- Templates with explicit `[Title]` / `[Section]` placeholders the user can fill
- Explicit `Input:` / `Output:` pairs for output-shape teaching
**Weak patterns (the 2/3 trap):**
- "Use the appropriate library to do X" without naming the library
- Placeholders that hide ambiguity: "Set the value to <appropriate>"
- "There are many ways to do this, here are some…" — pick one, ship it
- Discussing trade-offs before showing the solution
**Fix pattern:**
```diff
- ### Setup
-
- You'll need to configure Alloy with the appropriate scrape targets. Most users will want
- to start with a basic configuration and extend it.
+ ### Setup
+
+ ```alloy
+ prometheus.scrape "node_exporter" {
+ targets = [{"__address__" = "localhost:9100"}]
+ forward_to = [prometheus.remote_write.default.receiver]
+ }
+
+ prometheus.remote_write "default" {
+ endpoint {
+ url = sys.env("PROMETHEUS_REMOTE_WRITE_URL")
+ }
+ }
+ ```
```
## Workflow clarity
**What it scores:** Multi-step procedures with explicit ordering, decision points, and validation checkpoints.
**Strong patterns:**
- Numbered steps where order matters
- Explicit `if validation fails, do X; else continue` branches
- A copy-able checklist Claude can paste into its response
- Validate → fix → re-run feedback loops written out
**Weak patterns:**
- Bullet list of capabilities with no ordering
- "Configure X, then Y, then Z" without per-step validation
- No "what to do if it fails" guidance
- Implicit ordering ("Make sure to do A. By the way, you also need B before A")
**Fix pattern:**
```diff
- ## Setting up alerts
-
- Create contact points, add notification policies, write the alert rules. Make sure to
- test routing before going live.
+ ## Setting up alerts
+
+ 1. **Create contact points** (where notifications go):
+ ```bash
+ curl -X POST .../api/v1/provisioning/contact-points -d @contact-points.json
+ # Verify:
+ curl .../api/v1/provisioning/contact-points | jq '.[].name'
+ ```
+
+ 2. **Add notification policies** (which alerts go where):
+ ```bash
+ curl -X POST .../api/v1/provisioning/policies -d @policies.json
+ ```
+
+ 3. **Write alert rules**:
+ ```bash
+ curl -X POST .../api/v1/provisioning/alert-rules -d @rules.json
+ ```
+
+ 4. **Verify routing** before going live:
+ ```bash
+ curl -X POST .../api/v1/provisioning/policies/test -d @test-alert.json
+ # If output doesn't show the expected contact point, re-check matchers in step 2.
+ ```
```
The "verify" + "what to do if it fails" steps are what lift a workflow_clarity score from 2 to 3.
## Progressive disclosure
**What it scores:** Are you using the three-level loading model correctly? Or is everything dumped into one 600-line SKILL.md?
**The three levels** ([skill-creator SKILL.md](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md)):
1. **Metadata** (name + description) — always loaded, ~100 words
2. **SKILL.md body** — loaded when the skill triggers, <500 lines
3. **Bundled resources** (scripts/, references/, assets/) — loaded only when needed
**Strong patterns:**
- SKILL.md under 200 lines for the routing layer
- `references/*.md` for detailed material, linked one level deep
- Domain-organized refs: `references/aws.md`, `references/gcp.md`, `references/azure.md` (Claude reads only the relevant one)
- Reference files >100 lines have a table of contents at the top
**Weak patterns:**
- One 800-line SKILL.md with sections that should be separate files
- Nested references: `SKILL.md → a.md → b.md` (Claude may truncate at level 2)
- Dangling references — `[see X](references/x.md)` where the file doesn't exist
- All content inline even when there are clear domain boundaries
**Fix pattern (split):**
Before:
```
skills/grafana-cloud/admin/
└── SKILL.md (244 lines, includes SSO config, Terraform examples, full API endpoint reference)
```
After:
```
skills/grafana-cloud/admin/
├── SKILL.md (under 100 lines: overview, workflows, RBAC, service accounts)
└── references/
├── sso.md (OAuth + SAML + GitHub OAuth configs)
├── terraform.md (Terraform examples)
└── api-reference.md (Cloud API + Admin endpoints + audit logs)
```
Then SKILL.md links to each `references/*.md` one level deep.
### When NOT to split
Some skills are intentionally short routing documents (e.g. `grafana-k6/k6-docs` — 33 lines of overview pointing at a deep `references/workflows/*.md` bundle). The four-dimension rubric penalizes these for "not independently actionable", BUT the architecture is correct.
For routing documents: add a minimal copy-paste-ready "validation loop" inline so SKILL.md is independently actionable for the most common task, while preserving the bundle for everything else. See `skills/grafana-k6/k6-docs/SKILL.md` for the pattern that lifted its score from 72 → 100 without losing the bundle.
### The "bundle files weren't provided" 2/3 ceiling
`tessl skill review` (and CI) only reads SKILL.md — not files under `references/`. The LLM judge sees the link in SKILL.md but cannot verify the target exists, so it caps progressive_disclosure at 2/3 with the line *"no bundle files were provided, so the references are dangling"*. Two responses:
- **Login locally**: `tessl login`, then `tessl skill review` includes bundle files in the judgment (output note: *"Only SKILL.md was reviewed. Log in with tessl login to include all supporting files in the review."*). CI doesn't login, so this only helps local-only sanity checks.
- **Don't write your way into the trap**: a skill that *itself* names "dangling references" as a danger triggers the judge's caution explicitly. If your SKILL.md uses references heavily, keep the dangling-references warning in `references/anti-patterns.md` rather than in the body.
## Combined heuristic
If you see a 2/3 across all four dimensions, the skill is doing too much narrative and not enough structure. If just one dimension is low:
| Low dimension | First fix to try |
|---|---|
| Conciseness | Cut the introduction. Cut explanations of what known technologies are. |
| Actionability | Replace one prose paragraph per section with a code block. |
| Workflow clarity | Add numbered steps + one validation step per workflow. |
| Progressive disclosure | Split sections >50 lines into `references/*.md`. |
Then re-run `tessl skill review --json` and iterate. Score lift per fix is typically 5-15 points.
@@ -0,0 +1,70 @@
name: Validate skills
on:
push:
branches: [main]
paths:
- "skills/**"
- ".github/workflows/validate-skills.yml"
pull_request:
paths:
- "skills/**"
- ".github/workflows/validate-skills.yml"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Detect changed skills
id: changed
run: |
# If on PR, compare against base; otherwise compare against previous commit.
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
else
BASE="HEAD~1"
fi
# Detect all changed skill folders (new + modified).
CHANGED=$(git diff --name-only "$BASE"...HEAD 2>/dev/null | \
grep -oE '^skills/[^/]+' | sort -u || true)
# If no diff (e.g., first push), validate everything.
if [ -z "$CHANGED" ]; then
CHANGED=$(find skills -mindepth 1 -maxdepth 1 -type d ! -name '_template' -printf 'skills/%f\n')
fi
# Always exclude the underscore-prefixed template.
CHANGED=$(echo "$CHANGED" | grep -v '_template' || true)
echo "skills_list<<EOF" >> "$GITHUB_OUTPUT"
echo "$CHANGED" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Validate skill frontmatter and structure
env:
SKILLS: ${{ steps.changed.outputs.skills_list }}
run: |
echo "Validating skills:"
echo "$SKILLS"
echo "---"
while IFS= read -r skill; do
[ -z "$skill" ] && continue
echo "::group::Validating $skill"
if [ ! -f "$skill/SKILL.md" ]; then
echo "::error::Missing SKILL.md in $skill"
exit 1
fi
npx -y skills-ref validate "$skill"
echo "::endgroup::"
done <<< "$SKILLS"
- name: Check README skill list drift
run: |
# Read all skill names from skills/ and ensure each appears in README.md.
npx -y skills-ref readme --check
+36
View File
@@ -0,0 +1,36 @@
# Local reference clones (distillation sources, not committed)
MorpheApp/
# Android / Gradle
build/
.gradle/
*.iml
local.properties
captures/
.kotlin/
*.apk
*.aab
*.apkm
*.apks
*.xapk
*.dex
*.mpp
*.mpe
# IDE
.idea/
*.iws
*.ipr
.vscode/
.fleet/
# OS
.DS_Store
Thumbs.db
# Node (for skills-ref tooling)
node_modules/
npm-debug.log*
# Skill-packaging output
.skill/
+121
View File
@@ -0,0 +1,121 @@
# Contributing to Heretek Skills
Thanks for adding a skill! This guide covers the format, naming, and validation expectations.
## Folder layout
Every skill lives in its own folder under `skills/`:
```
skills/
├── <skill-name>/
│ ├── SKILL.md # Required. Entry point + metadata.
│ ├── references/ # Optional. Distilled docs loaded on demand.
│ ├── scripts/ # Optional. Helper shell/Python scripts.
│ └── assets/ # Optional. Templates, images, fixtures.
├── _template/ # Blank starter. Copy this to add a new skill.
├── .gitkeep
└── ...
```
Keep each skill **flat** under `skills/<skill-name>/`. Do not nest skill folders.
## How to add a new skill
1. **Copy the template:**
```bash
cp -r skills/_template skills/<your-skill-name>
```
2. **Edit `SKILL.md`** — fill in the YAML frontmatter and body sections.
3. **Validate locally:**
```bash
npx -y skills-ref validate skills/<your-skill-name>
```
4. **Open a PR.** The CI workflow (`.github/workflows/validate-skills.yml`) will run `skills-ref validate` and `skills-ref readme` and check for naming drift.
## Required frontmatter
Each `SKILL.md` must begin with a YAML block delimited by `---`:
```yaml
---
name: <skill-name> # Required. See "Naming rules" below.
description: <one-paragraph> # Required. See "Description rules" below.
---
```
### Naming rules
- `name` must **exactly match** the parent directory (e.g., folder `foo-bar/` → `name: foo-bar`).
- Lowercase letters, digits, and hyphens only.
- Max 64 characters.
- Cannot start or end with a hyphen; cannot contain `--`.
### Description rules
- Max 1024 characters.
- Must describe **what the skill does** AND **when to use it**.
- Include trigger phrases users will say. Examples: _"create a morphe patch"_, _"add a patch to my morphe bundle"_, _"remove ads from X"_, _"fingerprint and patch method Y"_.
- **All "when to use" info lives in the description**, not in the body. The body is loaded only after the skill is activated, so a thin description means the skill won't trigger.
- Be **pushy** — describe the symptoms the user might describe, not just the formal name. Common user phrasings like "I want to…", "how do I…", "add a…", "make X Y" should appear if natural.
## Body sections (recommended)
Body is freeform, but most skills follow this structure:
```markdown
## When to use
## Inputs required
## Workflow
## Reference index
## Common pitfalls
## Minimal worked example
```
Keep the body under **500 lines**. Anything longer should be split into `references/*.md` and linked with relative paths.
## Progressive disclosure
Files are loaded lazily:
1. **At startup** (~100 tokens): frontmatter (`name`, `description`) only.
2. **On activation** (≤ 5000 tokens): the full `SKILL.md` body.
3. **On demand** (unlimited): referenced files in `references/`, `scripts/`, `assets/`.
Use this to your advantage: keep `SKILL.md` focused on the workflow, and put deep-dives in `references/`.
## Scripts
- Place in `scripts/`. Name with `kebab-case.sh` or `kebab-case.py`.
- Start with `#!/usr/bin/env bash` (or `#!/usr/bin/env python3`) and `set -euo pipefail` for shell scripts.
- Wrap long-running operations with clear error messages.
- Document inputs and outputs in the script's top-of-file comment.
## Anti-patterns
- ❌ **No copy-pasted upstream docs.** Distill and rephrase content from third-party sources. The MIT license on this repo does not cover verbatim copies of GPLv3+ sources.
- ❌ **No reused Morphe branding in derivative bundle names or logos.** Section 7 of the upstream GPLv3 prohibits this — the skill itself is fine, but artifacts you produce must respect the upstream branding rules.
- ❌ **No hand-edited generated files.** If you reference `patches-list.json`, `patches-bundle.json`, or `CHANGELOG.md`, note that they are regenerated by Gradle; instruct the contributor to run the generator instead of editing them.
- ❌ **No underscore-prefixed skill names.** The `_template` is the only exception; it is intentionally skipped by registries.
- ❌ **No two skills with the same `description` semantics.** If you find a duplicate, fold it into the existing skill or split the existing skill first.
## Validation
The CI workflow runs `skills-ref validate` on every PR touching `skills/**`. Locally:
```bash
npx -y skills-ref validate skills/<skill-name>
```
This checks frontmatter constraints, name/directory match, and a few common mistakes.
## Releasing
The repo follows [Conventional Commits](https://www.conventionalcommits.org/) for clarity. Each merged PR that adds a skill should result in a README entry (auto-generated by `npx skills-ref readme`).
## Questions?
Open an issue or reach out to the maintainers.
+30
View File
@@ -0,0 +1,30 @@
MIT License
Copyright (c) 2026 Heretek-AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
NOTE ON PATCH ARTIFACTS: This MIT license applies to the skill code,
templates, and scripts in this repository. Artifacts produced by applying
these skills (e.g., `.mpp` patch bundles, `.mpe` DEX extensions) are
derivative works of the Morphe Patcher project and are subject to the
upstream GPLv3 license with Section 7 Additional Terms. See
https://github.com/MorpheApp/morphe-patcher/blob/main/LICENSE for details.
+44
View File
@@ -0,0 +1,44 @@
# Heretek Skills
Agent skills for creating [Morphe](https://morphe.software) patches. Each skill is a self-contained module that teaches an AI agent (Claude Code, Cursor, etc.) how to perform a specific Morphe task.
## Install
```bash
npx skills add Heretek-AI/heretek-skills
```
Or install a single skill:
```bash
npx skills add Heretek-AI/heretek-skills --skill create-morphe-patch
```
## Available skills
| Skill | Description |
|---|---|
| [`create-morphe-patch`](./skills/create-morphe-patch/) | Author a new Morphe patch: signature capture, fingerprinting, the `Compatibility` block, optional `.mpe` extensions, and Gradle build verification. |
## Future skills
These are planned but not yet implemented. Each will be extracted only when its content exceeds a self-contained section in the current skill.
- `fingerprint-morphe-method` — when fingerprint authoring alone grows past a self-contained section.
- `package-morphe-bundle` — when "how do I publish a `.mpp`" becomes a recurring question.
- `extend-morphe-patch-with-mpe` — when `.mpe` extension authoring needs its own workflow.
- `compat-block-for-app` — when `Compatibility` becomes its own generator/validator.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to add a new skill.
## License
The skill code, templates, and scripts in this repository are released under the **MIT License** — see [LICENSE](./LICENSE).
**Note on generated artifacts:** Skills here teach how to build Morphe patches. The `.mpp` and `.mpe` artifacts you produce are *derived works of Morphe patcher* and inherit the upstream **GPLv3 with Section 7 Additional Terms**. Read [Morphe Patcher LICENSE](https://github.com/MorpheApp/morphe-patcher/blob/main/LICENSE) before publishing derivative bundles. Section 7 places restrictions on reuse of the Morphe name and logos in derivative patch bundles — these are not relaxed by this skill's MIT license.
## Local reference (not committed)
The `MorpheApp/` directory in this checkout is a local-only reference of the upstream Morphe toolchain repos. It is included in `.gitignore` and will not be committed. Use it to verify doc references like `morphe-patcher/docs/2_2_patch_anatomy.md`.
+23
View File
@@ -0,0 +1,23 @@
{
"version": 1,
"skills": {
"brainstorming-research-ideas": {
"source": "orchestra-research/ai-research-skills",
"sourceType": "github",
"skillPath": "21-research-ideation/brainstorming-research-ideas/SKILL.md",
"computedHash": "52b90982250c80b21fc3512d7223d28413c0f1b726703f461d6ea8a166d22a1d"
},
"compatibility-testing": {
"source": "proffesor-for-testing/agentic-qe",
"sourceType": "github",
"skillPath": ".claude/skills/compatibility-testing/SKILL.md",
"computedHash": "91f99a08e85eaabf2e4e1bb790ef17f5ce9000f593488db90c4c6fe3d413a0bc"
},
"skill-authoring": {
"source": "grafana/skills",
"sourceType": "github",
"skillPath": "skills/grafana-core/skill-authoring/SKILL.md",
"computedHash": "cd4a3c32213e71229c10eddeff006ab0e7fb9844699108d0a7ac50aad30222a6"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
# Keep this directory tracked even when empty.
# Skills are added as siblings under this directory.
@@ -0,0 +1,38 @@
# Template skill
This directory is **not** an installed skill. It is a starter scaffold for contributors who want to add a new skill to this repository.
The `_` prefix on the directory name is intentional — it tells skills registries (e.g., `npx skills`, `skills-ref`) to skip this folder. Only skills without an underscore prefix are published as installable.
## How to use this template
1. **Copy the directory** to a new name:
```bash
cp -r skills/_template skills/<your-skill-name>
```
2. **Edit `SKILL.md`**:
- Replace `<skill-name>` in the YAML frontmatter with the directory name (they must match exactly).
- Replace `<Skill Title>` and the body content.
- Fill in the `description` field with trigger phrases — this is what makes the skill discoverable.
3. **Add supporting files** if needed:
- `references/<topic>.md` — distilled docs.
- `scripts/<name>.sh` — helper scripts.
- `assets/<template>.kt` — starter files.
4. **Validate locally**:
```bash
npx -y skills-ref validate skills/<your-skill-name>
```
5. **Open a PR.** CI will run validation and check that the README's skill list reflects the new skill.
## Conventions
- `name` (frontmatter) must **exactly match** the parent directory.
- `description` must be ≤ 1024 chars and include "when to use" trigger phrases.
- Keep `SKILL.md` body under 500 lines. Move deep content into `references/`.
- Use MIT-licensed code only. Distill any third-party documentation you reference — do not copy verbatim.
@@ -0,0 +1,49 @@
---
name: <skill-name>
description: <What this skill does and when to use it. Include trigger phrases — "create a", "add a", "how do I…", "make X Y".>
---
# <Skill Title>
<One-paragraph summary of what this skill does and why it exists.>
## When to use
- <Trigger scenario 1>
- <Trigger scenario 2>
**Do NOT use this skill for:**
- <Out-of-scope scenario 1>
- <Out-of-scope scenario 2>
## Inputs required
- <Input 1>: <description>
- <Input 2>: <description>
## Workflow
1. <Step 1>
2. <Step 2>
3. <Step 3>
## Reference index
- `references/<topic>.md` — <one-line description>
## Common pitfalls
- <Pitfall 1> → <fix>
- <Pitfall 2> → <fix>
## Minimal worked example
```kotlin
// 30-line example showing the skill in action.
```
## Where to look upstream
If you reference external docs, link them here so contributors can verify:
- <URL to canonical docs>
@@ -0,0 +1,149 @@
---
name: create-morphe-patch
description: Author a new Morphe patch in a Kotlin DSL patches bundle. Use when the user asks to "create a morphe patch", "add a patch to my morphe bundle", "write a patch for <app>", "fingerprint and patch <method>", "edit smali for a morphe patch", "add bytecodePatch or resourcePatch", "extend with .mpe", or describes a desired APK modification referencing the Morphe Patcher DSL (`bytecodePatch {}`, `resourcePatch {}`, `Fingerprint {}`, `extendWith`, `Compatibility`). Covers signature capture, fingerprinting, the `Compatibility` block, optional `.mpe` extensions, and Gradle build verification.
---
# Create a Morphe Patch
End-to-end workflow for authoring a new patch in a Morphe Patcher Kotlin DSL bundle (`morphe-patches` or a fork of `morphe-patches-template`).
## When to use
Use this skill when the user wants to:
- Author a new patch from a description + a base APK.
- Add a fingerprint for a method that needs to be patched.
- Define a `Compatibility` block for a target app.
- Ship a `.mpe` extension alongside a patch.
- Run the Gradle build and verify the patch appears in `patches-list.json`.
**Do NOT use this skill for:**
- Modifying the Morphe Patcher engine itself.
- Building the Morphe Manager Android UI.
- Non-Morphe APK modification (e.g., standalone smali edits without a patch bundle).
## Inputs required
Before starting, gather:
1. **Base APK path** — the original Play-signed `base.apk` (or any signing variant the patch should accept).
2. **Target app package** — `packageName` (e.g., `com.grindrapp.android`).
3. **Target app version** — at least one `version` string in `targets`. Multiple versions are fine.
4. **Human description of behavior** — what should the patch do? A single sentence is enough.
5. **Optional: `.mpe` extension source** — only if the patch needs compiled Java/Kotlin code injected.
## Workflow
1. **Capture the signature.** Run `scripts/compute-signature.sh <base.apk>` to get the delegated signer SHA-256. Use it for `Compatibility.signatures`. See `references/compatibility-block.md`.
2. **Author the fingerprint.** Open `references/fingerprinting.md` and construct a `Fingerprint {}` that uniquely identifies the method to patch. Prefer robust attributes (return type, parameter types, instruction filters) over obfuscated names.
3. **Declare compatibility.** If the patch targets a specific app, create a `Compatibility` instance per `references/compatibility-block.md`. If universal, omit it.
4. **Write the patch class.** Use `assets/PatchTemplate.kt` as a starting point. Follow the conventions in `references/structure-and-conventions.md` (verb-led name, third-person description, modularity).
5. **Add a `.mpe` extension if needed.** See `references/patch-anatomy.md` ("Extensions"). Use `assets/extension-stub/` as the skeleton and `extendWith("…mpe")` from the patch.
6. **Build the bundle.** Run `./gradlew :patches:buildAndroid :patches:generatePatchesList`. The generated `patches-list.json` should contain your new patch.
7. **Validate.** Run `scripts/validate-patch.sh "<patch substring>"` to confirm the build succeeded and the patch is registered.
## Reference index
Deeper docs, loaded on demand:
- `references/patch-anatomy.md` — `bytecodePatch`, `resourcePatch`, `compatibleWith`, `dependsOn`, `extendWith`, `extendWithAll`, `execute`, `finalize`, options.
- `references/fingerprinting.md` — `Fingerprint {}` DSL: `definingClass`, `accessFlags`, `returnType`, `parameters`, `filters`, `InstructionLocation`.
- `references/compatibility-block.md` — `Compatibility` class fields, signature capture, `AppTarget` versioning.
- `references/structure-and-conventions.md` — folder layout, naming, description style, modularity.
- `references/advanced-apis.md` — `classDefBy`, `mutableClassDefBy`, `get/delete`, `document(String/InputStream)`.
- `references/common-pitfalls.md` — gotchas that bite first-time patch authors.
## Common pitfalls
A short list with one-line fixes. Full discussion in `references/common-pitfalls.md`.
- ❌ Editing `patches-list.json` or `patches-bundle.json` by hand → ❌ regenerated by Gradle. Edit the source.
- ❌ Using the upload cert SHA-256 instead of the delegated signer → use `apksigner verify --print-certs` on the Play-signed APK.
- ❌ Fingerprinting on obfuscated method names → ✅ use return type, parameter types, and instruction filters.
- ❌ `extendWith` with a dynamic list → use `extendWithAll` so the provider is evaluated at patch time.
- ❌ Reusing Morphe name/logos in a derivative bundle → ✅ GPLv3 §7 prohibits this; pick your own brand.
- ❌ Missing `compatibleWith` → the patch is universal; usually that's wrong. Be explicit.
## Minimal worked example
Disable ads in `app.xyz.mobile` version `2.0.0`. Compute the signature, then:
```kotlin
// patches/src/main/kotlin/app/morphe/patches/xyz/AdsDisabled.kt
package app.morphe.patches.xyz
import app.morphe.patcher.Fingerprint
import app.morphe.patcher.bytecodePatch
import app.morphe.patcher.patch.Compatibility
import app.morphe.patcher.patch.AppTarget
import app.morphe.patcher.patch.PatchException
import app.morphe.patcher.opcode
import app.morphe.patcher.string
import app.morphe.patcher.fieldAccess
import app.morphe.patcher.Opcode
private const val PACKAGE = "app.xyz.mobile"
private const val VERSION = "2.0.0"
// 1. Replace with: bash scripts/compute-signature.sh base.apk
private const val SIGNATURE = "REPLACE_WITH_DELEGATED_SIGNER_SHA256"
private val COMPATIBILITY = Compatibility(
packageName = PACKAGE,
name = "XYZ",
apkFileType = ApkFileType.APK_REQUIRED,
appIconColor = 0xFF3300,
signatures = setOf(SIGNATURE),
targets = listOf(AppTarget(version = VERSION)),
)
private object ShowAdsFingerprint : Fingerprint(
definingClass = "Lapp/xyz/ads/AdsLoader;",
returnType = "Z",
parameters = listOf("Ljava/lang/String;", "I"),
filters = listOf(
fieldAccess(opcode = Opcode.IGET, definingClass = "this", type = "Ljava/util/Map;"),
string("showBannerAds"),
),
)
@Suppress("unused")
val disableAdsPatch = bytecodePatch(
name = "Disable ads",
description = "Disables ads in the app.",
default = true,
) {
compatibleWith(COMPATIBILITY)
execute {
ShowAdsFingerprint.let { match ->
match.method.addInstructions(
0,
"""
const/4 v0, 0x0
return v0
""",
)
}
}
}
```
Then build:
```bash
bash scripts/validate-patch.sh "Disable ads"
```
If the script exits 0, the patch is registered. If it exits 1, read the Gradle output.
## Where to look in the upstream docs
These files (in the local `MorpheApp/` reference checkout) are the canonical sources this skill distills:
- `MorpheApp/morphe-patcher/docs/2_2_patch_anatomy.md`
- `MorpheApp/morphe-patcher/docs/2_2_1_fingerprinting.md`
- `MorpheApp/morphe-patcher/docs/3_structure_and_conventions.md`
- `MorpheApp/morphe-patcher/docs/4_apis.md`
The reference docs in this skill are **distilled**, not verbatim. When the upstream docs change, update the references here.
@@ -0,0 +1,74 @@
/*
* Patch template — copy this file to patches/src/main/kotlin/<bundle>/patches/<feature>/<Name>Patch.kt
* and fill in the TODOs.
*
* Run `bash scripts/validate-patch.sh "<Name>"` after building to confirm the patch
* is registered in patches-list.json.
*/
package app.morphe.patches.<feature> // TODO: replace with your bundle namespace + feature folder
import app.morphe.patcher.Fingerprint
import app.morphe.patcher.bytecodePatch
import app.morphe.patcher.patch.Compatibility
import app.morphe.patcher.patch.AppTarget
import app.morphe.patcher.patch.PatchException
import app.morphe.patcher.Opcode
import app.morphe.patcher.opcode
import app.morphe.patcher.string
import app.morphe.patcher.fieldAccess
import app.morphe.patcher.methodCall
// TODO: replace with the actual Play-delegated signer SHA-256.
// Run: bash scripts/compute-signature.sh path/to/base.apk
private const val SIGNATURE = "<DELEGATED_SIGNER_SHA256>"
// TODO: replace with the Android package name + version you are targeting.
private const val PACKAGE_NAME = "<app.package.name>"
private const val VERSION = "<X.Y.Z>"
private val COMPATIBILITY = Compatibility(
packageName = PACKAGE_NAME,
name = "<App Name>", // TODO: visible app name
apkFileType = ApkFileType.APK_REQUIRED,
appIconColor = 0x000000, // TODO: 0xRRGGBB
signatures = setOf(SIGNATURE),
targets = listOf(AppTarget(version = VERSION)),
)
// TODO: replace with a fingerprint that uniquely identifies the method to patch.
// See references/fingerprinting.md for the full DSL.
private object TargetFingerprint : Fingerprint(
definingClass = "L<package>/<class>;", // Use obfuscated-safe strings (see docs).
returnType = "V",
parameters = listOf("Ljava/lang/String;"),
filters = listOf(
string("<unique-literal>"),
// Add more filters if needed: fieldAccess, methodCall, opcode, literal.
),
)
@Suppress("unused")
val <name>Patch = bytecodePatch(
name = "<Short verb-led name>", // TODO: e.g., "Disable ads"
description = "<Third-person description, ending with a period.>", // TODO
default = true, // Default on/off in the Manager UI.
) {
compatibleWith(COMPATIBILITY)
// Optional: dependsOn(otherPatch)
// Optional: extendWith("extension.mpe")
execute {
TargetFingerprint.let { match ->
// TODO: replace with the smali instructions you want to inject.
match.method.addInstructions(
0,
"""
const/4 v0, 0x0
return v0
""",
)
}
}
}
@@ -0,0 +1,56 @@
# Extension stub
A minimal Android library module that compiles to a `.mpe` DEX extension for use with Morphe patches.
## When to use
If your patch needs to inject compiled Java/Kotlin code (rather than raw smali instructions), build a `.mpe` extension and reference it from the patch via `extendWith("name.mpe")`.
## How to use
1. **Copy this directory** into your patches bundle under `extensions/<app>/<name>/`.
2. **Replace placeholders** in:
- `build.gradle.kts` — `namespace` and Java/Kotlin targets.
- `src/main/java/Stub.java` — class body and the reverse-DNS package.
- `src/main/AndroidManifest.xml` — namespace identifier.
3. **Add the module to `settings.gradle.kts`** at the bundle root:
```kotlin
include(":extensions:<app>:<name>")
```
4. **Build the `.mpe`**:
```bash
./gradlew :extensions:<app>:<name>:assembleRelease
```
The output is in `build/outputs/mpe/`.
5. **Copy the `.mpe`** into your patches module's `resources/` directory (or wherever the Gradle plugin expects extension files).
6. **Reference from the patch**:
```kotlin
val myPatch = bytecodePatch(name = "Use stub") {
extendWith("name.mpe")
execute {
// ... call into the extension via smali
match.method.addInstructions(
0,
"invoke-static {}, L<package>/Stub;->doSomething()V",
)
}
}
```
## Why `.mpe` instead of raw smali?
- **Complex logic** — multiple classes, helper methods, recursion.
- **Type safety** — write in Java/Kotlin, get all the IDE/tooling support.
- **Maintainability** — easier to read than hand-written smali.
## What goes in the extension
- Public static methods are the easiest to call from smali.
- Static fields are accessible via `sget-*` instructions.
- Non-static methods require you to instantiate first; rarely worth it.
## Constraints
- The extension is merged into the patched app's classpath at apply time.
- Avoid dependencies on Android system APIs that may not exist in the target app.
- Keep the extension small — the `.mpe` adds to the patched APK size.
@@ -0,0 +1,33 @@
// Minimal Android library module that compiles to a .mpe (DEX) extension for
// morphe-patches. Drop this directory into <your-bundle>/extensions/<app>/<name>/
// and add it to settings.gradle.kts as an included project.
//
// Build:
// ./gradlew :extensions:<app>:<name>:assembleRelease
//
// The .mpe artifact will be in build/outputs/mpe/. Copy it to your patches
// directory and reference it from the patch via:
// extendWith("<name>.mpe")
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "<your.bundle.namespace>.extension.<name>"
compileSdk = 34
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- This extension is merged into the patched app at apply time.
It does not produce a standalone APK. -->
</manifest>
@@ -0,0 +1,18 @@
package <your.bundle.namespace>.extension.<name>;
/**
* Stub class — replace with the Java/Kotlin class(es) your patch needs to call.
*
* From a patch, reference public static methods:
* invoke-static {}, L<your/bundle/namespace>/extension/<name>/Stub;->doSomething()V
*
* After building, place the produced .mpe artifact in your bundle's patches
* resources and reference it via extendWith("<name>.mpe").
*/
public final class Stub {
private Stub() {}
public static void doSomething() {
// Implementation goes here.
}
}
@@ -0,0 +1,152 @@
# Advanced APIs
A handful of helpers make patch development faster than raw smali mutation.
## `classDefBy(String)`
Look up an immutable class definition by its fully-qualified name.
```kotlin
execute {
val superClassOfReturnType = classDefBy(match().originalMethod.returnType).superclass
}
```
Use this when you need to walk a class hierarchy without going through a fingerprint match.
## `mutableClassDefBy(ClassDef)`
Make a class definition mutable so you can add/remove methods or fields.
```kotlin
execute {
val immutable = classDefBy("Lcom/some/app/Hook;")
val mutable = mutableClassDefBy(immutable)
mutable.methods.add(Method().apply {
definingClass = "Lcom/some/app/Hook;"
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC
name = "injectedMethod"
returnType = "V"
})
}
```
> The first call to `mutableClassDefBy` swaps the original class definition with the mutable copy in `context.classes`. Subsequent calls return the same mutable copy.
## `get(String, Boolean)` and `delete(String)`
Read, write, or delete resource files (any file inside the APK, not just `res/`).
```kotlin
execute {
val file = get("res/values/strings.xml")
val content = file.readText()
file.writeText(content.replace("Hello", "Goodbye"))
}
execute {
delete("res/values/strings.xml")
}
```
The second argument to `get` is `createIfMissing` — pass `true` if the patch should create the file if it doesn't exist.
## `document(String)` and `document(InputStream)`
Read and write XML files as DOM.
```kotlin
execute {
document("AndroidManifest.xml").use { doc ->
val element = doc.createElement("uses-permission").apply {
setAttribute("android:name", "android.permission.INTERNET")
}
doc.documentElement.appendChild(element)
}
}
```
Use the `InputStream` overload for resources embedded in the patch bundle:
```kotlin
execute {
val inputStream = classLoader.getResourceAsStream("fragment.xml")
document(inputStream).use { doc ->
// ... modify ...
}
}
```
The `.use { }` form ensures the document is closed and flushed.
## `originalMethod` vs `method`
When working with a fingerprint match:
- `originalMethod` — immutable view. Read-only access.
- `method` — mutable copy. First access creates the copy and replaces the original in `context.classes`.
```kotlin
execute {
val match = myFingerprint
// Reading only? Use originalMethod.
val accessFlags = match.originalMethod.accessFlags
// Modifying? Use method.
match.method.addInstructions(0, "return-void")
}
```
## Common combinations
**Modify a method's parameters:**
```kotlin
execute {
match.method.parameters = listOf("Ljava/lang/String;")
}
```
**Replace a method entirely:**
```kotlin
execute {
match.method.replaceInstructions(
"""
const-string v0, "replaced"
return-void
""",
)
}
```
**Add a new field to a class:**
```kotlin
execute {
val mutable = mutableClassDefBy(match.originalClassDef)
mutable.fields.add(Field().apply {
definingClass = mutable.type
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC
name = "addedField"
type = "I"
// initialValue = 0 // omit; default-initialized
})
}
```
## Threading and idempotency
Patches run on the JVM thread Morphe Patcher assigns. They are run sequentially in dependency order. Avoid global state, but if you must share between a dependency and the dependent patch, use a `Supplier<InputStream>` provider exposed through `extendWithAll` rather than mutable globals.
## Error handling
Throw `PatchException` to abort the apply with a clear message:
```kotlin
execute {
val match = myFingerprint.matchOrNull
?: throw PatchException("Could not find ShowAds in ${PACKAGE_NAME}")
}
```
The exception propagates, terminating the patch and rolling back any partial changes if the patcher supports it.
@@ -0,0 +1,141 @@
# Common pitfalls
The landmines that bite first-time patch authors. Most of these will silently produce a patch that compiles but does nothing useful.
## Pitfall 1: Hand-editing generated files
❌ Editing `patches-list.json`, `patches-bundle.json`, or `CHANGELOG.md` by hand.
✅ These are regenerated by Gradle (`./gradlew generatePatchesList`, `release.yml`). Edit the Kotlin source and let the build pipeline regenerate.
If you see a generated file with stale content, run `./gradlew :patches:generatePatchesList` to refresh.
## Pitfall 2: Wrong signature source
❌ Using the upload cert SHA-256 (from `apksigner verify` on the .apk you uploaded) or `keytool`'s output of a different cert.
✅ Use the **delegated signer** SHA-256. Run on the Play-signed `base.apk` (the APK the Play Store ships to users):
```bash
apksigner verify --print-certs base.apk
# Find "Signer #1 certificate DN" → "SHA-256 Digest: <hex>"
```
For `.apkm`/`.xapk` bundles, unzip and run on the inner `base.apk`. Different signing schemes (Play App Signing v2/v3) produce different certs; you want the one Play Store uses at install time.
## Pitfall 3: Fingerprinting on obfuscated names
❌ Match `definingClass = "Lb/a/a;"` or `name = "a"` when the target class is obfuscated.
✅ Use only attributes that survive R8/ProGuard:
- Return type (`"Z"`, `"V"`, `"Ljava/lang/String;"`).
- Parameter types (use `L` for obfuscated class objects).
- Instruction filters: unique strings, method calls, field accesses, literals.
- `accessFlags` (often stable).
If a fingerprint is too loose, it will match multiple methods. If too tight (e.g., relying on a single method instruction), it breaks on the next app update.
## Pitfall 4: `extendWith` with a dynamic list
❌ Building a list of `.mpe` files inside a `resourcePatch`'s `execute { }` and passing it to `extendWith`:
```kotlin
val prepare = resourcePatch { execute { derived += someStream() } }
val consumer = bytecodePatch {
dependsOn(prepare)
extendWith(derived.last()) // ❌ evaluated at build time, derived is empty
}
```
✅ Use `extendWithAll` with a `Supplier<InputStream>`:
```kotlin
val consumer = bytecodePatch {
dependsOn(prepare)
extendWithAll { derived.toList() } // evaluated at patch time
}
```
The provider runs after `prepare.execute {}` has populated the list.
## Pitfall 5: Missing `compatibleWith`
If a patch needs to target a specific app but has no `compatibleWith`, the patcher will try to run it against any package. The patch may match across apps (false positives) or fail in surprising ways.
✅ Always declare `compatibleWith` for app-specific patches. If truly universal, leave it off — but be explicit by writing a comment.
## Pitfall 6: Modifying without indexes refreshing
After `addInstructions` or `removeInstruction`, the underlying `InstructionList` shifts. Subsequent `instructionMatches[i].index` references become stale.
✅ Work from **last to first**, or call `clearMatch()` + `match()` after each modification:
```kotlin
execute {
myFingerprint.let { match ->
// Edit last filter first.
match.method.removeInstruction(match.instructionMatches[2].index)
match.method.clearMatch()
// Re-match to refresh indexes.
match.method.match()
// Now edit first filter.
match.method.addInstructions(match.instructionMatches[0].index, "const/4 v0, 0x0")
}
}
```
## Pitfall 7: GPLv3 §7 branding violations
❌ Naming a derivative bundle `xyz-morphe-patches`, using the Morphe logo, or including upstream Morphe branding in README screenshots.
✅ Section 7 of the upstream GPLv3 prohibits this. Use a distinct brand name (e.g., `GrindrPlus-ReMorphe`) and your own logo. The Morphe Patcher engine and `.mpp` format are GPLv3 — the skill here is MIT — but derivative bundles that ship with the Morphe name or logos are out of compliance.
## Pitfall 8: Forgetting `version = null`
If `targets` is an empty list after edits, `Compatibility` throws:
```kotlin
Compatibility(
packageName = "app.x",
name = "X",
targets = listOf(), // ❌ throws "Must declare at least one app target"
)
```
✅ Use `listOf(AppTarget(version = null))` to mean "any version":
```kotlin
targets = listOf(AppTarget(version = null))
```
## Pitfall 9: Wrong `appIconColor` format
❌ `appIconColor = 0xFFFF3300` (includes alpha).
✅ `appIconColor = 0xFF3300` (0xRRGGBB). The constructor enforces alpha = 0.
## Pitfall 10: Gradle build without Apache Maven credentials
`morphe-patches` (and downstream forks) typically publish snapshots to a private Maven repo via the `gradle.properties` `gpr.user` / `gpr.key` GitHub Packages token. If missing, the build fails with a 401.
✅ Ensure `~/.gradle/gradle.properties` has:
```
gpr.user=<github-username>
gpr.key=<github-pat-with-packages-write>
```
Or set them as environment variables in CI.
## Pitfall 11: Forgetting to publish the `.mpe`
If the patch uses `extendWith("foo.mpe")` but the `.mpe` is not in `patches/src/main/resources/` (or the configured extension folder), the build succeeds but the patch fails to apply at runtime with `ClassNotFoundException`.
✅ Verify the `.mpe` is in the `extensions/` folder referenced by the Gradle plugin. Run `./gradlew :patches:buildAndroid` and inspect the produced `.mpp` with `unzip -l`.
## Pitfall 12: Reusing a fingerprint's match
A fingerprint matches the **first** time its methods are accessed. If two patches use the same fingerprint object, the second patch sees the same match result (and any modifications the first patch made).
✅ If you need a fresh match, call `clearMatch()` on the fingerprint before reuse, or declare a new fingerprint object.
@@ -0,0 +1,112 @@
# Compatibility block
`Compatibility` declares which app, which versions, and which signing certificates a patch targets. Without it, the patch is **universal** — runs against any package.
## Capturing the signature
The `signatures` set contains SHA-256 fingerprints of the APKs the patch accepts. Use the **delegated signer** (the certificate the Play Store uses to sign the APK), not the upload cert.
```bash
# Run from the skill's scripts/ directory:
bash scripts/compute-signature.sh path/to/base.apk
# Output: one line of hex.
```
Or manually:
```bash
apksigner verify --print-certs path/to/base.apk
# Look for "Signer #1 certificate DN" → "SHA-256 Digest: <hex>"
```
> ⚠️ For `.apkm`/`.xapk` archives, unzip and run on the inner `base.apk`. The SHA-256 must match the cert Play Store delegates.
## Defining Compatibility
```kotlin
val COMPATIBILITY_XYZ = Compatibility(
packageName = "app.xyz.mobile",
name = "XYZ App",
description = "Optional user-facing description.",
apkFileType = ApkFileType.APK_REQUIRED, // or APK_OPTIONAL, APKM, etc.
appIconColor = 0xFF3300, // ARGB int, only used for Manager UI.
signatures = setOf("6e21a5f966a7252628cd8f5982d93086a0fcf76c09edb9d7731964bb1c842897"),
targets = listOf(
AppTarget(version = "2.0.0"),
AppTarget(version = "1.0.42"),
),
)
```
## Fields
| Field | Type | Required | Purpose |
|---|---|---|---|
| `packageName` | `String` | Yes (unless universal) | Android application ID. |
| `name` | `String` | Yes (if `packageName`) | App name shown in the Manager UI. |
| `description` | `String?` | No | User-facing app description. |
| `apkFileType` | `ApkFileType` | No | `APK_REQUIRED`, `APK_OPTIONAL`, `APKM`, `APKM_REQUIRED`, etc. |
| `appIconColor` | `Int` (0xRRGGBB) | No | Background color for the app icon in the Manager UI. |
| `signatures` | `Set<String>` | No | SHA-256 allowed cert digests. |
| `targets` | `List<AppTarget>` | Yes | Versions supported. |
## Universal patch
If the patch is not app-specific, declare it without a `packageName`:
```kotlin
val universalPatch = bytecodePatch(name = "Generic patch") {
// No compatibleWith → runs against any package.
}
```
## AppTarget
Each `AppTarget` declares a version. Newest first.
```kotlin
AppTarget(version = "2.0.0") // Any device with v2.0.0.
AppTarget(version = "1.0.42", minSdk = 32) // v1.0.42 requires SDK 32+.
AppTarget(version = "1.0.42", versionCode = 12345) // Specific release.
AppTarget(version = null) // Any version (default).
```
By default, `targets` is `listOf(AppTarget(version = null))` — meaning "any version of the package." Override with a list of explicit versions.
## ApkFileType
Specify whether the patch needs a `.apk` or accepts `.apkm`/`.xapk`/`.apks`:
- `APK_REQUIRED` — cannot process `.apkm` directly.
- `APK_OPTIONAL` — accepts `.apk`; `.apkm` may work but is not preferred.
- `APKM_REQUIRED`, `APKM` — `.apkm` (split APKs).
- `APKS_REQUIRED`, `APKS` — `.apks` (Google bundle).
- `XAPK_REQUIRED`, `XAPK` — `.xapk` (third-party split).
The `_REQUIRED` variants tell the Manager UI to refuse other types.
## Validation rules
The `Compatibility` constructor enforces these constraints:
- `packageName` must match the Android package name regex (if present).
- `appIconColor` must be `0xRRGGBB` (alpha = 0).
- `signatures` must be valid 64-hex SHA-256 strings.
- `targets` must not be empty.
- No duplicate `version` values in `targets`.
- A universal patch (null `packageName`) cannot declare any version targets.
## How to bump a version
When a new app release ships:
1. Update `targets` with the new `AppTarget(version = "X.Y.Z")`. Newest first.
2. If the signing cert rotated, update `signatures` with the new delegated signer SHA-256.
3. Rebuild. `./gradlew :patches:generatePatchesList` will reflect the change.
## Fingerprint compatibility
If your patch's `Fingerprint` matches the same method across multiple versions, you only need one patch. If the implementation differs per version, you typically need:
- One `AppTarget` per version with the same `compatibleWith`, OR
- Multiple patch entries each declared with its own `compatibleWith`.
@@ -0,0 +1,136 @@
# Fingerprinting
A `Fingerprint` is a partial description of a method — enough to uniquely identify it across app versions when names are obfuscated. The fingerprint matches if **all** declared attributes match.
## Anatomy
```kotlin
object AdLoaderFingerprint : Fingerprint(
definingClass = "Lcom/some/app/ads/AdsLoader;",
accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL),
returnType = "Z",
parameters = listOf("Ljava/lang/String;", "I", "L"),
filters = listOf(
fieldAccess(opcode = Opcode.IGET, definingClass = "this", type = "Ljava/util/Map;"),
string("showBannerAds"),
methodCall(definingClass = "Ljava/lang/String;", name = "equals"),
opcode(Opcode.MOVE_RESULT, InstructionLocation.MatchAfterImmediately()),
literal(1337),
opcode(Opcode.IF_EQ),
),
)
```
## Field meanings
| Field | What it matches |
|---|---|
| `definingClass` | Exact class name (StringComparisonType). Use `"this"` for the enclosing class. |
| `accessFlags` | `AccessFlags.PUBLIC`, `FINAL`, `STATIC`, etc. |
| `returnType` | Return type descriptor (`Z`, `V`, `Ljava/lang/String;`). |
| `parameters` | Ordered parameter type descriptors. |
> **Obfuscated class names** change between releases. For obfuscated types, use the bare object type `L` (no name). For non-obfuscated types, use the full name.
## Filters
Filters are ordered instruction descriptors. They must appear in the same order as the target method. Zero or more unrelated instructions can exist between filters.
- **`fieldAccess(opcode, definingClass, type)`** — match a field read/write.
- **`string("literal")`** — match a `const-string` of the given value.
- **`methodCall(definingClass, name, returnType, parameters)`** — match an invoke.
- **`opcode(Opcode.X, location)`** — match an arbitrary opcode.
- **`literal(value)`** — match a constant pool literal.
- **`anyInstruction(...)`** — match one of several alternative filters.
For full smali syntax on `methodCall`/`fieldAccess`, you can paste a smali statement:
```kotlin
methodCall(smali = "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;")
```
## `InstructionLocation`
Controls where the filter matches relative to the previous one:
- `MatchAfterImmediately()` — back-to-back (no instructions in between).
- `MatchAfterWithin(int)` — within N instructions of the previous.
- `MatchFirst()` — first instruction of the method.
## Strings: two ways
- **`filters = listOf(string("foo"), string("bar"))`** — ordered. `foo` must appear before `bar`.
- **`strings = listOf("foo", "bar")`** — unordered. Useful for enum-like constructors with many strings.
## Using a fingerprint
After declaration, use it inside `execute`:
```kotlin
execute {
AdLoaderFingerprint.let { match ->
val moveResultIndex = match.instructionMatches[3].index
val register = match.instructionMatches[3]
.getInstruction<OneRegisterInstruction>().registerA
match.method.addInstructions(moveResultIndex + 1, "const/4 v$register, 0x0")
}
}
```
Available result properties:
- `originalClassDef` / `originalMethod` — immutable.
- `classDef` / `method` — mutable (lazy copy on first access).
- `instructionMatches` — list of `InstructionMatch` results in declaration order.
> Use `*OrNull` variants (`matchOrNull`, `methodOrNull`) when a missing match is a normal case.
## Multiple modifications
If you make more than one modification to the same method, indexes shift. Work from **last to first**, or call `clearMatch()` + `match()` to refresh.
```kotlin
execute {
AdLoaderFingerprint.let { match ->
// Last filter first.
match.method.removeInstruction(match.instructionMatches[5].index)
match.method.addInstructions(
match.instructionMatches[3].index + 1,
"const/4 v0, 0x0",
)
}
}
```
A fingerprint matches **once per usage** unless `clearMatch()` is called. This makes fingerprints shareable across patches.
## Pure opcode matching
If no built-in filter works, use raw opcode patterns:
```kotlin
filters = OpcodesFilter.opcodesToFilters(Opcode.MOVE_RESULT_OBJECT, Opcode.IGET_OBJECT)
```
Opcode patterns are exact (no variable spacing) and fragile — prefer built-in filters when possible.
## Finding all matches
To transform every method that matches (e.g., rewrite a string everywhere):
```kotlin
Fingerprint(filters = listOf(string("target"))).matchAllOrNull()?.forEach { match ->
match.method.findInstructionIndicesReversedOrThrow(string("target")).forEach { idx ->
val register = match.method.getInstruction<OneRegisterInstruction>(idx).registerA
match.method.replaceInstruction(idx, "const-string v$register, \"replacement\"")
}
}
```
## Strong-fingerprint rules
- ✅ Use return type, parameter types, and instruction filters.
- ✅ Capture unique strings, method calls, or literals the method references.
- ❌ Never fingerprint on obfuscated names.
- ❌ Avoid opcode-only patterns unless nothing else disambiguates.
@@ -0,0 +1,110 @@
# Patch anatomy
A patch in Morphe Patcher is a Kotlin DSL object that describes one transformation. There are two kinds: `bytecodePatch` (modifies smali/code) and `resourcePatch` (modifies resources like XML, strings, layouts). Both share the same lifecycle.
## Basic structure
```kotlin
val myPatch = bytecodePatch(
name = "Short verb-led name",
description = "Third-person description ending with a period.",
default = true, // Default on/off in the Manager UI.
) {
compatibleWith(COMPATIBILITY_XYZ)
dependsOn(otherPatch)
extendWith("optional.mpe")
extendWithAll { dynamicList } // Evaluated at patch time.
execute {
// ... fingerprint matching, smali edits, etc.
}
finalize {
// ... post-processing, runs after all patches.
}
}
```
## The two patch kinds
- **`bytecodePatch`** — operates on bytecode (smali). Use for method-level modifications, instruction inserts, class mutations.
- **`resourcePatch`** — operates on resources (XML, files). Use for manifest edits, network security config, image swaps, string lists.
## `compatibleWith`
Pin a patch to a specific `Compatibility` (package + versions + signatures). Without it, the patch runs against any package. Usually you want this — be explicit.
```kotlin
compatibleWith(COMPATIBILITY_XYZ)
```
## `dependsOn`
Run another patch first. Dependencies execute before the depending patch. If a dependency raises an exception, the dependent patch will not run.
```kotlin
dependsOn(otherPatch)
```
## `execute { }`
The transformation body. For a `bytecodePatch`, this is where you fingerprint, match, and edit. For a `resourcePatch`, this is where you read/write files with `document()`, `get()`, `delete()`.
## `extendWith` and `extendWithAll`
Inject a precompiled DEX file (`.mpe`) into the patched app **before** the patch executes. This lets you ship complex Java/Kotlin logic instead of writing smali by hand.
```kotlin
// Compile-time: a single .mpe file bundled into the patch.
extendWith("disable-ads.mpe")
// Dynamic: a list that depends on another patch's output.
extendWithAll { derivedExtensions }
```
> **`extendWith` is evaluated when the patch is built.** If your extension list depends on runtime output, use `extendWithAll` and a `Supplier<InputStream>` provider. The provider runs after the dependency's `execute {}` block has populated the list.
## `finalize { }`
Post-processing. Runs after all patches have executed, in reverse order of execution. Useful for closing files, flushing logs, or any cleanup.
```kotlin
finalize {
println("done")
}
```
## Options
Make patches configurable with options. Useful for toggling behavior at runtime.
```kotlin
val patch = bytecodePatch(name = "Patch", default = true) {
val value by stringOption(name = "Inbuilt option")
val custom by option<String>(name = "Custom string")
execute {
println(value)
println(custom)
}
}
```
Options are typed (`stringOption`, `booleanOption`, `intOption`, etc.) and can be set after loading via `PatchLoader`.
## Failure mode
A patch can throw `PatchException` at any time during execution to signal failure. The exception propagates and terminates the apply.
```kotlin
execute {
val match = myFingerprint.matchOrNull
?: throw PatchException("Method not found in ${PACKAGE_NAME}")
}
```
## Tips
- Patches with no `name` are skipped by `PatchLoader`. Always set one.
- If `compatibleWith` is not used, the patch is universal.
- A null `targets` (or `AppTarget(version = null)`) means "any version".
- Patches can be referenced by multiple other patches through `dependsOn`.
@@ -0,0 +1,89 @@
# Structure and conventions
Patches are organized under `patches/src/main/kotlin/<bundle.namespace>/patches/<category>/` (or in a fork's namespace). Each category groups related patches for one feature area.
## Folder layout
```
📦your.patches.app.category
├ 🔍Fingerprints.kt
└ 🧩SomePatch.kt
```
- `Fingerprints.kt` — declares `object` fingerprints shared by the patches in this folder.
- `SomePatch.kt` — declares one patch (and its private fingerprints if needed).
A separate `Fingerprints.kt` file is not strictly required, but it helps when a patch uses multiple fingerprints.
## Real-world example
From `morphe-patches` (upstream):
```
patches/src/main/kotlin/app/morphe/patches/music/ad/
├ Fingerprints.kt
└ HideAdsPatch.kt
```
Nested grouping by app + feature keeps the patch list browsable.
## Naming rules
### Patch name
- **Verb-led, short, descriptive.** "Disable ads", "Change button color", "Override certificate pinning".
- **Capitalized** like a title. No periods.
- **Stable across versions** — don't put the version number in the name.
### Patch description
- **Third-person, present tense.** "Disables ads in the app."
- **End with a period.**
- Can be omitted only if the name is already self-explanatory.
### Fingerprint name
- **Best guess at what the target method does.** `AdLoaderFingerprint`, `ShowAdsFingerprint`, `OnCreateFingerprint`.
- Object-style: `object MyFingerprint : Fingerprint(...)`.
### File name
- **Matches the patch name** with `Patch` suffix: `HideAdsPatch.kt`.
- **Fingerprints file**: `Fingerprints.kt` (plural).
## Modularity
- **Patches can depend on other patches.** Use `dependsOn(...)` to chain them.
- **Write patches for reuse.** A patch that "removes ads" should not assume a specific app — let `compatibleWith` constrain it.
- **Keep patches minimal.** Tiny patches are robust across updates. If a patch grows large, split it or move logic into a `.mpe` extension.
## Documentation
Patches are abstract. Non-obvious code should be commented:
- Why a specific method is being patched.
- What the new instruction sequence does.
- Why a particular instruction index was chosen.
- Why an extension is required.
```kotlin
// PairIP bypass: replace verification result with success.
// The original method calls into native libpairipcore.so.
// Here we short-circuit the flow so the runtime check passes.
match.method.addInstructions(
0,
"""
const/4 v0, 0x1
return v0
""",
)
```
## Naming across the bundle
- **Bundle namespace** (e.g., `app.morphe.patches.youtube.ad`) matches the bundle's `groupId` in `settings.gradle.kts` and the `Patches` package in `morphe-patches-gradle-plugin`.
- **Forks** should use a different namespace (e.g., `grindrmorphe.morphe.patches.…`) to avoid collisions on the generated `.mpp` and to make the derivative clear.
## Linting
Most Morphe bundles ship an `.editorconfig` and a Kotlin lint config. Match the format of existing patches in your bundle. Use 4-space indentation, no wildcard imports, trailing commas where Kotlin allows.
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
#
# compute-signature.sh — extract the delegated signer SHA-256 from an Android APK.
#
# Usage: bash scripts/compute-signature.sh <path-to-apk>
#
# Outputs the SHA-256 digest (one line, lowercase hex) used in
# Compatibility.signatures. The delegated signer is the certificate the
# Play Store uses to sign the APK delivered to users — NOT the upload cert
# you signed with before upload, and NOT the v2/v3 signing scheme cert.
#
# Requires: apksigner (from Android SDK build-tools/<version>/apksigner).
# If apksigner is on PATH, this script works. Otherwise, supply a path via
# $APKSIGNER_BIN or set $ANDROID_HOME so it can be located.
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <path-to-apk>" >&2
echo "Outputs the Play-delegated signer SHA-256 to stdout." >&2
exit 2
fi
APK="$1"
if [ ! -f "$APK" ]; then
echo "::error::APK not found: $APK" >&2
exit 1
fi
# Locate apksigner.
APKSIGNER="${APKSIGNER_BIN:-}"
if [ -z "$APKSIGNER" ]; then
if command -v apksigner >/dev/null 2>&1; then
APKSIGNER="$(command -v apksigner)"
elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then
# Pick the highest-versioned build-tools/apksigner.
APKSIGNER="$(find "${ANDROID_HOME}/build-tools" -name apksigner -type f 2>/dev/null | sort -V | tail -1)"
fi
fi
if [ -z "$APKSIGNER" ] || [ ! -x "$APKSIGNER" ]; then
echo "::error::apksigner not found." >&2
echo "Install the Android SDK build-tools and ensure apksigner is on PATH," >&2
echo "or set APKSIGNER_BIN to its absolute path." >&2
exit 1
fi
# Print the certificate chain. apksigner prints blocks like:
# Signer #1 certificate DN: CN=...
# Signer #1 certificate SHA-256 digest: <hex>
# Signer #1 certificate SHA-1 digest: <hex>
# We want the SHA-256 digest under Signer #1 (the delegated signer).
SHA256="$(
"$APKSIGNER" verify --print-certs "$APK" 2>/dev/null \
| awk '
/Signer #1 certificate DN/ { in_signer = 1; next }
/Signer #2/ { in_signer = 0 }
in_signer && /SHA-256 digest:/ { print $NF; exit }
'
)"
if [ -z "$SHA256" ]; then
echo "::error::Could not parse SHA-256 digest for Signer #1." >&2
echo "Run '$APKSIGNER verify --print-certs \"$APK\"' manually to inspect." >&2
exit 1
fi
# Lowercase + strip whitespace.
echo "$SHA256" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
#
# validate-patch.sh — build the patches bundle and check that a patch
# substring is present in the generated patches-list.json.
#
# Usage: bash scripts/validate-patch.sh "<patch-name-substring>"
#
# Exit 0 if found, exit 1 if not, exit 2 on misuse.
#
# Requires: ./gradlew on PATH (or run from the bundle root), jq for JSON parsing.
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <patch-name-substring>" >&2
echo "Builds the bundle and checks for <substring> in patches-list.json." >&2
exit 2
fi
PATCH_SUBSTRING="$1"
if ! command -v jq >/dev/null 2>&1; then
echo "::error::jq is required. Install via your package manager." >&2
exit 1
fi
if [ ! -x "./gradlew" ]; then
echo "::error::./gradlew not found. Run this from the bundle root (e.g., morphe-patches/)." >&2
exit 1
fi
echo "==> Building :patches and generating patches-list.json"
./gradlew :patches:buildAndroid :patches:generatePatchesList --quiet
if [ ! -f "../patches-list.json" ] && [ ! -f "patches-list.json" ]; then
echo "::error::patches-list.json not produced. Build may have failed." >&2
exit 1
fi
LIST_FILE="patches-list.json"
[ -f "../patches-list.json" ] && LIST_FILE="../patches-list.json"
echo "==> Checking for \"${PATCH_SUBSTRING}\" in ${LIST_FILE}"
if jq -e --arg s "${PATCH_SUBSTRING}" '.patches[]? | select(.name | test($s; "i"))' "${LIST_FILE}" >/dev/null; then
echo "✓ Found: $(jq -r --arg s "${PATCH_SUBSTRING}" '.patches[] | select(.name | test($s; "i")) | .name' "${LIST_FILE}")"
exit 0
else
echo "::error::Patch containing \"${PATCH_SUBSTRING}\" not found in ${LIST_FILE}." >&2
echo "Available patches:" >&2
jq -r '.patches[]? | " - " + .name' "${LIST_FILE}" >&2 || true
exit 1
fi
+863
View File
@@ -0,0 +1,863 @@
{
"name": "heretek-re-marketplace",
"version": "1.0.0",
"description": "Heretek Reverse Engineering Marketplace Plugin",
"mcpServers": {
"re-android-dynamic": {
"command": "uv",
"args": [
"--directory",
"/servers/re-android-dynamic",
"run",
"re-android-dynamic"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-angr": {
"command": "uv",
"args": [
"--directory",
"/servers/re-angr",
"run",
"re-angr"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-analysis": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-analysis",
"run",
"re-anti-analysis"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-debug-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-debug-patch",
"run",
"re-anti-debug-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-vm-spoof": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-vm-spoof",
"run",
"re-anti-vm-spoof"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-apktool": {
"command": "uv",
"args": [
"--directory",
"/servers/re-apktool",
"run",
"re-apktool"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-binary-diff": {
"command": "uv",
"args": [
"--directory",
"/servers/re-binary-diff",
"run",
"re-binary-diff"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-c-injection-build": {
"command": "uv",
"args": [
"--directory",
"/servers/re-c-injection-build",
"run",
"re-c-injection-build"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-capa": {
"command": "uv",
"args": [
"--directory",
"/servers/re-capa",
"run",
"re-capa"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-catalog-match": {
"command": "uv",
"args": [
"--directory",
"/servers/re-catalog-match",
"run",
"re-catalog-match"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-cinematic-skip": {
"command": "uv",
"args": [
"--directory",
"/servers/re-cinematic-skip",
"run",
"re-cinematic-skip"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet",
"run",
"re-dotnet"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet-patch",
"run",
"re-dotnet-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-encrypted-vm-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-encrypted-vm-bypass",
"run",
"re-encrypted-vm-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-entitlement-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-entitlement-bypass",
"run",
"re-entitlement-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida",
"run",
"re-frida"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-runtime",
"run",
"re-frida-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-wine-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-wine-runtime",
"run",
"re-frida-wine-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-fuzz-replay": {
"command": "uv",
"args": [
"--directory",
"/servers/re-fuzz-replay",
"run",
"re-fuzz-replay"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-game-ac-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-game-ac-bypass",
"run",
"re-game-ac-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-gdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-gdb",
"run",
"re-gdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ghidra-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ghidra-remote",
"run",
"re-ghidra-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-hypervisor-detect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-hypervisor-detect",
"run",
"re-hypervisor-detect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ida-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ida-remote",
"run",
"re-ida-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp",
"run",
"re-il2cpp"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp-triage",
"run",
"re-il2cpp-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-injection-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-injection-runtime",
"run",
"re-injection-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ios": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ios",
"run",
"re-ios"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-kaitai": {
"command": "uv",
"args": [
"--directory",
"/servers/re-kaitai",
"run",
"re-kaitai"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-launch-and-observe": {
"command": "uv",
"args": [
"--directory",
"/servers/re-launch-and-observe",
"run",
"re-launch-and-observe"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-leak-scan": {
"command": "uv",
"args": [
"--directory",
"/servers/re-leak-scan",
"run",
"re-leak-scan"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-lief": {
"command": "uv",
"args": [
"--directory",
"/servers/re-lief",
"run",
"re-lief"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-llm-decompile": {
"command": "uv",
"args": [
"--directory",
"/servers/re-llm-decompile",
"run",
"re-llm-decompile"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-mitm2swagger": {
"command": "uv",
"args": [
"--directory",
"/servers/re-mitm2swagger",
"run",
"re-mitm2swagger"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-orchestrator": {
"command": "uv",
"args": [
"--directory",
"/servers/re-orchestrator",
"run",
"re-orchestrator"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch",
"run",
"re-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch-apply": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch-apply",
"run",
"re-patch-apply"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pcap": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pcap",
"run",
"re-pcap"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pdb",
"run",
"re-pdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-qemu-antidetect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-qemu-antidetect",
"run",
"re-qemu-antidetect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-report-write": {
"command": "uv",
"args": [
"--directory",
"/servers/re-report-write",
"run",
"re-report-write"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-rizin": {
"command": "uv",
"args": [
"--directory",
"/servers/re-rizin",
"run",
"re-rizin"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-runtime-dump": {
"command": "uv",
"args": [
"--directory",
"/servers/re-runtime-dump",
"run",
"re-runtime-dump"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-speakeasy": {
"command": "uv",
"args": [
"--directory",
"/servers/re-speakeasy",
"run",
"re-speakeasy"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-target-fingerprint": {
"command": "uv",
"args": [
"--directory",
"/servers/re-target-fingerprint",
"run",
"re-target-fingerprint"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-traffic-capture": {
"command": "uv",
"args": [
"--directory",
"/servers/re-traffic-capture",
"run",
"re-traffic-capture"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triage",
"run",
"re-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triton": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triton",
"run",
"re-triton"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vendor-anti-tamper": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vendor-anti-tamper",
"run",
"re-vendor-anti-tamper"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-bridge": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-bridge",
"run",
"re-vm-bridge"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-control": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-control",
"run",
"re-vm-control"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-debug": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-debug",
"run",
"re-vm-debug"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-decrypt": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-decrypt",
"run",
"re-vm-decrypt"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-launch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-launch",
"run",
"re-vm-launch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-memory": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-memory",
"run",
"re-vm-memory"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-ssh": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-ssh",
"run",
"re-vm-ssh"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vtil": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vtil",
"run",
"re-vtil"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-winedbg": {
"command": "uv",
"args": [
"--directory",
"/servers/re-winedbg",
"run",
"re-winedbg"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-x64dbg-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-x64dbg-remote",
"run",
"re-x64dbg-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara",
"run",
"re-yara"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara-author": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara-author",
"run",
"re-yara-author"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"ghidra-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ghidra-mcp",
"run",
"ghidra-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ida-pro-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ida-pro-mcp",
"run",
"ida-pro-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"x64DbgMCPServer": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/x64DbgMCPServer",
"run",
"x64dbg-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ssh-mcp": {
"command": "/vendored/ssh-mcp/ssh-mcp",
"args": [
"--storage",
"/data/ssh-mcp/storage.db"
],
"env": {}
},
"re-ui-automate": {
"command": "/vendored/touchpoint/.venv/bin/touchpoint-mcp",
"args": [],
"env": {
"PYTHONUNBUFFERED": "1",
"TOUCHPOINT_FALLBACK_INPUT": "true"
}
},
"re-persistproc": {
"command": "/vendored/persistproc/.venv/bin/persistproc",
"args": [
"serve",
"--port",
"8947"
],
"env": {
"PERSISTPROC_PORT": "8947"
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{yml,yaml,json,md,ts,tsx,js,jsx,html,css}]
indent_size = 2
[*.{py,pyi}]
indent_size = 4
[Makefile]
indent_style = tab
+39
View File
@@ -0,0 +1,39 @@
# Text files — normalize line endings
* text=auto
# Markdown
*.md text diff=markdown eol=lf
# Python
*.py text diff=python
*.pyi text
# Config / data
*.yml text
*.yaml text
*.json text
*.toml text
*.cfg text
*.ini text
# Shell scripts — LF only
*.sh text eol=lf
*.bash text eol=lf
# Web
*.html text
*.css text
*.js text
*.ts text
# Binary — do not touch
*.dll binary
*.exe binary
*.so binary
*.dylib binary
*.apk binary
*.aab binary
*.jar binary
*.dex binary
*.bin binary
*.dat binary
+35
View File
@@ -0,0 +1,35 @@
---
name: Bug report
about: Report a bug in this project
title: ''
labels: bug
assignees: ''
---
## Description
A clear description of the bug.
## Steps to reproduce
1.
2.
3.
## Expected behavior
What you expected to happen.
## Actual behavior
What actually happened.
## Environment
- OS:
- Python version:
- Package version:
## Additional context
Any other context (screenshots, logs, etc.).
+23
View File
@@ -0,0 +1,23 @@
---
name: Feature request
about: Suggest a new feature or improvement
title: ''
labels: enhancement
assignees: ''
---
## Description
A clear description of the feature you'd like.
## Use case
Why is this feature needed? What problem does it solve?
## Proposed solution
If you have ideas about how to implement it, describe them here.
## Alternatives considered
Any alternative solutions or workarounds you've considered.
+22
View File
@@ -0,0 +1,22 @@
## What
Brief description of the change.
## Why
Why is this change needed?
## How
How does it work? Key implementation details.
## Verification
How was this tested?
## Checklist
- [ ] Tests pass locally
- [ ] No engagement-specific references (game titles, SOW codes, host paths) in source or comments
- [ ] Documentation updated (if applicable)
- [ ] Breaking changes documented (if applicable)
+24
View File
@@ -0,0 +1,24 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.venv/
venv/
env/
build/
dist/
*.egg-info/
.pytest_cache/
.tox/
.mypy_cache/
.ruff_cache/
*.log
.vscode/
.idea/
*.swp
*.swo
.DS_Store
Thumbs.db
# Per-MCP servers
# servers/
+860
View File
@@ -0,0 +1,860 @@
{
"mcpServers": {
"re-android-dynamic": {
"command": "uv",
"args": [
"--directory",
"/servers/re-android-dynamic",
"run",
"re-android-dynamic"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-angr": {
"command": "uv",
"args": [
"--directory",
"/servers/re-angr",
"run",
"re-angr"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-analysis": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-analysis",
"run",
"re-anti-analysis"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-debug-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-debug-patch",
"run",
"re-anti-debug-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-anti-vm-spoof": {
"command": "uv",
"args": [
"--directory",
"/servers/re-anti-vm-spoof",
"run",
"re-anti-vm-spoof"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-apktool": {
"command": "uv",
"args": [
"--directory",
"/servers/re-apktool",
"run",
"re-apktool"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-binary-diff": {
"command": "uv",
"args": [
"--directory",
"/servers/re-binary-diff",
"run",
"re-binary-diff"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-c-injection-build": {
"command": "uv",
"args": [
"--directory",
"/servers/re-c-injection-build",
"run",
"re-c-injection-build"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-capa": {
"command": "uv",
"args": [
"--directory",
"/servers/re-capa",
"run",
"re-capa"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-catalog-match": {
"command": "uv",
"args": [
"--directory",
"/servers/re-catalog-match",
"run",
"re-catalog-match"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-cinematic-skip": {
"command": "uv",
"args": [
"--directory",
"/servers/re-cinematic-skip",
"run",
"re-cinematic-skip"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet",
"run",
"re-dotnet"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-dotnet-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-dotnet-patch",
"run",
"re-dotnet-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-encrypted-vm-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-encrypted-vm-bypass",
"run",
"re-encrypted-vm-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-entitlement-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-entitlement-bypass",
"run",
"re-entitlement-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida",
"run",
"re-frida"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-runtime",
"run",
"re-frida-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-frida-wine-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-frida-wine-runtime",
"run",
"re-frida-wine-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-fuzz-replay": {
"command": "uv",
"args": [
"--directory",
"/servers/re-fuzz-replay",
"run",
"re-fuzz-replay"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-game-ac-bypass": {
"command": "uv",
"args": [
"--directory",
"/servers/re-game-ac-bypass",
"run",
"re-game-ac-bypass"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-gdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-gdb",
"run",
"re-gdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ghidra-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ghidra-remote",
"run",
"re-ghidra-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-hypervisor-detect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-hypervisor-detect",
"run",
"re-hypervisor-detect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ida-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ida-remote",
"run",
"re-ida-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp",
"run",
"re-il2cpp"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-il2cpp-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-il2cpp-triage",
"run",
"re-il2cpp-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-injection-runtime": {
"command": "uv",
"args": [
"--directory",
"/servers/re-injection-runtime",
"run",
"re-injection-runtime"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-ios": {
"command": "uv",
"args": [
"--directory",
"/servers/re-ios",
"run",
"re-ios"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-kaitai": {
"command": "uv",
"args": [
"--directory",
"/servers/re-kaitai",
"run",
"re-kaitai"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-launch-and-observe": {
"command": "uv",
"args": [
"--directory",
"/servers/re-launch-and-observe",
"run",
"re-launch-and-observe"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-leak-scan": {
"command": "uv",
"args": [
"--directory",
"/servers/re-leak-scan",
"run",
"re-leak-scan"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-lief": {
"command": "uv",
"args": [
"--directory",
"/servers/re-lief",
"run",
"re-lief"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-llm-decompile": {
"command": "uv",
"args": [
"--directory",
"/servers/re-llm-decompile",
"run",
"re-llm-decompile"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-mitm2swagger": {
"command": "uv",
"args": [
"--directory",
"/servers/re-mitm2swagger",
"run",
"re-mitm2swagger"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-orchestrator": {
"command": "uv",
"args": [
"--directory",
"/servers/re-orchestrator",
"run",
"re-orchestrator"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch",
"run",
"re-patch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-patch-apply": {
"command": "uv",
"args": [
"--directory",
"/servers/re-patch-apply",
"run",
"re-patch-apply"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pcap": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pcap",
"run",
"re-pcap"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-pdb": {
"command": "uv",
"args": [
"--directory",
"/servers/re-pdb",
"run",
"re-pdb"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-qemu-antidetect": {
"command": "uv",
"args": [
"--directory",
"/servers/re-qemu-antidetect",
"run",
"re-qemu-antidetect"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-report-write": {
"command": "uv",
"args": [
"--directory",
"/servers/re-report-write",
"run",
"re-report-write"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-rizin": {
"command": "uv",
"args": [
"--directory",
"/servers/re-rizin",
"run",
"re-rizin"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-runtime-dump": {
"command": "uv",
"args": [
"--directory",
"/servers/re-runtime-dump",
"run",
"re-runtime-dump"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-speakeasy": {
"command": "uv",
"args": [
"--directory",
"/servers/re-speakeasy",
"run",
"re-speakeasy"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-target-fingerprint": {
"command": "uv",
"args": [
"--directory",
"/servers/re-target-fingerprint",
"run",
"re-target-fingerprint"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-traffic-capture": {
"command": "uv",
"args": [
"--directory",
"/servers/re-traffic-capture",
"run",
"re-traffic-capture"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triage": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triage",
"run",
"re-triage"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-triton": {
"command": "uv",
"args": [
"--directory",
"/servers/re-triton",
"run",
"re-triton"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vendor-anti-tamper": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vendor-anti-tamper",
"run",
"re-vendor-anti-tamper"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-bridge": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-bridge",
"run",
"re-vm-bridge"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-control": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-control",
"run",
"re-vm-control"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-debug": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-debug",
"run",
"re-vm-debug"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-decrypt": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-decrypt",
"run",
"re-vm-decrypt"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-launch": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-launch",
"run",
"re-vm-launch"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-memory": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-memory",
"run",
"re-vm-memory"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vm-ssh": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vm-ssh",
"run",
"re-vm-ssh"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-vtil": {
"command": "uv",
"args": [
"--directory",
"/servers/re-vtil",
"run",
"re-vtil"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-winedbg": {
"command": "uv",
"args": [
"--directory",
"/servers/re-winedbg",
"run",
"re-winedbg"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-x64dbg-remote": {
"command": "uv",
"args": [
"--directory",
"/servers/re-x64dbg-remote",
"run",
"re-x64dbg-remote"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara",
"run",
"re-yara"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"re-yara-author": {
"command": "uv",
"args": [
"--directory",
"/servers/re-yara-author",
"run",
"re-yara-author"
],
"env": {
"RE_AI_PLUGIN_ROOT": "",
"PYTHONUNBUFFERED": "1"
}
},
"ghidra-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ghidra-mcp",
"run",
"ghidra-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ida-pro-mcp": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/ida-pro-mcp",
"run",
"ida-pro-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"x64DbgMCPServer": {
"command": "uv",
"args": [
"--directory",
"/re-mcps/x64DbgMCPServer",
"run",
"x64dbg-mcp"
],
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"ssh-mcp": {
"command": "/vendored/ssh-mcp/ssh-mcp",
"args": [
"--storage",
"/data/ssh-mcp/storage.db"
],
"env": {}
},
"re-ui-automate": {
"command": "/vendored/touchpoint/.venv/bin/touchpoint-mcp",
"args": [],
"env": {
"PYTHONUNBUFFERED": "1",
"TOUCHPOINT_FALLBACK_INPUT": "true"
}
},
"re-persistproc": {
"command": "/vendored/persistproc/.venv/bin/persistproc",
"args": [
"serve",
"--port",
"8947"
],
"env": {
"PERSISTPROC_PORT": "8947"
}
}
}
}
+95
View File
@@ -0,0 +1,95 @@
# RE-AI Agent Guide
This document describes how AI agents compose the 31 per-MCP servers into workflows using the 29 skill definitions.
## Architecture
RE-AI is an **agent-space**: it doesn't run servers itself. It orchestrates per-MCP repos that are cloned at pinned versions. An agent interacting with RE-AI sees:
1. **31 MCP servers** registered in `.mcp.json` — each is a standalone tool surface
2. **29 skills** in `skills/` — each is a multi-step workflow that calls multiple servers
3. **5 catalogs** in `data/` — shared reference data consumed by servers and skills
## The per-MCP server pattern
Each server follows the same pattern:
```
re-<name>/
pyproject.toml # declares mcp[cli] + tool-specific deps
src/re_<name>/
__init__.py
__main__.py # entry: from server import main; main()
server.py # FastMCP app with @mcp.tool() functions
README.md
LICENSE
```
Server registration in `.mcp.json`:
```json
{
"re-<name>": {
"command": "uv",
"args": ["--directory", "${CLAUDE_PLUGIN_ROOT}/servers/re-<name>", "run", "re-<name>"]
}
}
```
## Skill structure
Each skill is a directory under `skills/` containing a `SKILL.md` with YAML frontmatter:
```yaml
---
name: re-static-triage
description: Triage a fresh binary against the technique catalog
tools: [re-catalog-match, re-triage, re-rizin, re-lief]
effect_envelope: read-only
test_cases:
- input: /path/to/sample.exe
expected: catalog match count > 0
---
```
### Skill categories
| Category | Skills | What they do |
|---|---|---|
| **Triage** | re-static-triage, re-dynamic-analysis, re-malware-triage, re-il2cpp-static-triage | Fast assessment of unknown binaries |
| **Analysis** | re-decompile, re-dotnet-analysis, re-vm-reverse, re-vuln-research, re-symbolic-exec | Deep reverse engineering |
| **Data** | re-format-decode, re-leak-scan, re-telemetry-extract, re-pcap-correlate | Data extraction and correlation |
| **Catalog** | re-yara-author, re-archive-author, re-report, re-drm-fingerprint | Tool authorship and reporting |
| **Bypass** | re-game-ac-bypass, re-eos-bypass, re-steam-stub-unwrap, re-origin-stub-drop | Protection analysis (detection, not circumvention) |
## Catalog data
The `data/` catalogs are shared across servers:
- `drm-indicators.yaml` — DRM / anti-tamper indicator mappings (pattern families, section shapes, string signatures)
- `anti-analysis-catalog.json` — anti-debug / anti-VM / anti-sandbox technique catalog
- `compiler-fingerprints.json` — compiler identification from binary heuristics
- `apkid-signatures.json` — APKiD packer / compiler signatures
- `ollvm-pass-catalog.json` — OLLVM obfuscation pass identification
## Composing workflows
A typical triage workflow:
1. Agent calls `re-triage.triage_target(path)` — runs RE-AI's static analysis primitives end-to-end
2. Agent calls `re-catalog-match.run_matcher(triage_json)` — matches triage against the catalog
3. Agent calls `re-report.write_report(matches, triage)` — generates a structured report
A typical decompile workflow:
1. Agent calls `re-rizin.disassemble_binary(path)` — gets disassembly
2. Agent calls `re-lief.parse_binary(path)` — gets PE/ELF structure
3. Agent calls `re-llm-decompile.decompile_function(address)` — LLM-assisted decompilation
## Version management
Servers are pinned in `versions.lock`. To update:
1. Edit `versions.lock`: bump the tag for the server you want
2. Run `./install.sh --update` to re-clone at the new tag
3. Run integration tests: `pytest tests/integration/`
4. If tests pass, the update is safe
+87
View File
@@ -0,0 +1,87 @@
# RE-AI
The agent-space monorepo that orchestrates the Heretek-RE per-MCP reverse-engineering toolchain. Contains 31 per-MCP server repos, 29 skill definitions, and shared catalog data.
## Structure
```
versions.lock # Per-server git tag pins (e.g., "re-capa v0.1.0")
install.sh # Clones all servers at pinned versions into servers/
.mcp.json # Registers all 31 MCP servers (reads from servers/)
skills/ # 29 SKILL.md files (agent workflows)
re-static-triage/ # Workflow: fresh binary → catalog match → plan
re-dynamic-analysis/ # Workflow: Frida attach → hook → decrypt
re-decompile/ # Workflow: Ghidra/IDA decompile → IL lift
re-malware-triage/ # Workflow: sample → YARA + capa + anti-analysis
... # (29 total)
data/ # Shared catalogs
anti-analysis-catalog.json
apkid-signatures.json
compiler-fingerprints.json
drm-indicators.yaml
ollvm-pass-catalog.json
servers/ # [git-ignored] Cloned per-MCP repos (populated by install.sh)
pyproject.toml # Integration test runner
tests/integration/ # Cross-server integration tests
```
## Quick start
```bash
git clone https://github.com/Heretek-RE/RE-AI.git
cd RE-AI
./install.sh # clones 31 per-MCP servers into servers/
```
## Build commands
### Install
```bash
./install.sh # clone all servers at pinned versions
./install.sh --update # re-clone at updated tags (edit versions.lock first)
./install.sh --clean # remove all cloned servers
```
### Test
```bash
pip install -e ".[dev]"
pytest tests/integration/
```
### Individual server (standalone)
```bash
cd servers/re-capa
pip install -e .
re-capa # starts MCP server on stdio
```
## How it works
1. `versions.lock` pins each per-MCP repo to a git tag
2. `install.sh` does `git clone --depth 1 --branch <tag>` for each
3. `.mcp.json` registers all 31 servers with Claude Code (reads from `servers/`)
4. `skills/` contains agent workflow definitions that compose the MCP tools
5. `data/` contains shared catalogs referenced by multiple servers
## Version management
Edit `versions.lock` to bump a server, then re-run `./install.sh --update`:
```
re-capa v0.2.0 # bumped
re-angr v0.1.0 # unchanged
```
## Conventions
- **Per-MCP repos** are independent: each has its own `pyproject.toml`, `src/`, tests, LICENSE
- **Skills** are agent-context files (SKILL.md), not executable code
- **Catalog data** is shared across servers; edits in `data/` affect the catalog matchers
- **No engagement references** in any committed file: no game titles, SOW codes, host paths, or per-target output paths
## Key files
- `versions.lock` — the version manifest for all 31 servers
- `install.sh` — the installer
- `.mcp.json` — the MCP server registry
- `README.md` — the public-facing overview
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Heretek-RE contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+63
View File
@@ -0,0 +1,63 @@
# RE-AI
The agent-space monorepo that orchestrates the Heretek-RE per-MCP reverse-engineering toolchain.
RE-AI does not contain server code itself. It contains:
- **`versions.lock`** — pins each per-MCP server to a specific git tag.
- **`install.sh`** — clones all 31 per-MCP servers at their pinned versions into `servers/`.
- **`.mcp.json`** — registers all 31 MCP servers with Claude Code (reads from `servers/`).
- **`skills/`** — 29 Claude Code skill definitions that compose the MCP tools into workflows.
- **`data/`** — shared catalog data (DRM indicators, compiler fingerprints, APKiD signatures, anti-analysis catalog, OLLVM pass catalog).
## Quick start
```bash
git clone https://github.com/Heretek-RE/RE-AI.git
cd RE-AI
./install.sh # clones 31 per-MCP servers into servers/
```
Then register `.mcp.json` with your Claude Code session.
## Per-MCP servers
Each server is an independent repo at `https://github.com/Heretek-RE/re-<name>`. You can also use any server standalone without the agent-space orchestration:
```bash
# Standalone usage (any server)
git clone https://github.com/Heretek-RE/re-capa.git
cd re-capa
uv run re-capa # starts the MCP server on stdio
```
See each server's README for its `.mcp.json` one-liner.
## Version management
Edit `versions.lock` to pin a server to a specific tag, then re-run `./install.sh --update`:
```
re-capa v0.2.0 # bumped to a newer release
re-angr v0.1.0 # unchanged
```
## Architecture
```
RE-AI/
├── .mcp.json # MCP server registry (reads from servers/)
├── versions.lock # per-server tag pins
├── install.sh # installer (git clone --depth 1 --branch <tag>)
├── skills/ # 29 SKILL.md files (agent workflows)
├── data/ # shared catalogs (drm-indicators, compiler-fingerprints, ...)
├── servers/ # [git-ignored] cloned per-MCP repos
├── tests/integration/ # cross-server integration tests
├── pyproject.toml # for integration test runner
├── LICENSE # MIT
└── README.md
```
## License
MIT. The per-MCP servers carry their own licenses (see each server's LICENSE file).
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
See [Heretek-RE Security Policy](https://github.com/Heretek-RE/.github/blob/main/SECURITY.md) for the full policy.
## Reporting
Use [GitHub Security Advisories](https://github.com/Heretek-RE/RE-AI/security/advisories/new) to report vulnerabilities privately. We acknowledge receipt within 7 days and aim to issue a fix or mitigation within 30 days for high-severity issues.
@@ -0,0 +1,407 @@
{
"_meta": {
"version": 2,
"last_updated": "2026-06-07",
"purpose": "Vendored, normalized subset of public anti-analysis primitive APIs + byte sequences used by the re-anti-analysis MCP server. Vendor-neutral: categories only, no product or vendor names.",
"arch_default": ["x86", "x86_64"],
"arch_default_note": "A15 fix (v2.8.0): every entry is implicitly scoped to x86/x86_64 unless an explicit `archs` field overrides it. The disasm pass in re-anti-analysis must skip x86-only entries when the target binary is AArch64/ARM32/etc. (the r03-stress APK pass produced 306 false-positive INT-3 hits on libproot.so/libzstd-jni because the x86 byte-pattern catalog was applied to AArch64 binaries).",
"category_legend": {
"anti_debug": "Primitives the binary uses to detect or interfere with debuggers (user-mode only).",
"anti_attach": "Primitives that prevent or detect a debugger attaching to the running process.",
"anti_vm": "Primitives the binary uses to detect a virtual machine (CPUID leaves, hypervisor bits, ACPI/SMBIOS probes).",
"anti_emulator": "Primitives that detect a CPU emulator (QEMU, Unicorn, Speakeasy) by timing skew, missing CPUID leaves, or behavior.",
"anti_sandbox": "Primitives that detect a sandbox (CAPE, Triage, Joe Sandbox) by file/process/registry/user-interaction probes.",
"process_introspection": "Primitives that read the host's process list, handle table, or module list.",
"memory_integrity": "Primitives that read or verify the integrity of the process's own memory (text-section hashing, self-checksumming).",
"code_integrity": "Primitives that install hook detection, IAT patching detection, or inline-hook detection."
},
"evidence_legend": {
"api_name": "API name appears in the binary's string table (string_only confirmation per anti_debug_indicators).",
"import": "API is in the IAT (import_only confirmation).",
"byte_sequence": "A canonical byte sequence (e.g. 0F 31 for RDTSC). Requires disasm to count.",
"symbol": "A debug symbol or string literal (e.g. /prodid, MachineGuid)."
},
"source_credits": [
"https://github.com/fr0gger/unprotect (open catalog of anti-analysis APIs)",
"https://github.com/CheckPointSW/Anti-Debug-DB (machine-readable Windows anti-debug DB)",
"https://github.com/kernelwernel/VMAware (cross-platform VM-detection primitives)",
"https://github.com/void-stack/Hypervisor-Detection (Windows hypervisor-detection primitives)"
]
},
"entries": [
{
"id": "ad-001",
"category": "anti_debug",
"primitive": "IsDebuggerPresent",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "PEB.BeingDebugged reader; the canonical user-mode anti-debug primitive. Single-primitive match in isolation is high-FP (debug-build heuristics); requires co-occurrence with another ad-* primitive to be a real signal."
},
{
"id": "ad-002",
"category": "anti_debug",
"primitive": "CheckRemoteDebuggerPresent",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Cross-process debugger detection via kernel32."
},
{
"id": "ad-003",
"category": "anti_debug",
"primitive": "NtQueryInformationProcess",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Used with ProcessDebugPort / ProcessDebugObjectHandle / ProcessDebugFlags to detect debug attach."
},
{
"id": "ad-004",
"category": "anti_debug",
"primitive": "OutputDebugStringW",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Often used as a debugger-attach tripwire (debugger receives the string; non-debugger context silently swallows it)."
},
{
"id": "ad-005",
"category": "anti_debug",
"primitive": "RDTSC",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F 31",
"platforms": [
"x86",
"x86_64"
],
"note": "Read time-stamp counter. High-FP in isolation (legit timing code); counted as anti-debug only when paired with a spin loop and a debugger-presence check."
},
{
"id": "ad-006",
"category": "anti_debug",
"primitive": "INT 2D",
"evidence_kind": "byte_sequence",
"byte_sequence": "CD 2D",
"platforms": [
"x86",
"x86_64"
],
"note": "Debug exception that fires only under a debugger (the kernel delivers DBG_PRINTEXCEPTION_C). The native code path is a no-op; the debugger path increments RIP past the next instruction."
},
{
"id": "ad-007",
"category": "anti_debug",
"primitive": "INT 3",
"evidence_kind": "byte_sequence",
"byte_sequence": "CC",
"platforms": [
"x86",
"x86_64"
],
"note": "Software breakpoint opcode. Patched into target by debuggers; presence in a code path that is *not* a known debug stub is a strong anti-debug signal."
},
{
"id": "ad-008",
"category": "anti_debug",
"primitive": "PEB.BeingDebugged",
"evidence_kind": "byte_sequence",
"byte_sequence": "64 A1 30 00 00 00",
"platforms": [
"x86"
],
"note": "Direct read of the PEB.BeingDebugged byte at fs:[0x30] + 2. Common in encrypted-VM dispatchers that want to avoid importing IsDebuggerPresent."
},
{
"id": "ad-009",
"category": "anti_debug",
"primitive": "PEB.NtGlobalFlag",
"evidence_kind": "byte_sequence",
"byte_sequence": "64 8B 05 30 00 00 00 83 78 68 70",
"platforms": [
"x86"
],
"note": "Direct read of the PEB.NtGlobalFlag dword (fs:[0x30] + 0x68). Set by the kernel to 0x70 when the process is created under a debugger."
},
{
"id": "ad-010",
"category": "anti_debug",
"primitive": "DebugActiveProcess",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Anti-anti-debug side: enumerate debuggers attached by walking the process-debug-object handle table."
},
{
"id": "ad-011",
"category": "anti_attach",
"primitive": "NtCreateDebugObject",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Create the debug object that NtDebugActiveProcess requires. If the call returns a non-NULL handle, the process is being debugged."
},
{
"id": "ad-012",
"category": "anti_attach",
"primitive": "NtDebugContinue",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Resume from a debug exception without yielding control to the debugger. High-FP in isolation."
},
{
"id": "av-001",
"category": "anti_vm",
"primitive": "CPUID.1.ECX hypervisor present bit",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F A2",
"platforms": [
"x86",
"x86_64"
],
"note": "CPUID leaf 1 ECX bit 31 = 1 when running under a hypervisor. Cross-platform standard VM-detection primitive."
},
{
"id": "av-002",
"category": "anti_vm",
"primitive": "CPUID.0x40000000 hypervisor vendor signature",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F A2",
"platforms": [
"x86",
"x86_64"
],
"note": "CPUID leaf 0x40000000 returns the hypervisor vendor signature (e.g. 'Microsoft Hv', 'VMwareVMware', 'KVMKVMKVM', 'TCGTCGTCGTCG'). Note: signature strings are excluded from this catalog; only the opcode is recorded."
},
{
"id": "av-003",
"category": "anti_vm",
"primitive": "RDTSC timing skew",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F 31",
"platforms": [
"x86",
"x86_64"
],
"note": "TSC skew between two back-to-back RDTSCs is a strong VM-detection signal (emulators and some hypervisors introduce 10-100x slowdown on the second read)."
},
{
"id": "av-004",
"category": "anti_vm",
"primitive": "VMXON/VMCALL instruction presence",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F C7",
"platforms": [
"x86_64"
],
"note": "VMXON and VMCALL are ring 0 in real hardware. Their presence in userland code is a hypervisor-detection or hypervisor-deployment primitive (anti-RE/anti-cheat use case)."
},
{
"id": "av-005",
"category": "anti_vm",
"primitive": "SMBIOS string probe",
"evidence_kind": "symbol",
"platforms": [
"windows",
"linux"
],
"note": "Reads SMBIOS tables and looks for vendor strings (e.g. 'VMware', 'QEMU', 'Microsoft Corporation'). Note: signature strings are excluded from this catalog; only the SYMBOL token is recorded."
},
{
"id": "av-006",
"category": "anti_vm",
"primitive": "ACPI table probe",
"evidence_kind": "symbol",
"platforms": [
"windows",
"linux"
],
"note": "Reads ACPI tables (\\\\.\\\\ACPI#...) to detect virtual hardware. Often paired with SMBIOS probe (av-005)."
},
{
"id": "av-007",
"category": "anti_vm",
"primitive": "\\\\.\\PhysicalDrive access",
"evidence_kind": "symbol",
"platforms": [
"windows"
],
"note": "Open \\\\.\\\\PhysicalDrive0 to read disk serial. The disk-serial string is one of the canonical VM-fingerprinting primitives (the VM's serial is often a known constant)."
},
{
"id": "av-008",
"category": "anti_vm",
"primitive": "HKLM\\\\SOFTWARE\\\\VMware registry probe",
"evidence_kind": "symbol",
"platforms": [
"windows"
],
"note": "Reads HKLM\\\\SOFTWARE\\\\VMware to detect VMware Tools / VMware hypervisor. Note: this is the Windows-side catalog of a single hypervisor; the analyst's job is to map the category to the *current* target."
},
{
"id": "av-009",
"category": "anti_vm",
"primitive": "MAC address vendor prefix probe",
"evidence_kind": "symbol",
"platforms": [
"windows",
"linux"
],
"note": "Reads the NIC's MAC address and checks against the IEEE OUI database for known VM-assigned prefixes (00:0C:29 = VMware, 00:1C:14, 08:00:27 = QEMU/KVM). Note: OUI prefixes are excluded from this catalog; only the SYMBOL token is recorded."
},
{
"id": "ae-001",
"category": "anti_emulator",
"primitive": "Sleep(INFINITE) blocking",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Sleep(INFINITE) hangs the emulator (no timeout). Anti-emulator primitive; in a real process the call returns when the system is shut down."
},
{
"id": "ae-002",
"category": "anti_emulator",
"primitive": "CPUID.0x40000100 leaf missing",
"evidence_kind": "byte_sequence",
"byte_sequence": "0F A2",
"platforms": [
"x86",
"x86_64"
],
"note": "Reads CPUID leaf 0x40000100; QEMU and Unicorn return 0 in EAX/EBX/ECX/EDX, real hardware returns the leaf-specific value. Cheap anti-emulator primitive."
},
{
"id": "ae-003",
"category": "anti_emulator",
"primitive": "GetModuleHandle(\"ntdll.dll\") IAT check",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Iterate the IAT of ntdll.dll and look for hooks. Emulators often present a stub IAT; real Windows has the full set."
},
{
"id": "as-001",
"category": "anti_sandbox",
"primitive": "GetCursorPos / GetForegroundWindow inactivity check",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Sleep + read cursor position; if unchanged after a long sleep, the process is in a sandbox (no human interaction)."
},
{
"id": "as-002",
"category": "anti_sandbox",
"primitive": "Cuckoo / CAPE pipe detection",
"evidence_kind": "symbol",
"platforms": [
"windows"
],
"note": "Looks for the Cuckoo / CAPE pipe path. Note: the Cuckoo path string is excluded from this catalog; only the SYMBOL token is recorded."
},
{
"id": "as-003",
"category": "anti_sandbox",
"primitive": "username / computername short-string check",
"evidence_kind": "symbol",
"platforms": [
"windows"
],
"note": "Reads the username + computername via GetUserNameW + GetComputerNameW and checks against a short whitelist of known sandbox names."
},
{
"id": "as-004",
"category": "anti_sandbox",
"primitive": "Sleep + low-res timer",
"evidence_kind": "api_name",
"platforms": [
"windows",
"linux"
],
"note": "Calls NtDelayExecution / usleep and checks that the elapsed wall-clock time matches the requested sleep. Sandboxes often fast-forward sleeps."
},
{
"id": "pi-001",
"category": "process_introspection",
"primitive": "CreateToolhelp32Snapshot / Process32First",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Enumerate the process list. The anti-analysis use is to look for analysis-tool process names."
},
{
"id": "pi-002",
"category": "process_introspection",
"primitive": "NtQuerySystemInformation(SystemProcessInformation)",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Native API equivalent of CreateToolhelp32Snapshot. The encrypted-VM dispatcher uses this to walk the process list looking for debuggers and analysis tools."
},
{
"id": "pi-003",
"category": "process_introspection",
"primitive": "EnumProcessModules / GetModuleFileNameEx",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Enumerate loaded modules in a target process. Anti-analysis use is to look for analysis-tool DLL names."
},
{
"id": "pi-004",
"category": "process_introspection",
"primitive": "NtQueryVirtualMemory",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Walk a process's address space. Encrypted-VM packers use this to find the unpacked code region after the lazy-decrypt stub runs."
},
{
"id": "mi-001",
"category": "memory_integrity",
"primitive": "text-section SHA-256 at runtime",
"evidence_kind": "symbol",
"platforms": [
"windows",
"linux"
],
"note": "Hashes the .text section at runtime and compares to a known-good value. Encrypted-VM packers use this to detect in-memory patching."
},
{
"id": "ci-001",
"category": "code_integrity",
"primitive": "IAT walk + compare to file",
"evidence_kind": "api_name",
"platforms": [
"windows"
],
"note": "Walk the IAT at runtime; if a function pointer doesn't match the file offset, the IAT has been hooked."
},
{
"id": "ci-002",
"category": "code_integrity",
"primitive": "first-byte-of-function check",
"evidence_kind": "byte_sequence",
"byte_sequence": "E9",
"platforms": [
"x86",
"x86_64"
],
"note": "Read the first byte of every function in the IAT; if it's 0xE9 (JMP rel32) or 0x68 0x?? 0x?? 0x?? 0x?? (PUSH imm32), the function has been hooked."
}
]
}
+128
View File
@@ -0,0 +1,128 @@
{
"_meta": {
"version": 1,
"last_updated": "2026-06-06",
"purpose": "Vendored subset of public APK protection detection signatures (DEX, native, manifest). Used by the re-apktool classify_apk_protection tool. Vendor-neutral: only category labels, no product / publisher / app names.",
"category_legend": {
"apk_packager": "The APK was assembled by a 3rd-party packager that obscures the original package layout.",
"dex_obfuscator": "The DEX classes were processed by a Java/Kotlin obfuscator (name-rename + control-flow flatten + string-encrypt).",
"native_vm_bytecode_interpreter": "A native .so was wrapped by an encrypted-VM bytecode interpreter; classes.dex is a thin shim.",
"anti_tamper_runtime": "A native .so contains a runtime integrity / debugger / emulator detection surface.",
"string_encryption": "The DEX string pool was processed by a string-encryptor that the consumer must decrypt before reading.",
"resource_encryption": "The APK resources were processed by a resource-encryptor; resources.arsc is opaque without the matching decryptor.",
"anti_debug_native": "A native .so contains anti-debug primitives (ptrace self-attach, /proc/status walk, signal-mask probe).",
"anti_frida_native": "A native .so contains Frida-gadget detection (port scan, library walk, dlsym lookup).",
"root_detection": "The APK or its native libs probe for Magisk / SuperSU / su binary.",
"ssl_pinning": "The network code uses OkHttp / TrustManager / NetworkSecurityConfig to pin certificates."
},
"evidence_legend": {
"class_name": "DEX class with a non-obfuscated name (the obfuscator's seed names tend to be a single letter or symbol).",
"method_signature": "DEX method signature with a known-encrypted prefix.",
"string_literal": "DEX or native lib contains a string literal that is the canonical signal.",
"native_section": "A .so has a section name (or absence of expected section) that signals the protection class.",
"manifest_meta": "The AndroidManifest.xml has a meta-data tag that the packager / obfuscator adds.",
"file_path": "An APK file is at a path that signals the protection class (e.g. /lib/.../lib<packager-name>.so)."
},
"source_credits": [
"https://github.com/rednaga/APKiD (APKiD open signatures for DEX/native/manifest protection detection)"
]
},
"entries": [
{
"id": "apk-001",
"category": "apk_packager",
"evidence_kind": "manifest_meta",
"match_pattern": "application-meta-data: packagername",
"platforms": ["android"],
"note": "Meta-data tag in AndroidManifest.xml that a known APK packager adds. Multiple packager tags on one APK = the APK was re-packed."
},
{
"id": "apk-002",
"category": "dex_obfuscator",
"evidence_kind": "class_name",
"match_pattern": "single-letter class names (a, b, c, aa, ab, ...)",
"platforms": ["android"],
"note": "Class names in classes.dex that are 1-2 letters are a strong obfuscator signal. Counter-example: some legitimate frameworks also use 1-2 letter names (kotlin.coroutines.intrinsics); review the package prefix."
},
{
"id": "apk-003",
"category": "dex_obfuscator",
"evidence_kind": "method_signature",
"match_pattern": "method bodies with only 1-3 instructions + a single call to a static decrypt stub",
"platforms": ["android"],
"note": "Method bodies that are just 'invoke-static decrypt-stub' are a control-flow-flattened string-decrypt pattern."
},
{
"id": "apk-004",
"category": "string_encryption",
"evidence_kind": "class_name",
"match_pattern": "presence of a 'Decrypt' or 'd' class with a single static 'd(String)' method",
"platforms": ["android"],
"note": "The canonical string-decrypt stub. The obfuscator rewrites every string-literal load as a call to this method."
},
{
"id": "apk-005",
"category": "native_vm_bytecode_interpreter",
"evidence_kind": "native_section",
"match_pattern": "lib<obfuscated>.so with a non-standard .text section (small, low entropy) and a large .data or .bss (high entropy)",
"platforms": ["android"],
"note": "Native encrypted-VM bytecode interpreter shape: the interpreter + dispatcher are in .text (small, normal entropy); the encrypted handlers are in .data / .bss (large, high entropy). The .init_array entry point is the lazy-decrypt stub."
},
{
"id": "apk-006",
"category": "anti_tamper_runtime",
"evidence_kind": "string_literal",
"match_pattern": "native lib contains the strings 'frida', 'gadget', 'gmain'",
"platforms": ["android"],
"note": "Strings that indicate the native lib is searching for Frida gadget or frida-server. Note: these strings are not in the catalog (they are matched at runtime); the catalog only describes the *category*."
},
{
"id": "apk-007",
"category": "anti_debug_native",
"evidence_kind": "string_literal",
"match_pattern": "native lib contains '/proc/self/status' and 'TracerPid'",
"platforms": ["android"],
"note": "The canonical Linux anti-debug primitive: read /proc/self/status, look for 'TracerPid: 0'. Used by native-side anti-debug in modern Android apps."
},
{
"id": "apk-008",
"category": "anti_frida_native",
"evidence_kind": "string_literal",
"match_pattern": "native lib contains 'LIBFRIDA' or 'frida-agent'",
"platforms": ["android"],
"note": "Native-side Frida detection: scan /proc/self/maps for the frida-agent library, scan TCP for the default 27042 port."
},
{
"id": "apk-009",
"category": "root_detection",
"evidence_kind": "string_literal",
"match_pattern": "DEX contains 'su', 'Magisk', 'Superuser'",
"platforms": ["android"],
"note": "Strings that indicate the app probes for root: check for the su binary, the Magisk mount, the Superuser.apk package. High FP in apps that legitimately interact with root (e.g. system tools)."
},
{
"id": "apk-010",
"category": "ssl_pinning",
"evidence_kind": "string_literal",
"match_pattern": "DEX contains 'OkHttp' and 'CertificatePinner'",
"platforms": ["android"],
"note": "OkHttp CertificatePinner is the canonical SSL-pinning pattern. The bypass is to override the pin check in OkHttp's CertificatePinner class."
},
{
"id": "apk-011",
"category": "ssl_pinning",
"evidence_kind": "manifest_meta",
"match_pattern": "networkSecurityConfig meta-data",
"platforms": ["android"],
"note": "The AndroidManifest declares a networkSecurityConfig XML that pins certs. The bypass is to repackage the APK with an empty networkSecurityConfig."
},
{
"id": "apk-012",
"category": "resource_encryption",
"evidence_kind": "string_literal",
"match_pattern": "resources.arsc is opaque, .arsc decryption stub in DEX",
"platforms": ["android"],
"note": "The .arsc is encrypted; the obfuscator's decrypt stub must be called before the resources can be read. Pair with apk-004 (string-encryption stub)."
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
{
"_meta": {
"version": 1,
"last_updated": "2026-06-06",
"purpose": "Compiler fingerprint catalog used by the re-rizin list_functions_with_metadata tool to attach a 'compiled_by' hint to each function. The hints are observed patterns (RSDS debug signature, .CRT section prefix, mangled-name scheme) — no product names.",
"compiler_categories": {
"msvc": "Functions compiled with Microsoft's MSVC toolchain. The .CRT section prefix is .CRT$XC; mangled names start with ?@; debug info is the standard PDB CodeView.",
"gcc": "Functions compiled with the GNU Compiler Collection. The .CRT section prefix is .init_array; mangled names use the Itanium ABI (_Z); debug info is DWARF.",
"clang": "Functions compiled with Clang/LLVM. The .CRT section prefix is .init_array; mangled names use the Itanium ABI; debug info is DWARF. Distinguishing from GCC requires the build-id or the presence of Clang's __cxxabiv1 namespace imports.",
"icc": "Functions compiled with Intel's ICC. Distinguishing from MSVC requires the Intel-specific .CRT section (.CRT$XC with __intel_* symbols).",
"rustc": "Functions compiled with rustc. The mangled names start with _ZN; the .CRT section is .init_array; the function name pattern is hash-suffixed (a Rust symbol like '_ZN3std2io5Write9write_all17h8c2a3b1e9d4a5b6E' is rustc-stable).",
"go": "Functions compiled with the Go toolchain. The mangled names contain '..func' or '..stkobj' separators; the .gopclntab section is unique to Go.",
"delphi": "Functions compiled with Embarcadero Delphi. The mangled names contain '@<unit>@<symbol>$qqrv' or similar patterns; the .itext / .data section prefix is Delphi-specific."
},
"evidence_legend": {
"section_prefix": "The .CRT / .init section name prefix is the canonical signal.",
"mangled_prefix": "The first 3 characters of the demangled name is the canonical signal.",
"debug_format": "The presence of CodeView (RSDS) vs DWARF debug info is a signal."
}
},
"fingerprints": [
{
"id": "cf-msvc",
"compiler": "msvc",
"evidence_kind": "section_prefix",
"match_pattern": "section name matches /^\\.CRT\\$XC[A-Z]+$/",
"note": "Canonical MSVC .CRT section. The '$' + single uppercase letter is the MSVC convention for ordered init sections."
},
{
"id": "cf-msvc-mangle",
"compiler": "msvc",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name starts with '?@' or '??'",
"note": "MSVC mangled-name scheme. Functions exported from a .dll start with '?@'; C++ type info starts with '??'."
},
{
"id": "cf-gcc-section",
"compiler": "gcc",
"evidence_kind": "section_prefix",
"match_pattern": "section name matches /^\\.init_array$/ or /^\\.fini_array$/",
"note": "Canonical GCC .init_array / .fini_array. Shared with Clang; distinguishing requires the .debug_info build-id."
},
{
"id": "cf-gcc-mangle",
"compiler": "gcc",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name starts with '_Z'",
"note": "Itanium C++ ABI mangled-name scheme. Shared with Clang and rustc; distinguishing requires the hash-suffix style."
},
{
"id": "cf-clang-marker",
"compiler": "clang",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name contains '__cxxabiv1'",
"note": "Clang's libcxxabi import marker. Distinguishes Clang from GCC when the .init_array section is ambiguous."
},
{
"id": "cf-icc-symbol",
"compiler": "icc",
"evidence_kind": "section_prefix",
"match_pattern": "section name contains '__intel_' or '__intel_ia32'",
"note": "ICC's .CRT section contains __intel_* initialization symbols. Distinguishes ICC from MSVC."
},
{
"id": "cf-rustc-mangle",
"compiler": "rustc",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name matches /^_ZN[a-z0-9_]+[0-9]+[a-zA-Z0-9]{16}E$/",
"note": "rustc stable mangled-name scheme. The 16-character hex suffix is a TypeName hash that rustc uses to dedup symbol names. Distinguishes rustc from C++ Clang/GCC."
},
{
"id": "cf-go-section",
"compiler": "go",
"evidence_kind": "section_prefix",
"match_pattern": "section name matches /^\\.gopclntab$/",
"note": "Go's program-counter-line-number table. Unique to Go binaries; presence confirms a Go target."
},
{
"id": "cf-go-mangle",
"compiler": "go",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name contains '..func' or '..stkobj' or '..stk_'",
"note": "Go's symbol-naming scheme uses '..' as a category separator. The '..func' separator is the most common in a Go binary."
},
{
"id": "cf-delphi-mangle",
"compiler": "delphi",
"evidence_kind": "mangled_prefix",
"match_pattern": "demangled name matches /@[A-Za-z0-9_]+@[A-Za-z0-9_]+\\$[a-z0-9]+/",
"note": "Delphi's mangled-name scheme. The '@UnitName@MethodName$qqrv' pattern is the canonical signal. Pairs with the .itext / .data section prefix."
},
{
"id": "cf-delphi-section",
"compiler": "delphi",
"evidence_kind": "section_prefix",
"match_pattern": "section name matches /^(itext|odata|tls|data|bss)$/",
"note": "Delphi's standard section set. The 'itext' section is the canonical signal — no other compiler uses it."
}
]
}
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
{
"_meta": {
"version": 1,
"last_updated": "2026-06-06",
"purpose": "The OLLVM / d810-ng simplification pass catalog. The re-vtil simplify_lifted_il tool takes a pass list and applies it to a lifted IL tree. Vendor-neutral: pass names are the public d810-ng / VTIL pass enum values, no product names.",
"pass_categories": {
"dead_store_elimination": "Remove stores to a memory location that is overwritten before any read.",
"branch_folding": "Fold branches whose condition is provably constant after a previous pass.",
"mem_dependency": "Reorder memory accesses to expose dataflow that the prior passes missed.",
"mba_fold": "Replace mixed boolean-arithmetic identities with the simpler form. Sources the re-mba-deobfuscate skill's mba_patterns.identities table from drm-indicators.yaml.",
"constant_folding": "Fold arithmetic on constants into a single constant.",
"opaque_predicate_eval": "Evaluate predicates whose truth is provable from prior dataflow (e.g. `x*x >= 0` for signed x).",
"string_decrypt": "Recognize the `ldstr -> call Get<name>(); ret` pattern in .NET IL and replace with the literal string. (Pairs with re-dotnet's run_il_simplification tool.)",
"control_flow_unflatten": "Unflatten switch-dispatcher state machines. The d810-ng canonical pass."
},
"source_credits": [
"https://github.com/w00tzenheimer/d810-ng (the d810-ng Ghidra plugin)",
"https://github.com/HikariObfuscator/Hikari (the OLLVM fork d810-ng targets)",
"https://github.com/can1357/NoVmp (VM-pack deobfuscation; some passes ported from here)"
],
"default_pass_order": [
"constant_folding",
"dead_store_elimination",
"branch_folding",
"mem_dependency",
"mba_fold",
"opaque_predicate_eval",
"control_flow_unflatten"
]
},
"passes": [
{
"id": "p-dse",
"name": "dead_store_elimination",
"category": "dead_store_elimination",
"description": "Remove stores to a memory location that is overwritten before any read.",
"vt_il_primitive": "stores",
"triggers": ["Always safe; always first pass."],
"pairs_with": ["branch_folding", "constant_folding"]
},
{
"id": "p-bf",
"name": "branch_folding",
"category": "branch_folding",
"description": "Fold branches whose condition is provably constant after a previous pass.",
"vt_il_primitive": "if",
"triggers": ["After constant_folding has simplified the branch condition."],
"pairs_with": ["dead_store_elimination", "mba_fold"]
},
{
"id": "p-md",
"name": "mem_dependency",
"category": "mem_dependency",
"description": "Reorder memory accesses to expose dataflow that the prior passes missed.",
"vt_il_primitive": "mem,store,load",
"triggers": ["After dead_store_elimination."],
"pairs_with": ["mba_fold"]
},
{
"id": "p-mba",
"name": "mba_fold",
"category": "mba_fold",
"description": "Replace mixed boolean-arithmetic identities with the simpler form.",
"vt_il_primitive": "add,sub,xor,or,and",
"triggers": ["When the re-mba-deobfuscate skill's identities table has fired on the same function."],
"pairs_with": ["branch_folding", "constant_folding"]
},
{
"id": "p-cf",
"name": "constant_folding",
"category": "constant_folding",
"description": "Fold arithmetic on constants into a single constant.",
"vt_il_primitive": "add,sub,imul",
"triggers": ["Always safe; always first pass."],
"pairs_with": ["branch_folding"]
},
{
"id": "p-ope",
"name": "opaque_predicate_eval",
"category": "opaque_predicate_eval",
"description": "Evaluate predicates whose truth is provable from prior dataflow.",
"vt_il_primitive": "if",
"triggers": ["When the predicate references only known-constant operands (e.g. x*x, (x | 1))."],
"pairs_with": ["branch_folding"]
},
{
"id": "p-sd",
"name": "string_decrypt",
"category": "string_decrypt",
"description": "Recognize the ldstr -> call Get<name>(); ret pattern and replace with the literal string.",
"vt_il_primitive": "call,ret",
"triggers": ["Only applies to .NET IL lifted to VTIL."],
"pairs_with": []
},
{
"id": "p-cfu",
"name": "control_flow_unflatten",
"category": "control_flow_unflatten",
"description": "Unflatten switch-dispatcher state machines.",
"vt_il_primitive": "jmp,switch",
"triggers": ["When a switch-dispatcher pattern is detected (a single basic block with high in-degree + a state-variable update + a switch on the state)."],
"pairs_with": ["mba_fold", "branch_folding"]
}
]
}
@@ -0,0 +1,32 @@
name: anti-debug-patch-apply
source: skills/re-anti-debug-patch-apply/SKILL.md
version: 0.3.0
when_to_use: Catalog match returned anti-debug entries (rdtsc-timing-trap, cpuid-hypervisor-leaf,
int2d, int3, vmcall, vmxon). Operator wants to actually apply the patch plan.
target_class: Any binary with anti-debug primitives (FM26, HKIA, LIR, P3R, CD, TWW3,
007FL)
catalog_entry: n/a (workflow)
expected_runtime_minutes: 5
success_probability: 0.85
tools:
- re-anti-debug-patch
- re-patch-apply
- re-speakeasy
steps:
- step: 1
tool: re-anti-debug-patch
action: patch_target with strategies=zero
purpose: generate the per-primitive patch plan
- step: 2
tool: re-patch-apply
action: apply_patch with the patch plan
purpose: actually apply the patches to a copy of the binary; write per-site patch
log (closes G5)
- step: 3
tool: re-speakeasy
action: run the patched binary under the emulator
purpose: verify no SIGABRT
- step: 4
tool: re-catalog-match
action: re-match the patched binary
purpose: confirm the Bypass worked (the anti-debug entries should no longer match)
@@ -0,0 +1,28 @@
name: c-injection-build
source: skills/re-c-injection-build/SKILL.md
version: 0.3.0
when_to_use: Target has hardened anti-Frida checks. Operator wants in-process hooks
(not Frida).
target_class: Denuvo ATD + EAC/BE kernel drivers + proprietary anti-RE wrappers (007FL,
P3R Denuvo, CD BlackSpace)
catalog_entry: n/a (workflow)
expected_runtime_minutes: 10
success_probability: 0.9
tools:
- re-c-injection-build
- re-runtime-dump (--mode=inject)
steps:
- step: 1
tool: re-c-injection-build
action: build_injection_library target_os=linux
purpose: compile the .so with the inline-trampoline hook engine + IAT/GOT override
+ Unix-socket IPC
- step: 2
tool: re-c-injection-build
action: build_injection_library target_os=windows
purpose: compile the .dll (requires mingw)
- step: 3
tool: re-runtime-dump
action: dump_target --mode=inject
purpose: actually inject the .so/.dll into the target + install the hooks + capture
the dumps
@@ -0,0 +1,105 @@
name: ca-warscape-eos
source: docs/PLAYBOOKS/ca-warscape-eos.md
version: 0.2.0
target_class: 'Creative Assembly Warscape / Empire engine titles with Epic Online
Services overlay. Example: **Total War Warhammer III v7.2.1** (TWW3).'
expected_runtime: 90 minutes (the EOS bypass is trivial; the CA Warscape bypass is
medium)
success_probability: 0.6
tools:
- re-runtime-dump
steps:
- number: 1
title: Bypass the EOS overlay (the easy part)
- number: 2
title: Confirm the CA Warscape light VM
- number: 3
title: Decrypt the .pack archives via libled.dll
- number: 4
title: Write a Kaitai .ksy for the .pack format
- number: 5
title: Apply the .ksy to data/data.pack
- number: 6
title: Document the result
- number: 7
title: Known limitations / next iterations
verify:
- The game launches without contacting the EOS auth server
- Single-player mode is available without an EOS login
- Section set intersects [`.link`, `.sbss`, `.xcode`]
- The binary has `libled.dll` sibling (the CA "Light Encryption Driver" runtime-decryption
loader)
- The binary has `EOSSDK-Win64-Shipping.dll` sibling (19MB)
- The first .pack file decrypted is `data/boot.pack` (42KB)
- The decrypted `boot.pack` has the CA Archive version-5 header
- The `boot.pack` contains 30 timestamped files (per the v2.9.0 honest-read finding)
- The .ksy parses `data/boot.pack` successfully
- The parsed file_count matches the .pack's actual entry count (30 for boot.pack)
- The .ksy parses `data/data.pack` successfully
- The file_count is 13159 (per the v2.9.0 honest-read finding)
- The TOC offset is reasonable (typically 0x100-0x10000)
- The CA Clockwork mod-loading framework is intact. Bypassing its mod-allow/deny decision
is trivial (it's a developer-mode flag, not a security feature).
- The `bypass_time.txt` flag disables the server-side time-limit check (this is a
separate opt-in, not part of the EOS bypass).
- The CA Warscape light VM is a 3-section subset of the full encrypted-VM bytecode
interpreter family. The .text section is 65.7MB and the .impdata section is 138.5MB
(W^X). A full lift is medium-complexity.
- Per-patch breakable; needs re-run on every CA update.
commands:
- '# Drop a .eos side-file in the game directory
echo "DEV_MODE=1" > "<installation_path>/game.eos"
# Or use the documented dev-mode escape hatch'
- 're-static-triage --target=Warhammer3.exe --output=/tmp/tww3-triage.json
re-lief --classify-native-protection --target=Warhammer3.exe
# Expect: "encrypted-vm-bytecode-interpreter" with 3-section subset'
- '# libled.dll''s runtime-decryption entry point: hook it
re-runtime-dump --target=Warhammer3.exe --mode=frida --output=/tmp/tww3-pack-decrypted/
--license-acknowledge --timeout=600
# The Frida hook:
# 1. Hook the libled.dll decryption function
# 2. Capture the AES key + IV + input path
# 3. Capture the decrypted output buffer
# 4. Write the decrypted .pack contents to /tmp/tww3-pack-decrypted/'
- "# Use the boot.pack header as the .ksy template\n# The .pack format: [magic 4B][version\
\ 4B][file_count 4B][toc_offset 4B][toc_size 4B][flags 4B][TOC][files]\n# Write\
\ the .ksy:\ncat > /tmp/tww3-pack.ksy << 'EOF'\nseq:\n - id: magic\n contents:\
\ 'PACK'\n - id: version\n type: u4le\n - id: file_count\n type: u4le\n\
\ - id: toc_offset\n type: u4le\n - id: toc_size\n type: u4le\n - id: flags\n\
\ type: u4le\n - id: toc\n type: toc_entry\n repeat: expr\n repeat-expr:\
\ file_count\ntypes:\n toc_entry:\n seq:\n - id: filename\n type:\
\ strz\n - id: offset\n type: u8le\n - id: size\n type:\
\ u8le\n - id: flags\n type: u4le\nEOF\n\n# Compile the .ksy\nkaitai-struct-compiler\
\ --target python /tmp/tww3-pack.ksy\n\n# Apply the .ksy to data/boot.pack\nkaitai-struct-compiler\
\ --target python /tmp/tww3-pack.ksy\npython3 -c \"\nfrom tww3_pack import Tww3Pack\n\
import json\np = Tww3Pack.from_file('/tmp/tww3-pack-decrypted/boot.pack')\nprint(json.dumps(p,\
\ indent=2, default=str))\n\""
- '# The big one: data/data.pack is 408MB and contains 13159 files
python3 -c "
from tww3_pack import Tww3Pack
p = Tww3Pack.from_file(''/tmp/tww3-pack-decrypted/data.pack'')
print(f''file_count: {p.file_count}'')
print(f''toc_offset: {p.toc_offset}'')
print(f''toc_size: {p.toc_size}'')
"'
- "re-bypass-result --target=Warhammer3.exe \\\n --pack-decrypted=/tmp/tww3-pack-decrypted/\
\ \\\n --ksy=/tmp/tww3-pack.ksy \\\n --runtime-cost-minutes=90 \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-c\
\ \\\n --output=/tmp/tww3-bypass-result.md"
@@ -0,0 +1,29 @@
name: il2cpp-triage
source: skills/re-il2cpp-triage/SKILL.md
version: 0.3.0
when_to_use: Target is a Unity IL2CPP launcher (~660KB), the companion GameAssembly.dll
has the encrypted-VM bytecode interpreter
target_class: Unity 6 / 2022.3 LTS / older Unity games (FM26, HKIA, LIR, P3R)
catalog_entry: n/a (workflow, redirects to Pattern A / A-DW / A-VMT)
expected_runtime_minutes: 5
success_probability: 0.85
tools:
- re-il2cpp-triage
- re-catalog-match
- re-vm-decrypt
- re-anti-analysis-scan
steps:
- step: 1
tool: re-il2cpp-triage
action: auto_detect + triage_il2cpp on the launcher
purpose: find GameAssembly.dll + global-metadata.dat + il2cpp.usym; run re-triage
on the .dll
- step: 2
tool: re-catalog-match
action: match_catalog with main_binary=GameAssembly.dll
purpose: rank catalog entries against the .dll's triage (which has the encrypted-VM
section set + anti-debug primitives)
- step: 3
tool: re-vm-decrypt
action: decrypt_target with pattern=Pattern A
purpose: plan the encryption-stub hook + per-method binary dump
@@ -0,0 +1,134 @@
name: pattern-a-dw
source: docs/PLAYBOOKS/pattern-a-dw-denuvo.md
version: 0.2.0
target_class: 'UE5 titles with Pattern A-DW (encrypted-VM bytecode interpreter + Denuvo
Anti-Tamper trigger-arm). Examples: **Persona 3 Reload** (P3R — the canonical A-DW
+ Denuvo case from the v2.9.0 stress test).'
catalog_entry: encrypted-vm.bytecode-interpreter.pattern-a-dw
expected_runtime: 240 minutes (per-target static + dynamic; the Denuvo bypass is the
long pole)
success_probability: 0.2
tools:
- re-anti-debug-patch
steps:
- number: 1
title: Confirm the target matches the pattern
- number: 2
title: Establish the dynamic baseline (with GPU + Steam token)
- number: 3
title: Bypass the POGO entry validation
- number: 4
title: Patch the anti-debug primitives
- number: 5
title: Spoof the hypervisor detection
- number: 6
title: Lift the encrypted-VM dispatcher
- number: 7
title: The Denuvo license check
- number: 8
title: Document the result
- number: 9
title: Known limitations / next iterations
verify:
- Section set intersects [`.arch`, `.sbss`, `.xcode`, `.xpdata`, `.xtext`, `.xtls`]
(the FULL A-DW section set)
- Debug directory contains a POGO entry (type 10) of size >= 1000 bytes — this is
the Pattern A-DW differentiator from base Pattern A
- '`re-anti-analysis` reports ≥ 30 VMCALL sites (Denuvo ATD characteristic)'
- '`re-anti-analysis` reports ≥ 100 VMXON sites'
- '`re-hypervisor-detect` reports `kernel-active` posture with 100+ VMX-EPT hits'
- The breakpoint at the first RDTSC site hits before the anti-debug layer triggers
- The RDTSC delta histogram (over 1000 samples) shows a bimodal distribution (normal
+ trapped)
- The trapped population is at the high-delta end (typically 1000+ cycles)
- The POGO entry's return-value check is patched (the binary now returns 0 unconditionally)
- The patched binary boots without triggering Denuvo's POGO-based exit
- All 10 RDTSC sites patched to return 0
- All 5 VMCALL sites patched to be no-ops
- The patched binary boots without the RDTSC delta-trap triggering
- All CPUID-leaf-1-ECX-bit-31 reads return 0
- All CPUID-leaf-0x40000000-vendor reads return bare-metal vendor
- All VMCALL probes are no-op'd
- The POGO entry's hypervisor check (if any) is satisfied
- The lifted IL has fewer than 2000 instructions (per-dispatcher for P3R's encrypted-VM
is larger than Pattern A base)
- The simplified IL has at least 30% fewer instructions than the raw lift
- The simplified IL has named basic blocks
- The Denuvo license check returns "entitled" (the patched return value)
- The binary continues past the license check
- The binary does NOT phone home to the Denuvo entitlement server (verify by network
capture)
- '**Denuvo ATD is the hardest commercial protection**. The 0.2 success_probability
reflects this — per-title bypass is months of work, no general solution.'
- POGO entry payload is opaque; the public documentation of POGO is sparse. Future
iterations need the vendor's POGO interpreter.
- Online entitlement check is still required; the binary's UE5 splash screen still
requires a valid license from the publisher's server.
- Per-build breakable; needs re-run on every patch.
- If the runtime-decrypted region is larger than 200MB (P3R's is borderline), the
lift may take 4-6 hours.
- Denuvo's ATD layer may also detect the POGO bypass and exit. The realistic alternative
is to *not* bypass the POGO entry but instead to *emulate* the POGO entry's return
value in a sandbox.
commands:
- 're-static-triage --target=P3R.exe --output=/tmp/p3r-triage.json
re-anti-analysis --target=P3R.exe --output=/tmp/p3r-anti.json
re-hypervisor-detect --target=P3R.exe --output=/tmp/p3r-hyp.json
re-lief --get-debug-directory --target=P3R.exe'
- 're-catalog-match --triage=/tmp/p3r-triage.json --intent=both
# Expect: encrypted-vm.bytecode-interpreter.pattern-a-dw (high confidence)
# Expect: anti-tamper-vendors.denuvo (very high confidence if ''denuvo'' string
is present)'
- '# Wine + winedbg + gdb on Wine 11.0
re-winedbg --target=P3R.exe --port=0 --session=p3r-baseline
# Set RVA-aware breakpoint on the first RDTSC site
re-winedbg-set-bp --target=P3R.exe --rva=<first_rdtsc_rva> --session=p3r-baseline
re-winedbg-continue --session=p3r-baseline --timeout=15
re-winedbg-read-registers --session=p3r-baseline'
- '# POGO entry is at image offset 84470104 (per the debug-directory read)
# The POGO entry is the trigger-arm signature; bypass by patching the entry''s
# return-value check to always return 0 (success)
re-patch --target=P3R.exe --offset=84470104 --strategy=pogo-bypass'
- "# Identify and patch the first 10 RDTSC sites\nfor offset in $(jq -r '.byte_sequences[]\
\ | select(.primitive == \"RDTSC\") | .matches[:10] | .[]' /tmp/p3r-anti.json);\
\ do\n re-anti-debug-patch --target=P3R.exe --offset=$offset --strategy=rdtsc-zero\n\
done\n\n# Identify and patch the first 5 VMCALL sites\nfor offset in $(jq -r '.byte_sequences[]\
\ | select(.primitive == \"VMCALL\") | .matches[:5] | .[]' /tmp/p3r-anti.json);\
\ do\n re-anti-debug-patch --target=P3R.exe --offset=$offset --strategy=vmcall-noop\n\
done"
- 're-anti-vm-spoof --snapshot-bare-metal --output=/tmp/cpuid-snapshot.json
re-anti-vm-spoof --target=P3R.exe --snapshot=/tmp/cpuid-snapshot.json --mode=frida'
- 're-rizin --target=P3R.exe --find-dispatcher --encrypted-vm-family=pattern-a-dw
re-vtil --target=P3R.exe --dispatcher=<dispatcher_rva> --output=/tmp/p3r-dispatcher.il
re-vtil --il=/tmp/p3r-dispatcher.il --simplify=ollvm-default --output=/tmp/p3r-dispatcher-simplified.il'
- '# Denuvo''s online license check is at the network level. The bypass primitive:
# (a) host-level firewall to block the Denuvo entitlement server
# (b) run the binary in an offline state
# (c) the binary''s Denuvo layer will detect the offline state and exit
# (d) the realistic alternative: patch the license check to return "entitled"
re-anti-debug-patch --target=P3R.exe --offset=<license_check_rva> --strategy=license-bypass'
- "re-bypass-result --target=P3R.exe \\\n --dispatcher-il=/tmp/p3r-dispatcher-simplified.il\
\ \\\n --runtime-cost-minutes=240 \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-a-dw\
\ \\\n --output=/tmp/p3r-bypass-result.md"
@@ -0,0 +1,94 @@
name: pattern-a-vmt
source: docs/PLAYBOOKS/pattern-a-vmt-blackspace.md
version: 0.2.0
target_class: 'Pearl Abyss BlackSpace Engine titles. Example: **Crimson Desert** (CD
— the canonical Pattern A-VMT case from the v2.9.0 stress test).'
catalog_entry: encrypted-vm.bytecode-interpreter.pattern-a-vmt
expected_runtime: 180 minutes
success_probability: 0.5
tools:
- re-vm-decrypt
steps:
- number: 1
title: Confirm the target matches the pattern
- number: 2
title: Read the .xcode dispatch table
- number: 3
title: Resolve the handler targets in .link (the hard part)
- number: 4
title: Reconstruct the handler table
- number: 5
title: Lift each handler to readable IL
- number: 6
title: Document the result
- number: 7
title: Known limitations / next iterations
verify:
- Section set intersects [`.arch`, `.link`, `.xcode`, `.xtext`, `.sbss`]
- '`.xcode` has the dual-regime signature: low-entropy head (dispatch table) + high-entropy
tail (encrypted metadata)'
- Debug directory contains a POGO entry AND an ILTCG entry (the dual-tell is the A-VMT
differentiator from A-DW)
- The first N entries have a non-zero `handler_id` and a non-zero `target`
- The `target` values are RVA-like (typically in the range 0x10000000-0xFFFFFFFF for
a 32-bit RVA, or 0x140000000+ for a 64-bit RVA)
- The Frida trace shows `CreateFileW` being called on `data.xpac` or similar
- The .link section at runtime is writable (it was read-only on disk; the runtime
decryption patches it in-place)
- The decrypted handler bodies are between 4KB and 100KB each
- Each handler body is at least 4KB (handlers smaller than this are likely placeholders
or stub)
- No two handlers have identical bodies (this would indicate a deduplication scheme,
which BlackSpace doesn't use)
- The aggregated handler-table view has all 4096 (or however many) handlers listed
- 'Each handler is named in the view (e.g. "handler_0001: addition", "handler_0002:
subtraction", ...)'
- The handler-table view is human-readable
- The .link runtime-decryption requires GPU + valid Pearl Abyss account (librdkafka
telemetry publishes a handshake at boot). Need a fully-functional environment for
the lift to succeed.
- 'Hermes SDK + librdkafka telemetry surface: capture the publish topics and payloads
as a side-effect of the bypass (see Pattern D catalog entry).'
- The .arch section (73.7MB) is the static x86_64 code; the .link is the runtime-decrypted
data. The two together form the full BlackSpace engine.
commands:
- 're-static-triage --target=CrimsonDesert.exe --output=/tmp/cd-triage.json
re-lief --classify-native-protection --target=CrimsonDesert.exe
# Expect: "encrypted-vm-handler-table-dispatch" (the new Pattern A-VMT class)'
- "# Extract the .xcode section\nre-lief --extract-section --target=CrimsonDesert.exe\
\ --section=.xcode --output=/tmp/cd-xcode.bin\n\n# The dispatch table is in the\
\ low-entropy head. The entries are 16-byte big-endian:\n# [u32 handler_id][u32\
\ reserved=0][u64 target]\n# Use re-triton or a simple script to parse the table\n\
python3 -c \"\nimport struct\ndata = open('/tmp/cd-xcode.bin', 'rb').read()\nfor\
\ i in range(0, min(8192, len(data)), 16):\n hid, reserved, target = struct.unpack('>IIQ',\
\ data[i:i+16])\n if hid == 0 and reserved == 0 and target == 0:\n break\n\
\ print(f' handler {hid}: target 0x{target:x}')\n\""
- '# The .link section contains the runtime-decrypted handler bodies
# At boot, BlackSpace decrypts .arch (the handler bodies) and patches them into
.link
# To capture the runtime-decrypted .link, use Frida
re-runtime-dump --target=CrimsonDesert.exe --mode=frida --output=/tmp/cd-link-decrypted/
--license-acknowledge --timeout=600'
- "# For each handler in the dispatch table, look up the corresponding decrypted body\
\ in .link\npython3 -c \"\nimport struct\nxcode = open('/tmp/cd-xcode.bin', 'rb').read()\n\
link_decrypted = open('/tmp/cd-link-decrypted/decrypted.bin', 'rb').read()\n\n#\
\ The target in the dispatch table is a RVA. Resolve it to a .link offset\n# (this\
\ is engine-specific; check BlackSpace's source or just brute-force)\n\nfor i in\
\ range(0, min(8192, len(xcode)), 16):\n hid, reserved, target_rva = struct.unpack('>IIQ',\
\ xcode[i:i+16])\n if hid == 0:\n break\n # Map target_rva to link_decrypted\
\ offset\n body = link_decrypted[target_rva:target_rva+8192]\n # Write the\
\ handler body to a separate file\n with open(f'/tmp/cd-handler-{hid:04x}.bin',\
\ 'wb') as f:\n f.write(body)\n print(f' handler {hid:04x}: rva 0x{target_rva:x},\
\ wrote {len(body)} bytes')\n\""
- "# Lift each handler to IL\nfor hid in $(ls /tmp/cd-handler-*.bin); do\n re-vtil\
\ --target=$hid --output=${hid%.bin}.il --simplify=ollvm-default\ndone\n\n# Aggregate\
\ the lifted IL into a single handler-table view\nre-runtime-dump --aggregate-handlers\
\ --input=/tmp/cd-handler-*.il --output=/tmp/cd-handler-table.md"
- "re-bypass-result --target=CrimsonDesert.exe \\\n --handler-table=/tmp/cd-handler-table.md\
\ \\\n --runtime-cost-minutes=180 \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-a-vmt\
\ \\\n --output=/tmp/cd-bypass-result.md"
+112
View File
@@ -0,0 +1,112 @@
name: pattern-a
source: docs/PLAYBOOKS/encrypted-vm-bytecode-interpreter-pattern-a.md
version: 0.2.0
target_class: 'Unity 6 / Unity 2022.3 LTS / older Unity games with an encrypted-VM
bytecode interpreter layer on top of the standard IL2CPP runtime. Examples: **Football
Manager 26** (FM26), **Hello Kitty Island Adventure** (HKIA), **Lost In Random**
(LIR — but with Origin stub-drop instead of bypass).'
catalog_entry: encrypted-vm.bytecode-interpreter.pattern-a
expected_runtime: 45 minutes (per-target static + dynamic)
success_probability: 0.7
tools:
- re-vm-decrypt
steps:
- number: 1
title: Confirm the target matches the pattern
- number: 2
title: Establish the dynamic baseline
- number: 3
title: Patch the anti-debug primitives
- number: 4
title: Spoof the hypervisor detection
- number: 5
title: Lift the encrypted-VM dispatcher
- number: 6
title: Document the result
- number: 7
title: Known limitations / next iterations
verify:
- Section set intersects [`.xtls`, `.xpdata`, `.xdata`, `.arch`, `.link`, `.sbss`,
`.xcode`]
- '`re-anti-analysis` reports ≥ 50 RDTSC sites'
- '`re-anti-analysis` reports ≥ 50 CPUID sites'
- '`re-anti-analysis` reports ≥ 50 VMXON sites'
- '`re-hypervisor-detect` reports `kernel-active` or `static-probes-only`'
- '`/tmp/fm26-emu/decrypted/` has at least 1 file (the lazy-decrypted region)'
- The decrypted file size is between 1MB and 100MB (typical for a Unity GameAssembly's
runtime-decrypted code)
- The `speakeasy-trace.json` shows at least 1 anti-debug API call (IsDebuggerPresent
or RDTSC)
- The patched binary boots without crashing (verify by attempting a 30s speakeasy
run after patching)
- No anti-debug API calls are intercepted after patching (verify by re-running speakeasy)
- All CPUID-leaf-1-ECX-bit-31 reads return 0
- All CPUID-leaf-0x40000000-vendor reads return the bare-metal vendor string
- All VMCALL probes are no-op'd (or return 0)
- The lifted IL has fewer than 1000 instructions (per-dispatcher)
- The simplified IL has at least 50% fewer instructions than the raw lift
- The simplified IL has named basic blocks (not just block_0, block_1, ...)
- The online entitlement check (Origin / Steam / EOS) still requires a valid publisher
account, unless you also apply the corresponding launch-entitlement bypass (`re-origin-stub-drop`
/ `re-eos-bypass` / etc.)
- The bypass is per-target; each new build of the same game requires re-running the
playbook
- Some encrypted-VM bytecode interpreters have built-in anti-tamper that detects the
patch + exits; in that case, use the Frida-only approach (`--mode=frida`) instead
of the static-patch approach
- If the runtime-decrypted region is larger than 100MB, the lift may take 1-2 hours
(consider `re-triton` symbolic execution as a faster alternative)
commands:
- '# Static triage
re-static-triage --target=GameAssembly.dll --output=/tmp/fm26-triage.json
# Anti-analysis scan
re-anti-analysis --target=GameAssembly.dll --output=/tmp/fm26-anti.json
# Hypervisor posture
re-hypervisor-detect --target=GameAssembly.dll --output=/tmp/fm26-hyp.json'
- 're-catalog-match --triage=/tmp/fm26-triage.json --intent=both
# Expect: encrypted-vm.bytecode-interpreter.pattern-a (high confidence)
# Expect: anti-debug.rdtsc-timing-trap (medium-high confidence)
# Expect: anti-vm.cpuid-leaf-1-ecx-bit-31 (medium-high confidence)'
- '# Speakeasy emulation
re-runtime-dump --target=GameAssembly.dll --mode=emulator --output=/tmp/fm26-emu/
--license-acknowledge --timeout=300'
- "# Identify the first 5 RDTSC sites\njq '.byte_sequences[] | select(.primitive ==\
\ \"RDTSC\") | .matches[:5]' /tmp/fm26-anti.json\n\n# Patch each RDTSC site to return\
\ 0\nfor offset in $(jq -r '.byte_sequences[] | select(.primitive == \"RDTSC\")\
\ | .matches[:5] | .[]' /tmp/fm26-anti.json); do\n re-anti-debug-patch --target=GameAssembly.dll\
\ --offset=$offset --strategy=rdtsc-zero\ndone"
- '# Snapshot a bare-metal CPUID profile (run on a known-bare-metal host)
re-anti-vm-spoof --snapshot-bare-metal --output=/tmp/cpuid-snapshot.json
# Apply the snapshot at runtime
re-anti-vm-spoof --target=GameAssembly.dll --snapshot=/tmp/cpuid-snapshot.json --mode=frida'
- '# Identify the dispatcher candidate
re-rizin --target=GameAssembly.dll --find-dispatcher
# Lift the dispatcher''s IL
re-vtil --target=GameAssembly.dll --dispatcher=<dispatcher_rva> --output=/tmp/fm26-dispatcher.il
# Simplify the lifted IL with the d810-ng pass set
re-vtil --il=/tmp/fm26-dispatcher.il --simplify=ollvm-default --output=/tmp/fm26-dispatcher-simplified.il'
- "# Generate the bypass result document\nre-bypass-result --target=GameAssembly.dll\
\ \\\n --dispatcher-il=/tmp/fm26-dispatcher-simplified.il \\\n --runtime-cost-minutes=42\
\ \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-a \\\n --output=/tmp/fm26-bypass-result.md"
+117
View File
@@ -0,0 +1,117 @@
name: pattern-b
source: docs/PLAYBOOKS/ea-origin-stub-drop.md
version: 0.2.0
target_class: 'EA-shipped games with the Origin activation gate. Example: **Lost In
Random** (LIR). The launcher imports only 2 functions, both from `Core/Activation64.dll`
ordinals 100 and 101.'
catalog_entry: encrypted-vm.bytecode-interpreter.pattern-b
expected_runtime: 30 minutes (one of the easiest playbooks)
success_probability: 0.9
tools:
- re-runtime-dump
steps:
- number: 1
title: Confirm the target matches the pattern
- number: 2
title: Establish the dynamic baseline
- number: 3
title: Locate ordinal 100's function body
- number: 4
title: Patch the function body to return 0
- number: 5
title: Apply the patch
- number: 6
title: Test the launch
- number: 7
title: Rollback plan
- number: 8
title: Document the result
- number: 9
title: Known limitations / next iterations
verify:
- The launcher imports ONLY 2 functions, both from `Core/Activation64.dll` ordinals
100 and 101
- '`Core/Activation64.dll` is a NATIVE C++ DLL (not .NET — re-dotnet returns "PE image
does not have metadata")'
- '`Core/Activation64.dll` is classified as `anti-debug-wrapped`'
- The pre-patch SHA-256 is recorded in `override-scope.md`
- The Frida baseline shows `Core/Activation64.dll!ord 100` being called once at launch,
returning 0 (entitled) after a WinHTTP round-trip to the Origin auth server
- The function body at ordinal 100 is a C++ member function (not a forwarder to a
separate DLL)
- The function body's prologue is `push rbp; mov rbp, rsp; sub rsp, ...` (or similar
MSVC x64 convention)
- The function body's epilogue is `add rsp, ...; pop rbp; ret` (or similar)
- The patched file is 8 bytes larger at the offset (the original function body is
replaced)
- The patched file's SHA-256 is recorded (this is the new SHA, will be used for rollback)
- The first 8 bytes at the function body RVA are `48 31 C0 C3` (xor rax, rax; ret)
— or equivalent `mov rax, 0; ret` = `48 C7 C0 00 00 00 00 C3`
- The patched file is in place at `<installation_path>/Core/Activation64.dll`
- The original file is backed up to `Core/Activation64.dll.bak` (the user already
had this, per the Input/ inventory)
- The game launches without contacting the Origin auth server (verify by network capture)
- The game starts in single-player mode (Origin entitlement is satisfied)
- If the game requires online (multiplayer, leaderboards), those features may not
work (this is expected; the Origin entitlement is what authenticates them)
- The restored file's SHA-256 matches the pre-patch baseline
- The game launches normally (with the original entitlement check)
- The Origin entitlement check is bypassed, but the EA / Origin account itself is
still required for online features (multiplayer, leaderboards, etc.)
- The patch is per-binary; each new build of LIR requires re-running the playbook
- If Origin updates their entitlement protocol, the patched function may not satisfy
the new protocol — the binary would crash on launch
- The stub-drop is a "launch entitlement" bypass, not a "DRM bypass". The GameAssembly.dll's
encrypted-VM bytecode interpreter (with 200+ RDTSC + 200+ VMXON + 32 VMCALL) is
still intact.
commands:
- 're-static-triage --target=Core/Activation64.dll --output=/tmp/lir-activation-triage.json
re-dotnet --parse-assembly --target=Core/Activation64.dll
# Expect: "PE image does not have metadata" - confirms it''s NATIVE C++, not .NET
re-lief --get-imports-exports --target="Lost In Random.exe" --max_imports=10
# Expect: only 2 imports, both from Core/Activation64.dll ordinals 100 and 101
re-lief --classify-native-protection --target=Core/Activation64.dll
# Expect: "anti-debug-wrapped"'
- '# Capture the pre-patch SHA-256
re-patch --sha256-manifest --target=Core/Activation64.dll
# Record the SHA-256 to override-scope.md as the pre-patch baseline
# Spy on the entitlement check at runtime (Frida approach)
re-runtime-dump --target=Core/Activation64.dll --mode=frida --output=/tmp/lir-frida-baseline/
--license-acknowledge --timeout=60'
- '# Find the function body at ordinal 100
re-rizin --target=Core/Activation64.dll --find-export --ordinal=100
# Note: the function body RVA is the address returned by the export lookup
# Disassemble the function body
re-rizin --target=Core/Activation64.dll --disassemble-function --rva=<function_rva>'
- '# Replace the original file
cp /tmp/lir-patched/Core/Activation64.dll <installation_path>/Core/Activation64.dll
# Or: use re-patch to write directly (it has an --in-place flag)'
- '# Launch the game
wine "/path/to/Lost In Random.exe"
# Or: native Windows launch'
- "# If something goes wrong, restore from the .bak file\ncp Core/Activation64.dll.bak\
\ Core/Activation64.dll\n\n# Or: use re-patch.restore_original\nre-patch --restore-original\
\ \\\n --original=Core/Activation64.dll.bak \\\n --restore-target=Core/Activation64.dll\
\ \\\n --expected-sha256=<original_sha256>"
- "re-bypass-result --target=Core/Activation64.dll \\\n --runtime-cost-minutes=15\
\ \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-b \\\n --output=/tmp/lir-activation-bypass-result.md"
+85
View File
@@ -0,0 +1,85 @@
name: pattern-c
source: docs/PLAYBOOKS/ioi-glacier-shielding.md
version: 0.2.0
target_class: IO Interactive Glacier 2 / 007 First Light (and likely the rest of IOI's
recent titles).
catalog_entry: encrypted-vm.bytecode-interpreter.unity-shielding-pdb-leak
expected_runtime: 120 minutes
success_probability: 0.5
tools:
- re-static-triage
steps:
- number: 1
title: The "shielding" PDB leak
- number: 2
title: The Confluence URL leak (Pattern D side-effect)
- number: 3
title: Confirm the protection surface
- number: 4
title: Patch the anti-debug primitives
- number: 5
title: Lift the encrypted-VM dispatcher
- number: 6
title: Document the result
- number: 7
title: Known limitations / next iterations
verify:
- The PDB path is `3. WindowsLauncher_shielding.pdb` (or similar — the project name
is "shielding")
- The compiler is MSVC 14.29 (Visual Studio 2019)
- The binary was built in 2026 (the build timestamp)
- The Confluence URL is found
- The URL is in the `TECH` space (IOI's internal Atlassian wiki)
- The page ID is 5618628 (the "Base Tag Reference" page)
- 1966 RDTSC sites (the v2.9.0 stress test finding)
- 200+ CPUID sites
- 200+ VMXON sites
- 28 VMCALL sites
- kernel-active hypervisor posture
- 274MB .tls section (the anomalous size)
- .data1 section with entropy 7.19 (encrypted)
- The patched binary is 007FirstLight-patched.exe with ~2000+ patched sites
- The patched binary boots without SIGABRT in the early-boot trace
- The lifted IL has fewer than 1500 instructions (per-dispatcher for 007FL)
- The simplified IL has at least 50% fewer instructions than the raw lift
- The 274MB .tls section is anomalously large. May be a static-decrypted region that
requires a different runtime hook.
- The IOI entitlement server (contacted via WinHTTP) is out of scope for this playbook.
- The .data1 section entropy 7.19 indicates encrypted data; runtime decryption is
the only way to read it.
- Per-build breakable.
commands:
- '# The launcher PDB path is the canonical finding
re-lief --categorize-strings --target=007FirstLight.exe --output=/tmp/007fl-strings.json
# Look for "WindowsLauncher_shielding.pdb" in the output
grep "shielding" /tmp/007fl-strings.json'
- 'grep "ioicloud" /tmp/007fl-strings.json
# Expect: https://ioicloud.atlassian.net/wiki/spaces/TECH/pages/5618628/Base+Tag+Reference'
- 're-static-triage --target=007FirstLight.exe --output=/tmp/007fl-triage.json
re-anti-analysis --target=007FirstLight.exe --output=/tmp/007fl-anti.json
re-hypervisor-detect --target=007FirstLight.exe --output=/tmp/007fl-hyp.json'
- '# Patch ALL RDTSC sites (not just the first 10)
re-anti-debug-patch --target=007FirstLight.exe --strategy=rdtsc-zero-all
# Patch ALL VMXON sites
re-anti-debug-patch --target=007FirstLight.exe --strategy=vmxon-noop-all
# Patch ALL CPUID sites
re-anti-debug-patch --target=007FirstLight.exe --strategy=cpuid-spoof-all'
- 're-rizin --target=007FirstLight.exe --find-dispatcher --encrypted-vm-family=pattern-a
re-vtil --target=007FirstLight.exe --dispatcher=<dispatcher_rva> --output=/tmp/007fl-dispatcher.il
re-vtil --il=/tmp/007fl-dispatcher.il --simplify=ollvm-default --output=/tmp/007fl-dispatcher-simplified.il'
- "re-bypass-result --target=007FirstLight.exe \\\n --dispatcher-il=/tmp/007fl-dispatcher-simplified.il\
\ \\\n --runtime-cost-minutes=120 \\\n --catalog-match=encrypted-vm.bytecode-interpreter.unity-shielding-pdb-leak\
\ \\\n --output=/tmp/007fl-bypass-result.md"
+96
View File
@@ -0,0 +1,96 @@
name: pattern-d
source: docs/PLAYBOOKS/ea-entitlement-replay.md
version: 0.2.0
target_class: EA-shipped games (LIR, Battlefield, FIFA, etc.) with an active Origin
auth flow. This is the LIR supplement to `ea-origin-stub-drop.md` — for cases where
the stub-drop doesn't satisfy the online entitlement re-check.
catalog_entry: encrypted-vm.bytecode-interpreter.pattern-b
expected_runtime: 120 minutes
success_probability: 0.4
tools:
- re-runtime-dump
steps:
- number: 1
title: Capture the Origin auth flow
- number: 2
title: Document the entitlement token format
- number: 3
title: Build a replay server
- number: 4
title: Configure the binary to use the replay server
- number: 5
title: Document the result
- number: 6
title: Known limitations / next iterations
verify:
- The Origin auth server URL is captured
- The request body has the entitlement token format
- The response has the entitlement grant
- The request body schema is documented
- The response body schema is documented
- The expiration semantics are understood (token lifetime, refresh logic)
- The replay server listens on 127.0.0.1:8080
- The replay server returns the same schema as the captured response
- The binary's WinHTTP requests are routed to the replay server
- The entitlement check returns "entitled" (the replayed response)
- '**DRM-adjacent territory**. The LICENSE-OFFENSIVE.md clause applies; ensure your
use case is authorized.'
- The Origin auth protocol changes; the replay may break after Origin updates their
auth flow.
- Online features (multiplayer, leaderboards) require additional entitlement checks
beyond the launch entitlement.
- Some titles use a per-launch nonce; the replay needs to handle the nonce.
- Some titles use certificate pinning; the replay needs to provide a matching certificate.
- The replay is brittle; the stub-drop approach in `ea-origin-stub-drop.md` is more
reliable.
commands:
- '# Use PCAP to capture the WinHTTP traffic from Core/Activation64.dll
sudo tcpdump -i any -w /tmp/lir-origin.pcap ''host auth.origin.com and tcp''
# Or use mitmproxy (mitmproxy)
re-mitm2swagger --intercept --target=Core/Activation64.dll --output=/tmp/lir-mitm.flow'
- '# The Origin entitlement token is typically:
# {
# "client_id": "...",
# "client_secret": "...",
# "grant_type": "client_credentials",
# "scope": "..."
# }
# Plus the entitlement grant:
# {
# "entitlement_id": "...",
# "user_id": "...",
# "product_id": "...",
# "entitled": true,
# "expires_at": "..."
# }'
- '# Use the hosts file to redirect auth.origin.com to 127.0.0.1
# (requires admin / root privileges)
echo "127.0.0.1 auth.origin.com" >> /etc/hosts
# Or use a proxy
mitmproxy --mode reverse:https://auth.origin.com --listen-port 8080'
- "re-bypass-result --target=Core/Activation64.dll \\\n --replay-server=/tmp/replay_server.py\
\ \\\n --runtime-cost-minutes=120 \\\n --catalog-match=encrypted-vm.bytecode-interpreter.pattern-d\
\ \\\n --output=/tmp/lir-replay-bypass-result.md"
@@ -0,0 +1,28 @@
name: runtime-frida
source: skills/re-runtime-frida/SKILL.md
version: 0.3.0
when_to_use: Catalog match returned encrypted-VM entries. Operator wants to actually
capture the decrypted method bodies.
target_class: Unity IL2CPP / UE5 / proprietary engine targets with encrypted-VM bytecode
interpreter (Pattern A / A-DW / A-VMT / B / C / D)
catalog_entry: n/a (workflow)
expected_runtime_minutes: 30
success_probability: 0.6
tools:
- re-frida-runtime
- re-runtime-dump
- re-vm-decrypt
steps:
- step: 1
tool: re-frida-runtime
action: frida_attach with pattern=Pattern A
purpose: actually attach Frida + install encryption-stub hook + capture per-method
plaintext
- step: 2
tool: re-vm-decrypt
action: decrypt_target to confirm the hook set
purpose: re-plan the encryption-stub hook for the specific target
- step: 3
tool: re-catalog-match
action: re-match with intent=defender
purpose: confirm the catalog re-match against the captured payloads
@@ -0,0 +1,26 @@
name: triage-fresh-target
source: skills/re-triage-fresh-target/SKILL.md
version: 0.3.0
when_to_use: Target has no prior analysis (no honest-read triage.json)
target_class: Any binary; the .exe / .dll / .so to triage from scratch
catalog_entry: n/a (workflow, not a technique)
expected_runtime_minutes: 2
success_probability: 0.95
tools:
- re-triage
- re-catalog-match
- re-anti-analysis-scan
- re-static-triage
steps:
- step: 1
tool: re-triage
action: Run re-triage on the target binary
purpose: compute sections, primitive counts, per-site RVAs, hypervisor posture
- step: 2
tool: re-catalog-match
action: Run catalog match against the fresh triage
purpose: rank catalog entries by defender-side confidence
- step: 3
tool: re-bypass-pattern-X
action: Continue with the per-Pattern playbook
purpose: Per-Pattern bypass orchestration (planning layer)
@@ -0,0 +1,23 @@
// v0.8.0+ Wave 3 (Item G): per-target YARA fingerprint rules
// Auto-generated. Do not edit by hand.
rule re_breaker_target_f1_25
{
meta:
target_key = "f1_25"
target = "RE_BREAKER_PLUGIN_ROOT/Input/F1.25.Iconic.Edition-InsaneRamZes/F1_25.exe"
confidence_this_target = 100
confidence_other_targets = 0
generated_by = "re-target-fingerprint v0.1.0"
generated_date = "auto-generated"
strings:
$p0 = { 64 65 6E 75 76 6F 5F 61 74 64 }
$p1 = { 61 6E 74 69 74 61 6D 70 65 72 64 69 61 67 6E 6F 73 69 73 }
$p2 = { 66 31 32 30 32 35 }
condition:
$p0 or
$p1 or
$p2
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
# Keep the per-category directories in git. Remove this file once at least
# one entry exists in every category.
@@ -0,0 +1,78 @@
---
title: "APK Structure"
category: "android"
platforms: ["android"]
difficulty: "beginner"
tags: ["fundamentals", "static-analysis", "packaging"]
summary: "How an Android Package is laid out on disk: the ZIP container, AndroidManifest, dex files, resources, and the native libraries that ship alongside."
updated: "2026-06-04"
related: ["android/02-smali-basics", "android/03-native-libs", "android/06-repackaging"]
---
## Summary
An APK is a ZIP archive with a well-defined internal layout. Knowing where every artifact lives is the prerequisite to almost every other Android RE task.
## Why this matters
Every tool you'll use — `apktool`, `jadx`, `unzip`, `aapt2`, frida, ghidra, IDA — reads the same on-disk structure. The moment you can navigate an APK by hand, you stop being dependent on tooling rendering it correctly.
## Mechanics
The high-level layout of an APK:
```
my-app.apk
├── AndroidManifest.xml # binary XML; resources, components, permissions
├── classes.dex # primary dex (Java/Kotlin compiled to dex)
├── classes2.dex ... # multidex; older devices required classes2+
├── lib/ # native libraries
│ ├── arm64-v8a/
│ ├── armeabi-v7a/
│ ├── x86/
│ └── x86_64/
├── assets/ # arbitrary files; read via AssetManager
├── res/ # compiled resources (resources.arsc + per-type)
├── resources.arsc # the resource table; string pools, style refs
├── META-INF/ # signing artefacts (CERT.RSA, MANIFEST.MF, *.SF)
└── kotlin/ # Kotlin metadata (if any)
```
A few details that surprise people the first time:
- `AndroidManifest.xml` is **binary XML** (AXML), not text. `apktool` decodes it; `aapt2 dump xmltree` works for spot-checks.
- `resources.arsc` is a separate index from the per-resource files in `res/`. Resource IDs (the `R.id.foo` ints you see in smali) are indices into this table.
- The dex format is little-endian and very compact; strings, types, methods, and fields are all pool-indexed. A `classes.dex` is effectively a serialized symbol table plus bytecode.
- Native libraries under `lib/<abi>/` are loaded by `System.loadLibrary("name")` from the `app`'s default classloader. Stripped or unstripped is a per-build-flag choice.
- `META-INF/CERT.RSA` and `MANIFEST.MF` are v1 signing artefacts; v2+ signatures live in a special block *between* ZIP entries and the central directory. Tools that don't understand v2/v3 (or that mangle them) will silently break signature verification.
## Approach
For a first pass on an unknown APK:
1. `unzip -l target.apk` to see the layout. Skim for `lib/`, `assets/`, anything unusual.
2. `aapt2 dump badging target.apk` (or `aapt dump badging` on older toolchains) to get the package name, version, target SDK, and permissions.
3. `apktool d target.apk -o out/` to decode everything to a directory tree of text XML, smali, and resource stubs. This is the canonical "I want to read the source" step.
4. For decompiled Java/Kotlin rather than smali: `jadx -d out-jadx target.apk`. jadx is a dex decompiler; it does best-effort reconstruction and falls back to smali for the parts it can't recover.
5. For native code: pull `lib/<abi>/*.so` out and load them in Ghidra / IDA / Binary Ninja directly. They are standard ELF.
## Common pitfalls
- **Treating the smali you see as canonical.** It's decompiled; the original may have had different control flow that the decompiler collapsed. Always check the dex for high-stakes claims.
- **Assuming `res/` is the source.** It's the *compiled* output. The actual XML sources live in the developer's build tree, not in the APK.
- **Multidex and `classes.dex` only.** Tools that walk "the dex file" will miss `classes2.dex`, `classes3.dex`, etc.
- **v2/v3 signature tampering.** Repackaging a signed APK will invalidate the v2+ block; you must re-sign with `apksigner` (or `jarsigner` for v1-only).
- **Resource ID collisions across packages.** Merged manifests and resource overlays can change IDs at build time; never hardcode resource IDs across builds.
## Tooling pointers
- [`apktool`](../tools/01-apktool.md) — decode and rebuild APKs.
- [`jadx`](../tools/02-jadx.md) — dex → Java decompiler.
- Ghidra / IDA / Binary Ninja — for the native libs.
- `aapt2` (Android SDK build-tools) — manifest and resource inspection from the CLI.
## References
- [Android docs: App Bundle / APK format](https://developer.android.com/build/building-an-app-bundle)
- [APK Signature Scheme v2 / v3](https://source.android.com/docs/security/features/apksigning)
- [DEX format (dalvik-bytecode)](https://source.android.com/devices/tech/dalvik/dex-format)
@@ -0,0 +1,121 @@
---
title: "Anti-Debug Techniques"
category: "anti-analysis"
platforms: ["linux", "windows", "macos", "android", "ios"]
difficulty: "intermediate"
tags: ["dynamic-analysis", "detection", "protection"]
summary: "How software detects the presence of a debugger — ptrace, NtQueryInformationProcess, exception-based traps, /proc probes, timing checks — and the high-level shape of how to neutralise each."
updated: "2026-06-06"
related: ["anti-analysis/03-anti-sandbox", "anti-analysis/04-anti-frida", "anti-analysis/07-vm-bytecode-proprietary", "tools/04-frida"]
---
## Summary
Anti-debug is the most common form of anti-analysis: a binary checks at runtime whether a debugger is attached, and exits, corrupts state, or runs a decoy if it is. The checks fall into a small number of families; once you know the families, recognising a new one is mostly pattern-matching.
## Why this matters
If you can bypass anti-debug, you can attach a debugger, set breakpoints, trace execution, and inspect memory. If you can't, you're limited to static analysis — slower, lower-fidelity, and often not feasible for code that only makes sense at runtime (e.g. JIT'd code, dynamically-decrypted strings, callback-heavy code).
## Mechanics
The families, with the canonical examples for each major platform:
### 1. ptrace-based (Linux, macOS, some Android)
`ptrace(PTRACE_TRACEME, 0, ...)` is a one-shot way for a process to declare "a tracer is already attached to me". A second tracer cannot then attach — `ptrace(PTRACE_ATTACH, ...)` returns `EPERM`. Self-debugging is a classic detection trick: if `PTRACE_TRACEME` fails, somebody else is already debugging you.
```c
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
exit(1); // already being debugged
}
```
The bypass is to intercept the `ptrace` syscall (or, more cleanly, the `dlopen`/`dlsym` call) and return 0.
### 2. PEB inspection (Windows)
The Process Environment Block (PEB) contains an `BeingDebugged` byte (set by the kernel when a debugger attaches) and a `NtGlobalFlag` (which gains `FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS` — 0x70 — under a debugger). Reading either is enough to detect.
```c
PPEB peb = (PPEB)__readgsqword(0x60);
if (peb->BeingDebugged) {
ExitProcess(1);
}
```
Some apps go further and call `NtQueryInformationProcess` with `ProcessDebugPort`, `ProcessDebugObjectHandle`, `ProcessDebugFlags`, or `ProcessBasicInformation` to query the kernel for the same information more directly.
### 3. Exception-based (cross-platform)
A debugger typically receives (and either swallows or passes back) exceptions raised by the traced process. Code can set a "trap" — write a `INT 3` / `0xCC` byte over its own code, or `INT 2D`, or `UD2` — and then check whether the exception is still pending. Under a debugger, the trap is consumed silently; without a debugger, it propagates and is caught by a SEH/`__try` block or a signal handler.
### 4. Timing / latency probes
Debugger breakpoints, single-stepping, and syscall traps all change timing measurably. A common pattern is `rdtsc` before/after a known-fast operation; if the delta is above a threshold, a debugger is suspected.
```c
uint64_t t0 = __rdtsc();
volatile int x = 0; (void)x;
uint64_t t1 = __rdtsc();
if (t1 - t0 > 1000) {
// probably being single-stepped
}
```
This is fiddly to bypass because it interacts with the legitimate performance of the machine; a common workaround is to scale the threshold by `QueryPerformanceCounter` or `clock_gettime` deltas taken before/after the user has had time to interact.
### 5. /proc and device probes (Linux, Android)
`/proc/self/status` contains a `TracerPid:` line that's non-zero only when a debugger is attached. Reading `/proc/self/maps` shows `[vdso]` and the loader; looking for the presence of `gdb`, `ltrace`, `strace` process names via `/proc/<pid>/comm` is also common.
### 6. Hardware breakpoint registers (cross-platform)
`GetThreadContext` / `SetThreadContext` (Windows) and `ptrace(PTRACE_GETREGS, ...)` (Linux) reveal whether the debug registers `DR0–DR7` are configured. Apps can periodically read their own context and check for non-zero debug addresses.
### 7. Self-integrity / checksum-over-self
A debugger requires `PROT_EXEC` on the code pages. Apps with self-checksumming can detect when their own `.text` has been modified (e.g. an `INT 3` planted by the analyst) and refuse to continue.
### Confirmation requirements — when a string-table hit is not enough
For each family above, a *string-table hit* alone is sometimes not enough evidence to fire the detection — the string may be a link-time artifact (e.g. C++ exception-fragment names, RTTI fragments, PDB suffix) rather than runtime code. The catalog (see [RE-AI: data/drm-indicators.yaml](https://github.com/Heretek-AI/RE-AI/blob/main/data/drm-indicators.yaml)) classifies each check by confirmation level:
| Confirmation | Meaning | Example |
|---|---|---|
| `string_only` | The string-table presence is sufficient — the API call is at a runtime-fixed address. | (None of the current checks — this is a degenerate case) |
| `import_only` | The binary must *import* the API (a link-time IAT entry); the string alone is not enough. C++ symbol fragments like `_Xlength_error` were the FPs the prior string-only-equal filter produced. | `IsDebuggerPresent`, `CheckRemoteDebuggerPresent`, `OutputDebugString`, `NtQueryInformationProcess` |
| `requires_disasm` | The byte-pattern check (RDTSC, INT 2D, INT 3) must be backed by a disasm hit at a call site. The string-table presence of `"RDTSC"` alone is meaningless. | RDTSC (`0F 31`), INT 2D (`CD 2D`), INT 3 (`CC`), exception-hooking decoy stack writes |
| `requires_xref` | The check requires manual xref — the automated catalog can't count it. | Scattered-bit VM-register storage (a marker of the encrypted-VM bytecode interpreter; see [`07-vm-bytecode-proprietary`](../07-vm-bytecode-proprietary.md)) |
In practice this means: when a triage shows many `import_only` checks firing, the category is real (the binary actually imports the detection API). When a triage shows many `requires_disasm` checks firing, the *count* alone is suggestive but the analyst should confirm at least one disasm site before reporting the finding. The "scattered-bit register storage" check is the most diagnostic single signal — when a binary's `obfuscation.count` includes that specific marker, the proprietary-engine encrypted-VM bytecode interpreter is very likely present.
## Approach
The general workflow, in order of escalation:
1. **Static first.** `strings` the binary for known detection strings (`"gdb"`, `"TracerPid"`, debugger PEB names). Identify the API calls being made.
2. **Attach a tracer and watch.** Run the binary under `strace -f -e trace=ptrace,openat,read` (Linux) or Procmon + DebugDiag (Windows) to see what files / syscalls it uses to detect you. The first detection that fires is usually the right one to bypass.
3. **Patch the binary.** Where the check is a single conditional branch, flipping the branch or NOPing it out works. For `IsDebuggerPresent` and friends, a single-byte patch is often sufficient.
4. **Hook the detection API.** For more sophisticated checks, use a hooking engine (frida, libpatch, custom LD_PRELOAD) to make `ptrace` / `NtQueryInformationProcess` return "no debugger here" regardless of reality.
5. **Anti-anti-anti-debug.** A binary that detects *the detector* (e.g. by hashing the code at a known location) is a tier-2 problem. Solutions include: running from a custom hypervisor, emulating the binary entirely (unicorn), or modifying the binary to be aware of the bypass and patch it back.
## Common pitfalls
- **Patching out one check and missing the next.** Production binaries usually have *several* checks at different points; the first one you see is rarely the only one. A loop in a background thread that re-checks every few seconds is a common trap.
- **Time-based checks interacting with your test harness.** If you pause the process under a debugger to inspect state, you've just tripped the timing check.
- **Self-debugging checks that you can't bypass by hooking ptrace.** If the binary spawns a child that `PTRACE_TRACEME`s, you have to either let the child live (and accept the overhead) or patch the fork.
- **Checksumming the code at runtime.** If the binary computes a hash over its own `.text` and detects that you've patched a check, the patch won't survive long. Either disable the checksum or modify the binary to always report the expected hash.
## Tooling pointers
- [`frida`](../tools/04-frida.md) — runtime hooking across platforms; the standard tool for dynamic bypass.
- [`ghidra`](../tools/01-ghidra.md) — static analysis to find detection routines in advance.
- [`unicorn` / `qiling`](https://github.com/unicorn-engine/unicorn) — CPU emulation when the binary is too hostile to run live.
- [ScyllaHide](https://github.com/x64dbg/ScyllaHide) — Windows-specific anti-anti-debug plugin for x64dbg.
## References
- [Microsoft: NtQueryInformationProcess](https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryinformationprocess)
- [ptrace(2) — Linux man page](https://man7.org/linux/man-pages/man2/ptrace.2.html)
- [The "Ultimate" Anti-Debugging Reference (PDF, Peter Ferrie)](https://pferrie.host22.com/papers/antidebug.pdf)
@@ -0,0 +1,150 @@
---
title: "Encrypted-VM Bytecode Interpreter (Unity IL2CPP target)"
category: "anti-analysis"
platforms: ["windows"]
difficulty: "advanced"
tags: ["vm-protection", "obfuscation", "unity", "il2cpp", "static-analysis", "protection"]
summary: "The observable composition of a register-based bytecode VM that has replaced the binary's real x86 code — section names, W^X .idata, lazy-decrypt entry stub, late-bound export tail, and HWID-vector imports — and the order to triage them in."
updated: "2026-06-06"
related: ["anti-analysis/01-anti-debug", "anti-analysis/07-vm-bytecode-proprietary", "native/02-pe-section-layout", "tools/01-ghidra"]
---
## Summary
An encrypted-VM bytecode interpreter replaces the binary's real x86 code with a register-based virtual machine. A dispatcher fetches handlers from a table; the original code never executes as x86. The recognition signal is not a single feature but a 7-observable composition that fires together — section-name regexes, a W^X (write+execute) giant code-bearing section, a packed-shape `.text`, a tiny entry stub at the PE entry point, a vendor-tagged PDB reference, a late-bound export tail, and an unusually large HWID-vector import set.
## Why this matters
When this composition fires, every static analysis technique that targets the binary's *code* gives the wrong answer: the disassembled function prologues are not the game's code, they are the VM runtime's stub. Decompilation produces noise. Function-boundary recovery produces noise. String extraction over the bytecode sections wastes hours and gigabytes of memory on a payload that is opaque by design.
Three common analyst traps:
- **Spending an hour on a function that turns out to be a handler.** The handler calls a 97-byte trampoline; the trampoline is in a section named `.ecode`; everything above it is the encrypted bytecode.
- **Running `strings` on a 470 MB `.idata` and OOMing.** The bytecode region is dense random-looking data, and a naive strings pass will try to allocate it all at once.
- **Assuming a "no symbols" Unity game is just stripped.** An encrypted-VM bytecode interpreter has the *symbols of the VM runtime*, not the game's. Both look like a stripped binary from the outside; the section shape is what disambiguates.
## Mechanics
The 7-observable composition, in **triage order** (cheapest to most expensive). When 4+ of these fire together, the category is the encrypted-VM bytecode interpreter.
### 1. Section names matching the encrypted-VM family
The PE's section table contains at least four of the seven section-name regexes:
```
\.xtls
\.didata
\.ecode
\.xdata
\.xpdata
\.udata
\.00cfg
```
These are well-known section names that this category uses as containers. The full set is diagnostic; fewer than four is suggestive but not conclusive. The section *names* in the binary are easy to grep for, so this is the cheapest first-pass check.
### 2. W^X on a giant code-bearing section
The largest code-bearing section has R + W + X permissions simultaneously. In PE characteristics:
- `MEM_READ` = 0x40000000
- `MEM_WRITE` = 0x80000000
- `MEM_EXECUTE` = 0x20000000
A 100+ MB `.idata` with all three of those bits set (plus `CNT_CODE`) is the canonical example. The "writable + executable" combination on a section that holds the bulk of the binary's content is the headline signal.
### 3. `.text` with `virtual_size >> raw_size`
The canonical `.text` section has `virtual_size` (in-memory size) much larger than `raw_size` (on-disk size) — 2.2 MB virtual vs 512 raw is a common shape. The loader zero-fills the gap at load time. This is the "large section with tiny text" packed-shape rule, and it indicates the on-disk bytes are stubs that the runtime expands.
### 4. A tiny `.ecode` entry stub
A small (under 200 bytes) section named `.ecode` sits at the PE entry point. It contains a lazy-decrypt routine — typically a 2-instruction page-stride loop that fills a key/material block on first call, gated by a one-byte "done" flag. Disassembling the entry point shows the stub; everything after the first `jmp` out of the stub is the encrypted bytecode.
The stub pattern:
```
; Pseudo-disassembly of a typical .ecode stub
push rax; push rcx; push rdx
cmp byte [done_flag], 0
jne .skip_decrypt
.loop:
mov rdx, [page_ptr]
; ... transform rdx ...
add page_ptr, 0x1000
cmp page_ptr, end_ptr
jl .loop
mov byte [done_flag], 1
.skip_decrypt:
pop rdx; pop rcx; pop rax
jmp vm_entry ; jumps to the real interpreter entry
```
The exact instructions vary, but the shape — three pushes, a flag check, a page-stride loop, a flag set, the inverse teardown, a `jmp` out — is stable.
### 5. Vendor-tagged PDB reference
The PE debug directory references a PDB filename that embeds a name fragment not matching the binary's own basename. The literal string in the COFF debug directory is the easy signal. Vendor-neutral translation: *any* non-matching tag in the PDB reference is the indicator. (The exact product name is the user's call to make, not the corpus's.)
### 6. Late-bound export tail
The exports table ends with a single late-bound entry — a stub the game calls *after* the encrypted-VM bytecode interpreter is initialized. The name typically suggests "wait" or "activation" (e.g. an `WaitForActivation` export). The interpreter is "armed but inert" until this export returns. If your dynamic instrumentation skips the call, the binary may not exhibit the protected behaviour you expect.
### 7. 8+ of the 12 HWID-vector APIs imported
The import table shows 8 or more of the 12 high-signal HWID APIs:
```
CryptAcquireContextW, CryptGenRandom, GetUserNameW, GetComputerNameW,
GetVolumeInformationW, GetAdaptersInfo, GetAdaptersAddresses,
NtQuerySystemInformation, NtQuerySystemInformationEx,
NtQueryInformationProcess, ...
```
The fingerprint-vector set is unusual for a non-protected Unity IL2CPP game. Eight or more of the twelve is the static threshold.
## Approach
1. **Section list first** (cheapest; identifies the category-shape). If 4+ of the 7 section names fire, you are looking at this category.
2. **Imports check** — `re-lief.get_imports_exports` (or `re-rizin.list_imports_exports`). The 8+ HWID APIs confirm. `re-lief.categorize_strings` is the keyword-bucketed equivalent; the `obfuscation` and `hwid` buckets fire on the same data.
3. **Entry-point disassembly** of `.ecode` — `re-lief.disasm_capstone` or `re-rizin.disassemble_function`. The 2-instruction lazy-decrypt stub is the signature.
4. **String search for VM-dispatch markers** — the `obfuscation` bucket from `categorize_strings` should have hits on `dispatch`, `handler`, `vm_entry`, `lookup`.
5. **Do not decompile the entire bytecode section.** The `skip_sections` parameter on `re-lief.categorize_strings` keeps the memory footprint bounded. The encrypted bytecode is opaque by construction; the *control-flow surface* (the table-driven dispatcher) is the analyzable artifact.
## Variants
The Unity-IL2CPP variant described above is the most widely shipped. A second, distinct variant exists in proprietary engines (often AAA cross-platform titles) — the same conceptual category (encrypted-VM bytecode interpreter) with a different observable composition. The proprietary variant is documented in [`07-vm-bytecode-proprietary`](../07-vm-bytecode-proprietary.md) and complements this entry; the table below summarises the differences so a reader scanning both can spot which variant they're looking at.
| Signal | Unity-IL2CPP (this entry) | Proprietary-engine ([`07`](../07-vm-bytecode-proprietary.md)) |
|---|---|---|
| Section set (encrypted-VM containers) | `.xtls` `.didata` `.ecode` `.xdata` `.xpdata` `.udata` `.00cfg` | `.arch` `.xcode` `.xtext` `.sbss` `.link` |
| W^X giant code-bearing section | Yes — `.idata` is often 100+ MB with R+W+X | **No** — bytecode lives in custom sections, not the canonical `.idata` |
| `.text` with `virtual_size >> raw_size` | Yes — the lazy-decrypt stub at the entry point | No — `.text` is normal; the entry is a real CRT prologue + a branchless dispatcher |
| Entry-stub shape | 2-instruction page-stride loop in `.ecode` (3 pushes, flag check, page loop, flag set, 3 pops, `jmp`) | A single `cmova r8, rbx` (or `cmo*` family) followed by an indirect `jmp` into the dispatch table — no lazy-decrypt loop |
| Encrypted VM context location | The `.idata` / `.xtls` / etc. code-bearing section | A `.bss`-shaped section with entropy > 7.0 (BSS that isn't BSS) |
| PDB reference suffix | Typically matches the binary's own basename | A code-integrity module name (`shielding.pdb`, `guard.pdb`, `integrity.pdb`) |
| Launcher activation library | Often co-located, separate layer | Often co-located, separate layer |
The two variants can coexist in the same binary (an IL2CPP-style entry that dispatches into a proprietary-engine VM context). Treat the entries as *complementary*, not competing.
## Common pitfalls
- **Assuming "no symbols" means "stripped".** An encrypted-VM bytecode interpreter has the symbols of the VM runtime, not the game's. Both look like a stripped binary from the outside; the section shape is the disambiguator.
- **Decompiling the bytecode section end-to-end.** The disassembled instructions are encrypted handlers, not game logic. Decompile the *dispatcher* and a small sample of *handlers*, not the whole section.
- **Treating the late-bound entry-point stub as decorative.** It is the handshake with the license server. The interpreter is "armed but inert" until that export returns; if your dynamic instrumentation skips the call, the binary may not exhibit the protected behaviour you expect.
- **Memory blow-up on `strings`.** The `re-lief.categorize_strings` `skip_sections` parameter exists for this case; use it.
## Tooling pointers
- [`ghidra`](../tools/01-ghidra.md) — static analysis; decompile the dispatcher and a small sample of handlers.
- [`re-lief.get_sections`](https://github.com/Heretek-AI/RE-AI) — canonical accessor for the section table.
- [`re-lief.categorize_strings`](https://github.com/Heretek-AI/RE-AI) — keyword-bucketed strings dump; pass `skip_sections=[".idata", ".xtls", ".xpdata", ".udata", ".xdata", ".didata", ".ecode", ".00cfg"]` to skip the encrypted-VM bytecode regions on a 500+ MB binary.
- [`re-rizin.list_imports_exports`](https://github.com/Heretek-AI/RE-AI) — for the 8+ HWID API confirmation.
- [`re-rizin.disassemble_function`](https://github.com/Heretek-AI/RE-AI) — for the entry-stub disassembly.
- [`native/02-pe-section-layout`](../native/02-pe-section-layout.md) — primer on the PE section table that this entry depends on.
## References
- [RE-AI: ANTI-TAMPER-TAXONOMY.md](https://github.com/Heretek-AI/RE-AI/blob/main/ANTI-TAMPER-TAXONOMY.md) — "Recognizing the patterns in arbitrary binaries" section.
- [Microsoft PE/COFF specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) — for the section characteristics bits and the debug directory.
- [Wine PE documentation](https://wiki.winehq.org/PE_Format) — a readable summary of the same format.
@@ -0,0 +1,122 @@
---
title: "Launcher Activation Library — Fingerprinting + Anti-Debug"
category: "anti-analysis"
platforms: ["windows"]
difficulty: "advanced"
tags: ["hwid", "fingerprint", "launcher", "activation", "ordinal-exports", "protection", "static-analysis"]
summary: "A small native DLL with ordinal-only exports that gates launch on a license-server round-trip and a host fingerprint — the WinHTTP + OpenSSL + X.509 import catalogue, the high-signal HWID-vector API set, the split anti-debug surface between the activation DLL and the game DLL, and how to read the strings dump to confirm."
updated: "2026-06-05"
related: ["anti-analysis/01-anti-debug", "anti-analysis/02-vm-bytecode-interpreter", "native/02-pe-section-layout", "tools/01-ghidra"]
---
## Summary
A launcher activation library is a small native DLL (1-3 MB) sitting alongside the main game binary, gating launch on a license-server round-trip and a host fingerprint. The activation library and the game library are *separate* layers; the launcher `.exe` is the glue. The composition is distinct from any encrypted-VM bytecode interpreter in the game DLL — and the two often coexist, layering on the same install.
## Why this matters
This composition is the *license-gate* layer, separate from any protection embedded inside the game DLL. Skipping it means a static review of the game DLL alone misses the entire host-fingerprint + license-server surface. The activation library is also where most of the Win32 anti-debug primitives live — `IsDebuggerPresent`, `OutputDebugStringW`, `NtQueryInformationProcess` — so a triage that looks only at the game DLL's anti-debug will miss the activation library's contribution.
A caveat: the composition isn't intrinsically malicious. Many legitimate launchers look like this; many malware droppers do too. The category is descriptive of a *structural shape*, not a verdict. Treat the result as a recommendation to read the activation library carefully, not a flag for maliciousness.
## Mechanics
The 7-observable composition that fires together. When 5+ of these are present on a single small native DLL, the category is the launcher activation fingerprinting routine.
### 1. Ordinal-only exports on the activation DLL
A 1-3 MB native DLL with **ordinal-only exports** — `@100`, `@101` — no symbol names. Exports are deliberately stripped; the names would help an analyst identify the role of each export. The launcher `.exe` knows the ordinals (it imports them by ordinal), but a third-party reader of the binary sees only `@100`.
### 2. Launcher imports only 2-3 ordinals from the activation DLL
The launcher `.exe` imports only 2-3 ordinals from the activation DLL. Nothing else. The DLL is opaque to the launcher. A typical launcher `.exe` is 500-800 KB with 5-15 total imports; the bulk of the imports are to the activation DLL by ordinal.
### 3. The activation DLL statically links a recognizable crypto library
The activation DLL statically links a crypto library. The fingerprint is the `.\crypto\...` path fragments in `.rdata` — 1,000+ of them on a typical build, with names like `.\crypto\asn1\a_bitstr.c`, `.\crypto\rsa\rsa_gen.c`, `.\crypto\evp\e_aes.c`. OpenSSL is the most common (look for `EVP_*`, `RSA_*`, `X509*`, `PKCS*`, `BIO_*`, `PEM_*` substrings); mbedTLS and BoringSSL use different but recognizable substring sets.
### 4. WinHTTP + X.509 / Authenticode imports
The activation DLL's import table shows WinHTTP plus the X.509 / Authenticode APIs:
```
WinHttpOpen, WinHttpConnect, WinHttpOpenRequest, WinHttpSendRequest,
WinHttpReceiveResponse, WinHttpQueryHeaders, WinHttpReadData,
WinHttpCrackUrl, WinHttpSetOption, WinHttpSetCredentials,
WinHttpSetStatusCallback, WinHttpCloseHandle
CryptQueryObject, PFXImportCertStore, CertFindCertificateInStore,
CryptMsgGetParam, WinVerifyTrust
```
WinHTTP is the license-server HTTP client. The X.509 / Authenticode APIs are the cert validation chain. Together, they're the license-gate's network-and-crypto surface.
### 5. 8+ of the 12 HWID-vector APIs imported
The activation DLL imports 8 or more of the 12 high-signal HWID APIs:
```
GetComputerNameW, GetUserNameW, GetVolumeInformationW, GetSystemDirectoryW,
GetWindowsDirectoryW, GetAdaptersInfo, GetAdaptersAddresses,
GetNetworkParams, NtQuerySystemInformation, NtQuerySystemInformationEx,
CryptAcquireContextW, CryptGenRandom
```
This is the *fingerprint* set — the APIs the activation routine calls to assemble a host fingerprint before sending it to the license server.
### 6. Anti-debug primitives imported
The activation DLL imports the catalog's anti-debug primitives:
```
IsDebuggerPresent (kernel32)
OutputDebugStringW (kernel32)
NtQueryInformationProcess (ntdll)
```
This is the activation library's anti-debug surface. The combination of these three (and the absence of other anti-debug tricks) is the typical activation-library signature.
### 7. Strings dump shows the activation + obfuscation categories
`re-lief.categorize_strings` populates the `activation` and `obfuscation` buckets with non-trivial counts on a typical activation DLL (50-200 strings each on a 3 MB binary). The `activation` bucket surfaces the `Activate`, `License`, `Entitlement`, `Recipient`, `SignedData`, `EnvelopedData` PKCS#7 / CMS strings; the `obfuscation` bucket surfaces the `decrypt`, `dispatch`, `handler` markers from the embedded OpenSSL build.
## The split anti-debug surface
**Important observation**: when a Unity IL2CPP game is layered with an encrypted-VM bytecode interpreter *and* a launcher activation library, the anti-debug primitives are typically *split* between the two DLLs:
- The **activation DLL** owns the Win32 anti-debug APIs (`IsDebuggerPresent`, `OutputDebugStringW`, `NtQueryInformationProcess`).
- The **game DLL** (encrypted-VM wrapped) owns the VM-encrypted anti-debug. The strings from the encrypted bytecode are not directly grep-able, but the `obfuscation` bucket in `re-lief.categorize_strings` surfaces the VM-side anti-debug fingerprints.
When you see anti-debug primitives split between two DLLs in the same install directory, both layers are firing. A triage that runs `categorize_strings` on only one DLL will miss the other.
## Approach
1. **Launcher `.exe` import list.** The 2-3 ordinal-only imports from a single sibling DLL are the category-shape signal.
2. **Activation DLL export list.** `@100` / `@101` with no names confirms the opacity-by-design.
3. **Activation DLL import list** (WinHTTP + X.509 + 8+ HWID APIs + anti-debug primitives). This is the category confirmation.
4. **Strings dump** — `re-lief.categorize_strings` for `activation.count` (≥ 50) and `crypto.count` (≥ 100); `obfuscation.count` and `anti_debug.count` for the cross-check.
5. **Cross-reference the game DLL's imports** for the same HWID set. If the game DLL also imports 8+ of the 12, the binary is in *both* the encrypted-VM bytecode interpreter category and the hardware-fingerprinting routine category. This *layered* composition is the common Unity IL2CPP shipped-binary shape.
## Common pitfalls
- **Treating the launcher `.exe` as the binary to analyze.** The launcher is a 5-import forwarder; the analysis happens on the activation DLL and the game DLL.
- **Missing the split anti-debug surface.** Run `re-lief.categorize_strings` on *both* DLLs separately; the union of the `anti_debug` and `obfuscation` buckets is the full surface.
- **Assuming "ordinal-only" = "packed" or "malicious".** Many legitimate launchers ship this way; the *combination* with WinHTTP + OpenSSL + HWID APIs is the category signal, not the ordinal exports alone.
- **Missing the late-bound entry-point handshake.** The activation DLL is called *before* the encrypted-VM bytecode interpreter completes initialization. Your dynamic instrumentation must respect this order; if the launcher calls the activation library synchronously and the interpreter waits for a return, your breakpoint sequence needs to mirror that.
- **Treating the activation library as a one-time call.** Some launchers call the activation library on *every* game launch; some cache the result for N days; some re-check on every level transition. The cadence affects the dynamic instrumentation strategy.
## Tooling pointers
- [`ghidra`](../tools/01-ghidra.md) — static decompilation of the activation DLL.
- [`anti-analysis/02-vm-bytecode-interpreter`](../anti-analysis/02-vm-bytecode-interpreter.md) — the encrypted-VM family the activation library is *separate from*; both layers can coexist.
- [`native/02-pe-section-layout`](../native/02-pe-section-layout.md) — primer on the ordinal-only-exports + WinHTTP + X.509 import surface.
- [`re-lief.categorize_strings`](https://github.com/Heretek-AI/RE-AI) — keyword-bucketed strings dump; no `skip_sections` (the activation DLL is small enough to walk in full).
- [`re-rizin.list_imports_exports`](https://github.com/Heretek-AI/RE-AI) — for the WinHTTP + X.509 + HWID import confirmation.
- [`re-rizin.analyze_function`](https://github.com/Heretek-AI/RE-AI) — to identify the export functions' callers (the launcher calls the activation library's ordinals; this is the disassembly of those callsites).
## References
- [RE-AI: ANTI-TAMPER-TAXONOMY.md](https://github.com/Heretek-AI/RE-AI/blob/main/ANTI-TAMPER-TAXONOMY.md) — "Recognizing the patterns in arbitrary binaries" section.
- [OpenSSL source](https://github.com/openssl/openssl) — for the `.\crypto\...` substring patterns; the version-string pattern is the static-link fingerprint.
- [Microsoft: WinHTTP](https://learn.microsoft.com/en-us/windows/win32/winhttp/winhttp-start-page) — for the WinHTTP API family.
- [Microsoft: WinVerifyTrust](https://learn.microsoft.com/en-us/windows/win32/api/wintrust/nf-wintrust-winverifytrust) — for the Authenticode / X.509 validation surface.
@@ -0,0 +1,140 @@
---
title: "Unity IL2CPP Game Triage Workflow"
category: "anti-analysis"
platforms: ["windows", "linux", "macos"]
difficulty: "intermediate"
tags: ["unity", "il2cpp", "triage", "static-analysis", "metadata", "global-metadata"]
summary: "A first-pass workflow for triaging an unknown Unity IL2CPP game — confirm the IL2CPP runtime, recover the publisher's class graph from the unprotected global-metadata.dat, identify the protection layers, and decide which category-level entries apply."
updated: "2026-06-05"
related: ["anti-analysis/01-anti-debug", "anti-analysis/02-vm-bytecode-interpreter", "anti-analysis/05-launcher-activation-fingerprinting", "tools/01-ghidra"]
---
## Summary
Unity ships two managed runtimes: Mono (C# → CIL → JIT) and IL2CPP (C# → C++ → AOT). Most commercial Unity games use IL2CPP for performance and to make dynamic code-analysis tooling less effective. Triaging a Unity IL2CPP game means *first* recovering the publisher's class graph (which the runtime leaves unprotected), *then* identifying the protection layers around the AOT-compiled C++ binary.
## Why this matters
`global-metadata.dat` is **unprotected**. The magic at offset 0x00 is `0xFAB11BAF`. The version at offset 0x04 is 24-29 in modern Unity. The file is 10-20 MB. It contains 400,000+ string entries covering the publisher's full class graph — every Assembly-CSharp class, every method name, every field name, every type namespace.
The publisher's class graph is recoverable *without* defeating any protection layer. The AOT binary is encrypted-VM-wrapped, stripped, or otherwise hostile to static analysis; the metadata is sitting in plain text in a sibling file.
A common analyst mistake is to spend hours reverse-engineering `GameAssembly.dll` function prologues when the same information is sitting in the metadata file. Recover the class graph first; use it as a map for the AOT binary analysis.
## Mechanics
### The Unity IL2CPP install layout
A typical Unity IL2CPP install on Windows:
```
<install_dir>/
├── GameAssembly.dll # AOT-compiled C++ (50-500 MB; size is a signal)
├── UnityPlayer.dll # The Unity engine (28 MB on this version)
├── baselib.dll # Unity's low-level OS shims (~500 KB)
├── dbdata.dll # Unity Burst helper (~1 MB)
├── UnityCrashHandler64.exe # Unity's crash reporter
├── Game.exe # The launcher (5 imports; small forwarder)
├── Core/
│ ├── Activation64.dll # The launcher activation library (3 MB; ordinal-only exports)
│ └── Activation.dll # 32-bit sibling of Activation64.dll
└── Game_Data/
├── app.info # 26 bytes, build id
├── boot.config # 120 bytes, gfx/GC settings
├── globalgamemanagers # ~340 KB
├── globalgamemanagers.assets
├── globalgamemanagers.assets.resS
├── il2cpp_data/
│ └── Metadata/
│ └── global-metadata.dat # 10-20 MB; THE unprotected class graph
├── level0..levelN # Unity level bundles
├── resources.assets
└── sharedassets*.assets
```
The same layout works on Linux (`.so` for `GameAssembly`, `GameAssembly.so`) and macOS (`.dylib`). The protection composition is identical across platforms.
### The AOT binary size is a signal
A normal Unity IL2CPP game is 30-80 MB. An encrypted-VM bytecode-wrapped one is 200+ MB. The size delta is the encrypted bytecode region in the W^X `.idata` (or the equivalent in the platform-specific section name set). Anything over 100 MB on Windows is a *signal* to check the section list for the 7-observable composition in the encrypted-VM bytecode interpreter entry.
### The metadata header
```
offset field meaning
------ ----- -------
0x00 sanity 0xFAB11BAF (little-endian) — the unprotected magic
0x04 version 24..29 in modern Unity; 24=Unity 2019, 27=2020, 29=2021+
0x08 stringLiteralDataOffset
0x0C stringLiteralDataCount
0x10 stringOffset
0x14 stringCount typically 400,000+
```
If the magic is `0xFAB11BAF`, the metadata is unprotected. There is no encryption / no obfuscation to defeat.
### The class graph table
The metadata's `typeDefinitions` table is the publisher's class graph. Walk it with `re-il2cpp.get_type_definitions` to enumerate every type. The publisher's `Assembly-CSharp` is one image; standard Unity / Asset-Store libraries (`UnityEngine.*`, `Unity.Mathematics.*`, `Rewired.*`, `Cinemachine`, `Aura2_Core`, etc.) are other images. Knowing which is which is the triage result.
A typical publisher namespace:
```
<Publisher>.Core (the primary class library)
<Publisher>.Hierarchy (scene-graph helpers)
<Publisher>.Performance (profiling / optimization)
<Publisher>.Editor (editor-only utilities; stripped from builds)
```
Namespaces that are publisher-specific (not standard Unity / Asset-Store) are the *publisher's* code. Filter out the rest.
### Sibling files that signal protection layers
- `Core/Activation*.dll` — the launcher activation fingerprinting library (3 MB; ordinal-only exports). See the launcher activation entry.
- `__Installer/Cleanup.exe` and `__Installer/Touchup.exe` — installer clean-up utilities. Not protection; just installer scaffolding.
- `Support/` — localized EULA and help docs. Not protection; not part of the triage.
### The protection-layer map
Once you've found the metadata, walked the class graph, and checked the AOT binary size, you have a *protection-layer map*:
| Observation | Implied layer | Follow-up entry |
|---|---|---|
| 4+ of the 7 encrypted-VM section names; 8+ HWID APIs in the AOT binary; `WaitForActivation`-style export tail | Encrypted-VM bytecode interpreter | `anti-analysis/02-vm-bytecode-interpreter` |
| `Core/Activation*.dll` present; 1-3 MB; ordinal-only exports; 8+ HWID APIs in the activation DLL | Launcher activation fingerprinting | `anti-analysis/05-launcher-activation-fingerprinting` |
| Both | Layered — both categories are firing | Both entries |
| Neither | Plain Unity IL2CPP, no protection layer | Triage complete; static analysis is straightforward |
## Approach
1. **Find the metadata** — `find . -name 'global-metadata.dat'`. Read the magic at offset 0 and the version at offset 4. If the magic is `0xFAB11BAF`, the metadata is unprotected.
2. **Recover the class graph** — `re-il2cpp.list_namespaces`, `re-il2cpp.get_type_definitions`, `re-il2cpp.get_assembly_types`. The publisher's `Assembly-CSharp` is the surface; the standard Unity / Asset-Store libraries are noise to filter out.
3. **Check the AOT binary size** — anything over 100 MB on Windows is a *signal*; check the section list for the 7-observable composition.
4. **Check for sibling activation libraries** — `find . -name '*.dll' -size -5M -size +500k` is a rough heuristic. Anything matching is the launcher activation fingerprinting library.
5. **Run `re-lief.categorize_strings`** on every DLL in the install. The combined `by_category` output is the protection-layer map.
6. **Defer decompilation of the AOT binary** until you know which protection layer(s) you're looking at. Decompiling a 500 MB VM-wrapped binary without first identifying the VM dispatcher is the analysis trap.
## Common pitfalls
- **Skipping the metadata** because the binary is 500 MB and the analysis queue is long. The metadata is *the cheapest signal you'll get*.
- **Treating the AOT binary as a normal PE/ELF/Mach-O.** The section names, characteristics, and import patterns may be shaped by the protection layer, not by the Unity runtime.
- **Conflating "Unity game" with "no protection".** Most commercial Unity games ship with at least one anti-tamper layer; the question is which one(s), not whether.
- **Forgetting cross-platform builds.** A Windows-`GameAssembly.dll` is the same protection composition as a Linux-`GameAssembly.so`; if the analyst has both, diffing the two is a structural characterization of the protection layer.
- **Walking the typeDefinitions table for a 530 MB binary directly.** `re-il2cpp.get_type_definitions` reads the metadata, not the AOT binary — the typeDefs table is in the unprotected metadata file. Don't try to extract types from the AOT binary; extract them from the metadata.
## Tooling pointers
- [`re-il2cpp.check_il2cpp`](https://github.com/Heretek-AI/RE-AI) — confirm the magic and version.
- [`re-il2cpp.list_namespaces`](https://github.com/Heretek-AI/RE-AI) — enumerate the namespace surface.
- [`re-il2cpp.get_type_definitions`](https://github.com/Heretek-AI/RE-AI) — walk the publisher's class graph.
- [`re-lief.get_sections`](https://github.com/Heretek-AI/RE-AI) — AOT binary section table.
- [`re-lief.categorize_strings`](https://github.com/Heretek-AI/RE-AI) — keyword-bucketed strings dump.
- [`ghidra`](../tools/01-ghidra.md) — AOT decompilation when the protection layer is known.
- [`anti-analysis/02-vm-bytecode-interpreter`](../anti-analysis/02-vm-bytecode-interpreter.md) and [`anti-analysis/05-launcher-activation-fingerprinting`](../anti-analysis/05-launcher-activation-fingerprinting.md) — for the protection-layer follow-up.
## References
- [RE-AI: ANTI-TAMPER-TAXONOMY.md](https://github.com/Heretek-AI/RE-AI/blob/main/ANTI-TAMPER-TAXONOMY.md) — for the category vocabulary.
- [Unity Manual: IL2CPP](https://docs.unity3d.com/Manual/IL2CPP.html) — for the official IL2CPP overview.
- [Il2CppDumper](https://github.com/Perfare/Il2CppDumper) — a community tool for IL2CPP metadata / binary parsing; useful as a third-party reference for the metadata format.
- [Perfare/Il2CppDumper metadata format](https://github.com/Perfare/Il2CppDumper/blob/master/Il2CppDumper/Metadata.cs) — a community-clarified walkthrough of the metadata table layout.
@@ -0,0 +1,118 @@
---
title: "Encrypted-VM Bytecode Interpreter (Proprietary-Engine Target)"
category: "anti-analysis"
platforms: ["windows"]
difficulty: "advanced"
tags: ["vm-protection", "obfuscation", "proprietary-engine", "static-analysis", "protection"]
summary: "The proprietary-engine variant of the encrypted-VM bytecode interpreter — a different section set (no W^X giant code-bearing section), a branchless cmova/cmo-based dispatcher instead of the IL2CPP table-walk, an anomalous .sbss entropy signal (BSS that is encrypted VM context), and a 'shielding' PDB suffix pattern that points to a code-integrity module."
updated: "2026-06-06"
related: ["anti-analysis/02-vm-bytecode-interpreter", "anti-analysis/05-launcher-activation-fingerprinting", "anti-analysis/01-anti-debug", "native/02-pe-section-layout", "tools/01-ghidra"]
---
## Summary
The proprietary-engine variant of the encrypted-VM bytecode interpreter has a different observable composition than the Unity-IL2CPP variant in [02-vm-bytecode-interpreter](../02-vm-bytecode-interpreter.md). Where the IL2CPP variant hides the bytecode in a giant W^X `.idata` and uses a 2-instruction lazy-decrypt entry stub, the proprietary variant keeps the bytecode in a custom section set (`.arch`/`.xcode`/`.xtext`/`.sbss`/`.link`) and dispatches it via a *branchless* `cmova r8, rbx` (or `cmo` family) instead of the IL2CPP table-walk. The "encrypted VM context" lives in a section that pretends to be BSS — its entropy of 7.3-7.5 is the unique tell. The same binary can also be hosting a launcher activation library ([05](../05-launcher-activation-fingerprinting.md)); the two layers coexist.
## Why this matters
The IL2CPP variant's giant `.idata` is a *visually obvious* signal. The proprietary variant is harder to spot because the section names look like a custom (but plausible) section layout — `.arch`/`.xcode`/`.xtext`/`.sbss` aren't in any widely-shared packer / protector section-name list. A naive search for "the 7 IL2CPP section names" will miss this category entirely. The three diagnostic signals that *do* fire are:
- The branchless dispatcher at the entry point (not a table-walk).
- The `.sbss` (or "uninitialized data") section with entropy > 7.0 — BSS in a normal binary has entropy < 0.5 (zero-init memory).
- A PDB reference whose suffix is `shielding.pdb` (or similar — a code-integrity module name).
When these three fire together, the category is the proprietary-engine variant of the encrypted-VM bytecode interpreter.
## Mechanics
The 3-observable composition, in **triage order**:
### 1. The branchless `cmova` / `cmo` dispatcher at the entry point
A function at offset 0x15025c6ca (a typical entry) with a single `cmova r8, rbx` followed by a `jmp` into the dispatch table. The `cmova` selects between two VM handler pointers based on a flag — the *register state* of a recent call, not a data-driven conditional branch. The disassembly is something like:
```asm
; Pseudo-disassembly of a typical proprietary-engine entry0
push rbp
mov rbp, rsp
sub rsp, 0x60
mov rax, [rel ctx_state]
cmova r8, rbx ; <-- branchless dispatcher
mov rax, [r8 + rax*8 + 0x40]
jmp rax ; indirect jump into the bytecode handler
```
The shape — a single `cmova` (or `cmo*` family) followed by an indirect `jmp` — is the signature. There's no "table-walk" here: the dispatch is one indirect branch, branchless on the flag state. Static disassembly that tries to follow the indirect jump will land in the encrypted bytecode region.
### 2. The anomalous `.sbss` entropy signal
The proprietary engine's "encrypted VM context" lives in a section that *looks* like BSS (`.bss`, `.sbss`, or a custom section the linker was told was BSS-style). BSS in a normal binary has entropy < 0.5 — the loader zero-fills it at load time, so the on-disk bytes are all-zero.
The proprietary variant's "BSS" section has entropy **7.0 to 7.9** — the same range as code or compressed data. This is a *load-bearing* tell: it's the only signal that requires no parsing, just `re-lief.get_sections` + an entropy read per section.
```python
# Quick triage — entropy per section
import lief
binary = lief.parse("target.exe")
for s in binary.sections:
h = s.entropy # Shannon entropy of the section's on-disk bytes
if s.name in {".bss", ".sbss"} and h > 4.0:
print(f" {s.name}: entropy {h:.2f} ← BSS-shaped but high-entropy")
```
The 7.0+ entropy is the canonical Pattern C signal. (Note: `.sbss` is a C++-specific BSS variant — short for "section BSS". Some toolchains name this `.bss.sbss` or merge it into `.bss`. The signal is the same: a BSS-shaped section with high entropy.)
### 3. A "shielding" PDB reference
The PE debug directory references a PDB filename whose **suffix** is `shielding.pdb` (or similar — a code-integrity module name). The literal string in the COFF debug directory is the easy signal. Vendor-neutral translation: any non-matching tag that suggests a *code-integrity* module (the suffix contains "shield", "guard", "integrity", "code", "verify", "tamper", "license" — these are the publisher-internal naming patterns). The exact product name is the user's call to make, not the corpus's.
```python
# Quick triage — PDB suffix
import re
import lief
binary = lief.parse("target.exe")
for dbg in binary.debug_entries:
if dbg.type == lief.PE.DebugEntry.Type.CODEVIEW:
pdb_path = dbg.path # e.g. "C:\\builds\\WindowsLauncher_shielding.pdb"
suffix = pdb_path.rsplit("\\", 1)[-1]
if re.search(r"(shield|guard|integrity|code.?verify|tamper|license)", suffix, re.I):
print(f" {pdb_path} ← code-integrity PDB suffix")
```
The PDB reference itself is a literal string in the binary, so this check is one `grep` away.
## Approach
The triage workflow, in order of escalation:
1. **Section list + entropy** — `re-lief.get_sections` (per-section entropy). Identify the `.bss` / `.sbss` / custom-BSS-shaped section with entropy > 7.0. One signal: the proprietary variant is present.
2. **Entry-point disassembly** — `re-rizin.disassemble_function` (or `re-lief.disasm_capstone`) on the function at offset 0x1000 (or wherever the entry point is). Look for a `cmova`/`cmo*` + indirect `jmp` pair. Two signals: confirm.
3. **PDB reference grep** — search the COFF debug directory for a PDB whose suffix is `shielding.pdb` (or any of the code-integrity naming patterns above). Three signals: confirm.
4. **Do not decompile the encrypted VM context region** — the `.sbss`-shaped section is opaque by construction; the *dispatcher surface* (the one branchless `cmova` + indirect `jmp` per function entry) is the analyzable artifact. Lifting the dispatcher is enough to understand the VM's control flow; lifting the bytecode region is wasted effort.
5. **The encrypted-VM bytecode interpreter and the launcher activation library can coexist.** A 1-3 MB sibling DLL with ordinal-only exports + WinHTTP + OpenSSL + a high HWID-vector import set is the [launcher activation library](../05-launcher-activation-fingerprinting.md). It's a separate layer; treat it as such.
## Common pitfalls
- **Mistaking the proprietary variant for a normal binary with a custom section layout.** Custom section names are normal in C++ projects that use `__declspec(allocate)` or `-fdata-sections`. The signal is the *combination* of (a) `.bss`-shaped section with entropy > 7.0, (b) a branchless dispatcher, (c) a code-integrity PDB suffix — any one alone is suggestive, all three together is conclusive.
- **Assuming a low `.bss` entropy is a "small" binary.** BSS entropy is always low in a normal binary (zero-init memory). A high `.bss` entropy is the *diagnostic* signal, not a "this binary is compressed" signal.
- **Decompiling the `.sbss` region.** The disassembled instructions are the encrypted VM context — opaque by construction. The *dispatcher* (one `cmova` + `jmp` per entry) is what you analyze. Decompile a few hundred functions, not the whole region.
- **Treating the PDB reference as a comment.** The literal PDB string in the COFF debug directory is a high-signal string; don't ignore it. A `shielding.pdb` / `guard.pdb` / `integrity.pdb` suffix is the single most diagnostic 30-byte string in the binary.
- **The IL2CPP and proprietary variants share some surface.** Both are encrypted-VM bytecode interpreters. Both have a dispatcher at the entry point. Both have high entropy in their bytecode region. The differences are *which* section set, *which* dispatcher style, and *what* PDB suffix. Don't conflate the two.
## Tooling pointers
- [`native/02-pe-section-layout`](../native/02-pe-section-layout.md) — primer on the PE section table and the entropy signal that this entry depends on.
- [`anti-analysis/02-vm-bytecode-interpreter`](../02-vm-bytecode-interpreter.md) — the Unity-IL2CPP variant. The proprietary variant complements it; together they cover the two observable compositions in the wild.
- [`anti-analysis/05-launcher-activation-fingerprinting`](../05-launcher-activation-fingerprinting.md) — the launcher activation library that often sits alongside the encrypted-VM bytecode interpreter in the same install. Different layer; different observable composition.
- [`anti-analysis/01-anti-debug`](../01-anti-debug.md) — the anti-debug surface (split between the activation library and the game DLL).
- [`ghidra`](../tools/01-ghidra.md) — for static analysis; decompile the dispatcher, not the bytecode region.
- `re-lief.get_sections` (via [RE-AI](https://github.com/Heretek-AI/RE-AI)) — per-section entropy read.
- `re-rizin.disassemble_function` (via [RE-AI](https://github.com/Heretek-AI/RE-AI)) — for the entry-point disassembly.
- `re-rizin.search_bytes` (via [RE-AI](https://github.com/Heretek-AI/RE-AI)) — to grep for the PDB-suffix string `shielding.pdb` (or your preferred code-integrity naming pattern).
## References
- [RE-AI: ANTI-TAMPER-TAXONOMY.md](https://github.com/Heretek-AI/RE-AI/blob/main/ANTI-TAMPER-TAXONOMY.md) — "Recognizing the patterns in arbitrary binaries" section, the Pattern C (proprietary-engine variant) fire rule.
- [RE-AI: data/drm-indicators.yaml](https://github.com/Heretek-AI/RE-AI/blob/main/data/drm-indicators.yaml) — the catalog; this entry is the prose form of the `section_indicators` rules for the proprietary variant.
- [Microsoft PE/COFF specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) — for the section characteristics and the debug directory.
- [Shannon entropy on bytes](https://en.wikipedia.org/wiki/Entropy_(information_theory)) — for the entropy signal in section 2.
@@ -0,0 +1,102 @@
---
title: "CDM Architecture"
category: "drm"
platforms: ["android", "ios", "linux", "windows", "macos", "web"]
difficulty: "advanced"
tags: ["architecture", "cryptography", "fundamentals"]
summary: "The generic shape of a Content Decryption Module: the OEMCrypto boundary, the license-acquisition flow, the key-ladder, and the parts that move between hardware and software."
updated: "2026-06-06"
related: ["drm/02-publisher-internal-telemetry-relay"]
---
## Summary
A Content Decryption Module (CDM) is the client-side component that turns an encrypted media stream and a license into viewable frames. The shape is remarkably consistent across the major systems: an HTTP-based license dance, a key-ladder that derives per-session keys from a device-bound root, a sandboxed crypto core, and a hardware/software boundary that the security level of the deployment depends on.
> **Note on scope.** This entry describes the *architecture* — the protocol shape, the key-ladder, the hardware/software boundary — without naming specific systems. Readers who work in the space will recognise which system each paragraph is about. The library's content policy (see [CONTRIBUTING.md](../../CONTRIBUTING.md)) explains why.
## Why this matters
If you understand the CDM shape, you understand the *attack surface* the security model is trying to defend. The choices about where keys live, which side does the decryption, and what's attested to the server, all flow from this shape. Without it, every other DRM entry in this library reads like folklore.
## Mechanics
### The high-level components
A typical CDM has three logical pieces:
1. **The client library** (in the browser engine, in the media player, in the OS framework). Talks to the server, holds session state, hands encrypted samples to the CDM core.
2. **The CDM core** (sometimes called the *CDM*, sometimes the *OEMCrypto* module, sometimes a *vendor-specific binary*). Receives encrypted samples, returns clear samples. Holds the keys.
3. **The key-store / TEE bridge**. Where the device-bound root keys live. May be a TEE (ARM TrustZone, SGX enclave), a hardware secure element, or — at the lowest security level — the CDM core's own obfuscated binary.
The boundary between (2) and (3) is the security boundary. The CDM core is treated as untrusted by the key-store; the key-store only ever releases a *key handle* that the CDM core uses to decrypt specific samples. The CDM core never holds the raw device key.
### The license flow
1. The client requests a license from the license server, including a *license request* that contains a device identifier, the content ID, and (for higher security levels) an attestation token.
2. The license server validates the device/attestation, mints a license, and returns it. The license contains the *content key*, optionally wrapped in something device-specific.
3. The CDM unwraps the content key, derives per-sample keys via the key-ladder, and uses them to decrypt the media as it streams.
The license request is the place where attestation is performed — see [`drm/05-attestation`](05-attestation.md) for the chain of trust.
### The key-ladder
Keys are derived in tiers:
- A device-bound **root key** lives in the key-store. Never leaves the TEE.
- A **service key** is bound to a specific service (e.g. "the streaming app"). Server-side, this gates whether a device can play content from a particular service.
- A **content key** is the symmetric key for one piece of content. The license binds it to a particular device, period of validity, and policy (e.g. "no HD", "no offline").
- **Sample keys** (often per-segment IVs combined with a derived key, or per-sample subkeys) are used to decrypt the actual media frames.
Each tier is wrapped by the tier below. A compromise of the content key compromises only that piece of content; a compromise of the device-bound root key compromises the whole device.
### The OEMCrypto boundary
The CDM core talks to the key-store through a small, audited interface — sometimes called *OEMCrypto*, sometimes *CDM host*, sometimes simply the "keybox" API. The interface is roughly:
```
oemcrypto_open_buffer(handle, key_handle, ciphertext, iv, dst) -> plaintext_or_error
oemcrypto_get_key_handle(session_id, key_id) -> key_handle
oemcrypto_close_buffer(handle) -> status
```
The CDM core passes *encrypted* sample data and a *key handle* (not the key) to the key-store. The key-store decrypts inside its secure world and returns the cleartext. The CDM core can copy the cleartext into the media pipeline (or, at higher security levels, hand it directly to a hardware decoder path so the cleartext is never visible to the application CPU at all).
### The hardware/software boundary
This is what security levels measure. The canonical tier model (L1 / L2 / L3 / L4 in the industry-standard classification) is summarised inline below; a deeper dive is the standard text on the topic. Briefly:
- At the highest level, the entire CDM core — key-ladder, decryption, even sometimes the license parser — runs inside the TEE, and the cleartext is handed straight to a hardware decoder that draws it to a hardware-protected video path.
- At the lowest level, the CDM core is a software binary on the main CPU, the key-store is obfuscated but inspectable, and the cleartext is just a buffer in the application's address space.
The boundary defines the difficulty of *any* attack on the system. Higher levels require either compromising the TEE, finding a protocol-level bug, or going around the device (server-side mitigations). Lower levels can be attacked by patching the software CDM, or by attaching a debugger to the application and reading the cleartext buffer.
## Approach
If you're studying a CDM for defensive research, hardening, or to understand the threat model:
1. **Map the components.** Identify which binary / library / module implements the client library, the CDM core, and the key-store. On Android, this is often three separate `.so` files; on the web, the CDM core may be a sandboxed NaCl/wasm module.
2. **Trace the license flow.** With a network capture, watch the license request and license response. The size, structure, and the specific fields (especially any opaque blobs) tell you what the server is asserting.
3. **Find the OEMCrypto boundary.** On Android, the `liboemcrypto.so` interface is a small, well-defined JNI surface; on iOS, the analogous interface is the *voucher* and *vendor proprietary* libraries. Listing the exports is the first step.
4. **Identify the security level.** The tier classification (L1 / L2 / L3 / L4 in the industry-standard model) is described in most public CDM documentation; the canonical reference is the latest published specification. Most commercial deployments cluster at one of three tiers; identifying which one you're looking at collapses the rest of the analysis.
5. **Read the protocol shape, not the protocol names.** The protocol is rarely documented publicly, but the *shape* — license request, license response, key-ladder, sample decryption — is. See [`drm/03-key-ladders`](03-key-ladders.md) and [`drm/04-proxy-relay`](04-proxy-relay.md).
## Common pitfalls
- **Conflating "the protocol" with "the implementation".** The protocol is what the server expects; the implementation is what runs in the client. Both have bugs; the interesting attacks often exploit the gap between them.
- **Ignoring the server side.** The hardest part of modern DRM is the *server-side* validation: device attestation, licence-binding, anomaly detection. A protocol-level weakness in the client can be mitigated by the server refusing to issue licenses.
- **Assuming a single TCB.** A typical device has multiple TEEs (one per SoC vendor, sometimes one per OS), and the CDM may bridge between them. The actual trusted computing base is the *intersection*, not the union.
- **Reading "hardware-backed" as "unbreakable".** Hardware TEEs have had their share of CVEs (trustzone, SGX, secure boot). Hardware-rooted is a higher bar than software-only, not a perfect one.
## Tooling pointers
- `strace` / `ltrace` / frida — to watch the client library talk to the CDM core.
- A protocol-aware proxy (e.g. mitmproxy with a custom script) — to inspect the license dance.
- The disassembly tools in [`tools/`](../tools/) — for the CDM core binary itself.
- [`drm/02-publisher-internal-telemetry-relay`](02-publisher-internal-telemetry-relay.md) — the publisher-side telemetry pattern; pairs with the CDM architecture for the leak-detection side.
## References
- [W3C: Encrypted Media Extensions (EME)](https://www.w3.org/TR/encrypted-media/)
- [Marlin: DRM Reference Architecture](https://www.marlin-community.com/)
- Various academic papers on TEE-based media pipelines (search "TEE media pipeline attestation").
@@ -0,0 +1,126 @@
---
title: "Publisher-Internal Diagnostic-Relay Hostnames"
category: "drm"
platforms: ["linux", "windows", "macos", "android", "ios"]
difficulty: "advanced"
tags: ["telemetry", "fingerprinting", "leak", "diagnostic", "publisher-network", "detection"]
summary: "A new category of telemetry leak — binaries that ship with the hostname of a publisher-internal diagnostic / observability server (jenkins.internal, grafana.corp, sentry.io.internal) and conditionally send the un-hashed machine fingerprint only when the host is on RFC1918 corporate network space. The hostname is the leak; the public variant (jenkins.io) does not fire."
updated: "2026-06-06"
related: ["drm/01-cdm-architecture", "anti-analysis/05-launcher-activation-fingerprinting", "anti-analysis/02-vm-bytecode-interpreter"]
---
## Summary
A publisher-internal diagnostic-relay hostname is a DNS name that resolves only inside the publisher's corporate network (`.internal`, `.corp`, `.lan`, `.local`, `.intra`, `.private`, `.home.arpa` TLDs) and is *paired* with a diagnostic product stem (`jenkins`, `jira`, `grafana`, `prometheus`, `kibana`, `splunk`, `sentry`, `bitbucket`, `gerrit`, `artifactory`, `nexus`, `sonarqube`, `vault`, `consul`, `etcd`, `datadog`, `newrelic`, `pagerduty`). The hostname alone is a *leak*: it tells the reader the binary was built against an internal corporate resolver and shipped without scrubbing.
The new danger surface is the *conditional upload*: many of these binaries check whether the host is on RFC1918 corporate network space (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and, only if so, send the **un-hashed** machine fingerprint to the internal hostname. Off the corporate network, the binary looks inert (the hostname doesn't resolve, the upload is skipped). The fingerprint is the second leak.
## Why this matters
This category is *not* in the standard telemetry-leak catalogs (Sentry DSN, Logstash, Confluence wiki page, Google Drive document, AWS access key, Slack token). Those are public infrastructure. The diagnostic-relay hostname is a *publisher-private* endpoint — a build artifact that survived the dev-to-prod pipeline and shipped in the release binary. The pattern is:
> A string in the binary references a diagnostic product (grafana, sentry, jenkins) at a TLD that doesn't exist on the public internet (`.internal`, `.corp`).
The reader's first hint is *why* the string is in the binary at all — the binary is either (a) a tool the publisher built for their own developers and never stripped, or (b) shipped a build of an upstream library that embeds a developer-environment config. Both are leaks.
## Mechanics
### The hostname pattern
A typical example is `jenkins.internal` (the build server at a publisher that hosts Jenkins internally on a `.internal` TLD) or `grafana.corp` (the metrics dashboard). The full regex is a diagnostic-product stem + a private TLD:
```
(?P<diagnostic_product>jenkins|jira|grafana|prometheus|kibana|splunk|sentry|...)
(?P<tld>\.internal|\.corp|\.lan|\.local|\.intra|\.private|\.home\.arpa)
```
The diagnostic-product stem is a closed list (not free-form) because the leak is specifically a *product* name (a server product the publisher deploys), not a generic host. The TLD list is the standard private-network TLD set; the public internet doesn't use these.
A real example is a publisher's .NET WPF companion binary with a `PASystemInfoScanner.SenderInfomation` class that does a DNS lookup of a `*.io.internal` hostname, checks the resolved IP against RFC1918, and conditionally sends the un-hashed machine fingerprint. The hostname in the binary is the leak; the conditional upload is the second leak.
### Why the public variant does *not* fire
A *public* `jenkins.io` is the website of the Jenkins project. A `grafana.com` is a SaaS vendor. These are *not* leaks — they're public web properties that the binary legitimately links to for documentation, status pages, or update checks. The pattern requires a *private TLD* to be diagnostic.
```python
# A live example with the test string
import re
P = re.compile(
r"\b(?:[a-z0-9\-]+\.)*"
r"(?:jenkins|jira|grafana|prometheus|kibana|splunk|sentry|"
r"bitbucket|gerrit|artifactory|nexus|sonarqube|vault|consul|"
r"etcd|datadog|newrelic|pagerduty)"
r"(?:\.[a-z0-9\-]+)*"
r"\.(?:internal|corp|lan|local|intra|private|home\.arpa)"
r"\b",
re.IGNORECASE,
)
# These fire (the leak):
P.search("jenkins.internal") # match
P.search("grafana.corp") # match
P.search("sentry.io.internal") # match
P.search("splunk.lan") # match
P.search("artifactory.intra.example") # match
# These do NOT fire (public, not a leak):
P.search("jenkins.io") # no match
P.search("github.com") # no match
P.search("grafana.com") # no match
```
The match is **case-insensitive** — `Asian-Internal.corp` would also match (because `asian` is in the diagnostic-product stem list? actually no — the stems are specific products, not generic words). The `\.home\.arpa` TLD is the special-case for mDNS (RFC 8375) — modern Unix systems register their hostname under `.home.arpa` and a publisher's internal hostnames may use it too.
### The conditional upload
The leak usually doesn't end at the hostname. The same binary often has a code path that:
1. Resolves the hostname via the system resolver.
2. Checks whether the resolved IP is in RFC1918 space (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) — the "are we on the corporate LAN?" probe.
3. If on the corporate LAN, sends the *un-hashed* machine fingerprint to the internal hostname.
4. If not on the corporate LAN, the upload is skipped (the hostname doesn't resolve, the request would fail).
The off-corporate-network case looks inert (no network traffic, no upload), which is why this category is hard to catch with a "run the binary and watch the network" workflow. The static-string signal is the only reliable one.
```python
# A typical conditional-upload pattern (pseudocode)
hostname = "fingerprint-relay.internal"
ip = dns_resolve(hostname)
if is_rfc1918(ip):
fingerprint = read_machine_fingerprint() # no hashing
http_post(f"https://{hostname}/v1/fingerprint", json=fingerprint)
```
The un-hashed fingerprint is the *second* leak — even if the network is corporate-only, the publisher's diagnostic server is now logging the un-hashed machine identifier. If the diagnostic server is later compromised, every machine that ever ran the binary is at risk.
## Approach
The triage workflow:
1. **Grep for the diagnostic-product stems** in the binary's strings — `jenkins`, `grafana`, `sentry`, `prometheus`, etc. The result is a *candidate list*; not all hits are leaks (e.g. a binary that links a public Grafana plugin is fine).
2. **Filter to the private-TLD anchors** — only hostnames with `.internal`, `.corp`, `.lan`, `.local`, `.intra`, `.private`, `.home.arpa` are diagnostic. The regex above does this in one step.
3. **Cross-reference the public variant** — if the same hostname (sans the private TLD) is a public website, the leak is weaker (the binary may legitimately link to a public status page). If the hostname is *only* in the private-TLD form, the leak is conclusive.
4. **Check the conditional-upload code path** — find the DNS-resolution + RFC1918 + upload code (the binary's strings dump usually surfaces the relevant substrings: `10.0.0.0`, `192.168.`, `home.arpa`, etc.). Document the upload for the publisher's security team.
5. **Do not run the binary on a corporate network** — the upload path may fire and send the un-hashed fingerprint to a server you can't audit. The static analysis is enough; the dynamic analysis is the threat surface.
## Common pitfalls
- **Assuming the binary is "off-corporate" so the leak is harmless.** The binary may be installed on an analyst's workstation that is *VPN'd into* the publisher's corporate network. The conditional upload fires. The static string is the only signal you have.
- **Mistaking a public diagnostic product (grafana.com, sentry.io) for a leak.** The pattern is the *private TLD* — `grafana.com` is the vendor's SaaS; `grafana.corp` is the publisher's internal instance. The regex enforces this.
- **Treating the un-hashed fingerprint upload as "just a feature".** The fingerprint goes to a server the publisher can't audit (and may be a misconfigured bucket). The un-hashed form is the danger: even an internal upload creates a high-value target.
- **Missing the diagnostic-product stems in non-English binaries.** A CJK-market binary may have the same diagnostic infrastructure with `internal` → `内部` or `.corp` → `.集团`. The English regex won't fire. Build a localized stem list per market.
## Tooling pointers
- [`drm/01-cdm-architecture`](../drm/01-cdm-architecture.md) — the broader "publisher network architecture" topic. The diagnostic-relay hostname is a sibling concern: both are about "what the binary talks to that you didn't know about".
- [`anti-analysis/05-launcher-activation-fingerprinting`](../anti-analysis/05-launcher-activation-fingerprinting.md) — the activation library that often co-locates the diagnostic-relay code in the same launch flow.
- `re-leak-scan.scan` (via [RE-AI](https://github.com/Heretek-AI/RE-AI)) — the leak catalog includes a `publisher-internal-diagnostic-hostname` detector. The 8 patterns in the catalog (Sentry DSN, Logstash URL, Confluence wiki, Google Drive, AWS access key, Slack token, generic hex secret) cover the public-infrastructure leaks; the diagnostic-relay pattern covers the publisher-private one.
- `re-lief.categorize_strings` (via [RE-AI](https://github.com/Heretek-AI/RE-AI)) — the `telemetry_leak` category surfaces candidates; the `license-activation` category catches the activation-library context. Cycle 2 of the categorizer added `exclude_keywords` to suppress Unicode UCD constant false positives (e.g. `East_Asian_Width` was matching the `width` keyword).
- `httpx` (or `curl -I`) for verifying the *public* variants of the diagnostic-product names (so you can exclude them as leaks). The reachability of the *private* variants cannot be verified from outside the publisher's network.
## References
- [RFC 1918 — Address Allocation for Private Internets](https://datatracker.ietf.org/doc/html/rfc1918) — the `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` private network ranges.
- [RFC 8375 — Special-Use Domain `home.arpa`](https://datatracker.ietf.org/doc/html/rfc8375) — the `.home.arpa` TLD for residential networking.
- [RE-AI: data/drm-indicators.yaml](https://github.com/Heretek-AI/RE-AI/blob/main/data/drm-indicators.yaml) — the `hwid_apis` and `telemetry_leak` categories that informed this entry.
- [RE-AI: servers/re-leak-scan/src/re_leak_scan/patterns.py](https://github.com/Heretek-AI/RE-AI/blob/main/servers/re-leak-scan/src/re_leak_scan/patterns.py) — the `publisher-internal-diagnostic-hostname` pattern definition.
@@ -0,0 +1,44 @@
# BlackSpace Engine
**Engine type:** Proprietary game engine (handler-table dispatch architecture)
**Characteristics:** Handler-table dispatch architecture (Pattern A-VMT in the anti-tamper taxonomy)
## Headline
The BlackSpace Engine is a proprietary game engine. The engine's encrypted-VM protection is NOT a traditional bytecode interpreter — it uses a **handler-table-dispatch** architecture. RE-AI's `ANTI-TAMPER-TAXONOMY.md` calls this **Pattern A-VMT** (`encrypted-vm-handler-table-dispatch`).
## Architecture
The `.xcode` section has TWO sub-regimes:
1. **Dispatch table** (192KB low-entropy 3.4-3.7): 36+ 16-byte big-endian entries `[u32 handler_id][u32 reserved=0][u64 target_address]`
2. **Encrypted metadata** (3.2MB high-entropy 6.3-7.2): the encrypted dispatch metadata / key tables
Handler IDs fall in range 0x01-0x35 (with gaps for reserved/unused slots). The targets point into `.link` (zero-initialized in file = runtime-decrypted BSS). The handler bodies live in `.arch` (statically linked OpenSSL + handler library; the first 64KB contains 10+ x86_64 function prologues).
## Section map (representative analysis)
| Section | Size | Entropy | Role |
|---|---|---|---|
| `.arch` | 73MB | 6.752 | Handler library + static OpenSSL |
| `.link` | 21MB | 5.54 | Relocation + BSS |
| `.rodata` | 300MB | 6.726 | Resources |
| `.xcode` | 3.4MB | (dual) | Dispatch + encrypted metadata |
| `.xtext` | 36KB | 5.856 | Handlers (legacy) |
| `.sbss` | 2.7MB | 7.304 | BSS |
## Cross-publisher view
| Game | Status | Doc |
|---|---|---|
| Representative binary | Pattern A-VMT canonical case | See the anti-tamper taxonomy doc |
| Black Desert Online | Predecessor — likely same architecture | (deferred) |
| DokeV | Sequel — same engine | (deferred) |
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A-VMT (the taxonomy entry this engine motivates)
- RE-AI `servers/re-lief/src/re_lief/protection_catalog.py` `_classify_avmt_signature` (the v2.9.1+ classifier)
- See the RE-AI output directory for per-binary triage data. (the v2.9.0 source data)
- RE-UNLEASHED `protection/encrypted-vm-bytecode-interpreter/pattern-a-vmt.md`
- RE-UNLEASHED `publishers/pearl-abyss/crimson-desert/`
@@ -0,0 +1,14 @@
# Glacier 2 — placeholder
**Engine type:** Proprietary game engine (encrypted-VM-style sections)
**Characteristics:** Encrypted-VM-style section layout (proprietary protection, NOT third-party ATD wrapping)
## Status
**Placeholder.** The v2.9.0 stress test's per-binary triage of 007FL captured the section set + anti-analysis surface, but a deep engine-level reverse wasn't done in the v2.9.1 cycle. The Glacier 2 engine is a proprietary game engine; the encrypted-VM-style sections in the target binary (`.text + .sxdata + .edata + .xtls + .xpdata + .trace`) are characteristic of its own protection (NOT a third-party-ATD wrapping like Denuvo ATD — see the zero third-party ATD string hits in the per-binary artifact).
## Cross-references
- See the RE-AI output directory for per-binary triage data.
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A-DW (close sibling — same section-set family)
- RE-UNLEASHED `publishers/io-interactive/007-first-light/`
@@ -0,0 +1,25 @@
# Unity IL2CPP — cross-publisher view
**Publisher:** Various (Unity Technologies engine, used by many publishers)
**Characteristics:** IL2CPP-compiled games with encrypted-VM bytecode interpreters in GameAssembly.dll
## Headline
IL2CPP (Intermediate Language To C++) is Unity's ahead-of-time compilation pipeline. The original C# source is converted to C++ and compiled to native code; the metadata (`global-metadata.dat`) retains the type/method/field names for runtime reflection.
The v2.9.1+ re-il2cpp walker handles versions 24-31 (Unity 2019.4 LTS through Unity 6). For metadata-stripped or non-standard-location targets, see the Step 1a branch in `skills/re-il2cpp-static-triage/SKILL.md`.
## Per-game table
| Game | Metadata version | Source artifact | Notes |
|---|---|---|---|
| Representative IL2CPP target (IL2CPP metadata v27) | Fully supported; OriginSDK.dll in metadata |
| Representative IL2CPP target (IL2CPP metadata v31) | Unity 6; Gap 25 canonical |
| Representative IL2CPP target | (stripped metadata variant) | Per-binary triage | Metadata stripped; Gap pattern |
## Cross-references
- RE-AI `servers/re-il2cpp/src/re_il2cpp/_common.py` (`MAX_VERSION=31`, v2.9.1+)
- RE-AI `servers/re-il2cpp/src/re_il2cpp/version_table.py`
- RE-AI `skills/re-il2cpp-static-triage/SKILL.md`
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A (encrypted-VM bytecode interpreter, Unity IL2CPP variant)
@@ -0,0 +1,23 @@
# Unreal Engine 5 — cross-publisher view
**Publisher:** Epic Games (engine), various (games)
**Characteristics:** Encrypted-VM bytecode interpreters (protected Blueprint bytecode) + potential third-party ATD wrapping
## Headline
Unreal Engine 5 games compile to native C++ and do not have the IL2CPP metadata format. The C#/.NET reflection surface that IL2CPP depends on does not exist. The re-il2cpp walker is not applicable to UE5 binaries.
UE5 games can still carry encrypted-VM bytecode interpreters (the protected Blueprint bytecode format) and Denuvo ATD wrapping. A representative UE5 target showed the full Pattern A-DW section set with third-party ATD string hits.
## Per-game table
| Game | Pattern | Doc |
|---|---|---|
| Representative UE5 target | Pattern A-DW (third-party ATD-wrapped) | See the anti-tamper taxonomy doc |
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A-DW
- RE-AI See the RE-AI output directory. (9-stage static ATD analysis)
- RE-AI `skills/re-drm-fingerprint/SKILL.md`
- RE-AI `servers/re-lief/src/re_lief/parsers.py` `get_debug_directory` (Gap 22 fix — POGO check for Denuvo ATD detection)
@@ -0,0 +1,81 @@
---
title: "IPA Structure"
category: "ios"
platforms: ["ios", "macos"]
difficulty: "beginner"
tags: ["fundamentals", "static-analysis", "packaging"]
summary: "How an iOS App Package is laid out on disk: the ZIP container, Info.plist, embedded mobile provision, the Mach-O binary, and the bundles and frameworks it ships with."
updated: "2026-06-04"
related: ["ios/02-objc-runtime", "ios/03-swift-demangling", "ios/05-frida-ios"]
---
## Summary
An IPA is a ZIP archive with a well-defined internal layout. Knowing the shape of an IPA — and the metadata the App Store / device use to decide whether to launch it — is the prerequisite for almost every other iOS RE task.
## Why this matters
Every tool you'll use — `unzip`, `otool`, `class-dump`, Hopper, Ghidra, frida-ios-dump, objection — reads the same on-disk structure. The moment you can navigate an IPA by hand, you can validate what tools are telling you.
## Mechanics
The high-level layout of an IPA:
```
MyApp.ipa (a ZIP file; conventionally named .ipa)
└── Payload/
└── MyApp.app/ (a "bundle" directory; name matches the executable)
├── MyApp # the Mach-O executable
├── Info.plist # text plist; metadata, supported devices
├── PkgInfo # 8-byte APPL???? stub (legacy)
├── embedded.mobileprovision # dev certs + entitlements + UDIDs
├── _CodeSignature/
│ └── CodeResources # v1 signing manifest
├── Frameworks/ # embedded private frameworks
├── PlugIns/ # app extensions (.appex)
├── en.lproj/ … # localised resources
├── Assets.car # compiled asset catalog
└── *.dylib, *.nib, images, … # arbitrary resource files
```
A few details that surprise people the first time:
- `Info.plist` is a text plist (XML or binary; `plutil -convert xml1` makes it readable). The most important keys for RE: `CFBundleExecutable` (the binary to load), `CFBundleIdentifier`, `MinimumOSVersion`, and `LSRequiresIPhoneOS`.
- `embedded.mobileprovision` is a CMS-signed plist listing the team, entitlements, and (for development) allowed device UDIDs. `security cms -D -i embedded.mobileprovision` decodes it.
- The Mach-O binary is at the bundle root, **not** in a `Contents/MacOS/` subdirectory (which is the macOS .app convention — iOS uses a flat bundle).
- The binary's load commands are critical: `LC_LOAD_DYLIB` entries tell you which system frameworks and embedded private frameworks the app links against, and `LC_CODE_SIGNATURE` points to the signature blob appended after the load commands.
- iOS apps ship **embedded private frameworks** under `Frameworks/` (e.g. `MyApp.framework/MyApp` plus `MyApp.framework/Info.plist`). These are themselves bundles-with-a-binary, and the binary is just another Mach-O you can `otool -L` on.
- App extensions are sibling `.appex` bundles under `PlugIns/`. They run in a separate process; calls into the host app go through `NSExtensionContext`.
## Approach
For a first pass on an unknown IPA:
1. `unzip -l target.ipa` to see the layout. Look for `Frameworks/` (private frameworks are a rich target), `PlugIns/`, and `Assets.car` (compiled assets — `Asset Catalog Tinkerer` can unpack them).
2. `plutil -convert xml1 -o - Payload/MyApp.app/Info.plist` to read the bundle metadata.
3. `security cms -D -i Payload/MyApp.app/embedded.mobileprovision` to see the entitlements and signing chain.
4. `otool -L Payload/MyApp.app/MyApp` to see what the binary links against. System frameworks start with `/System/Library/...` or `/usr/lib/...`; embedded frameworks start with `@rpath/`.
5. `otool -l Payload/MyApp.app/MyApp | grep -A 5 LC_CODE_SIGNATURE` to find the signature blob.
6. For decompiled ObjC: `class-dump` the binary, or open it in Hopper / Ghidra (the latter has a Mach-O loader and an ObjC class reconstruction pass).
7. For Swift: combine `class-dump` (limited Swift coverage) with Swift demangling (`swift demangle` from the Swift toolchain, or Ghidra's Swift plugin).
## Common pitfalls
- **Treating class-dump output as authoritative.** The class-dump is reconstructed from the ObjC metadata section; it's not the original header. Some methods will be missing (especially those that the compiler eliminated).
- **Hoping for the Swift type info to be useful.** Swift strips a lot of metadata into the binary's Mach-O `__swift5_*` sections, but private types and inlined generics can be heavily mangled. Plan to read the disassembly for high-stakes claims.
- **Missing embedded frameworks.** A malicious-looking or interesting function in the *main* binary may actually be calling into an embedded framework that you haven't pulled apart yet.
- **Assuming the .ipa extension is required.** It's a ZIP; the App Store, Xcode, and `ios-deploy` all use the same on-disk layout. Renaming to `.zip` lets you `unzip` it on Linux.
- **Running on the wrong OS.** Mach-O is also the macOS executable format; many of the same tools (otool, class-dump, Hopper) work on macOS binaries. The differences are in load commands and dyld semantics, not the basic layout.
## Tooling pointers
- `otool` and `class-dump` — first-pass binary inspection (both ship with Xcode's command-line tools, and there are Linux ports).
- Hopper, Ghidra, Binary Ninja — full disassembly + ObjC class reconstruction.
- `plutil`, `security` — reading plist and CMS blobs from the command line.
- frida-ios-dump / objection — for runtime introspection once the app is on a device.
## References
- [Apple: Bundle Structure](https://developer.apple.com/documentation/bundleresources/bundle-structures)
- [Apple: Mach-O Programming Topics](https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachORuntime/)
- [class-dump](https://github.com/nomad-software/class-dump)
@@ -0,0 +1,140 @@
---
title: "ELF Format"
category: "native"
platforms: ["linux", "macos", "android"]
difficulty: "intermediate"
tags: ["file-format", "static-analysis", "fundamentals"]
summary: "The Executable and Linkable Format: program headers, section headers, the dynamic loader's view, the dynamic symbol table, relocations, and the linker structures you'll be reading at the RE bench."
updated: "2026-06-04"
related: ["native/02-pe-section-layout", "native/03-macho-format", "native/06-hooking"]
---
## Summary
ELF (Executable and Linkable Format) is the binary format for Linux, *BSD, Android, Solaris, and most non-Windows desktop and embedded systems. It's also the on-disk shape of shared libraries, object files, and core dumps. The RE-relevant view is the dynamic loader's view: program headers, the dynamic section, the symbol table, and the relocation tables.
## Why this matters
If you reverse-engineer any Linux/Android/FreeBSD/embedded binary, you read ELF. If you debug crashes, you read core dumps — ELF. If you patch a binary, you edit ELF sections. If you write a custom loader, you parse ELF by hand. The format is the lingua franca of everything outside Windows.
## Mechanics
### Two views, one file
ELF has *two* parallel header tables:
- The **program header table** describes *segments* — what the loader reads. Each `PT_LOAD` entry is a chunk of the file that gets mapped into memory with specific permissions (`PF_R`, `PF_W`, `PF_X`).
- The **section header table** describes *sections* — what the linker reads. `.text`, `.data`, `.bss`, `.rodata`, `.symtab`, `.strtab`, `.dynsym`, `.dynstr`, `.rela.*`, `.init_array`, `.fini_array`, `.got`, `.plt`, etc.
The two are related but not identical. A single `PT_LOAD` segment typically covers several sections. Tools like `readelf -l` show segments, `readelf -S` shows sections, and `readelf -e` shows both. The mapping between them is one of the first things you internalise.
### The structure of an ELF file
```
+--------------------------------+
| ELF header (Ehdr) | entry point, machine, header table offsets
+--------------------------------+
| Program header table | (Phdr array — for the loader)
+--------------------------------+
| .text | code
| .rodata | read-only data, string literals
| .plt / .plt.got | procedure linkage table (PLT)
| .got / .got.plt | global offset table
| .data | initialised mutable data
| .bss | zero-initialised mutable data (no on-disk bytes)
| .dynsym / .dynstr | dynamic symbol tables
| .rela.dyn / .rela.plt | relocations (with addend)
| .init_array / .fini_array | constructor/destructor function pointers
| .note.* / .note.gnu.* | notes (build-id, ABI tags, etc.)
| .interp | dynamic linker path (for ET_DYN/ET_EXEC)
+--------------------------------+
| Section header table | (Shdr array — for the linker)
+--------------------------------+
| .shstrtab | section name string table
+--------------------------------+
```
### Important fields in the ELF header
- `e_entry` — virtual address of the entry point. The loader jumps here.
- `e_phoff`, `e_phentsize`, `e_phnum` — program header table.
- `e_shoff`, `e_shentsize`, `e_shnum`, `e_shstrndx` — section header table.
- `e_type` — `ET_EXEC` (static executable), `ET_DYN` (shared object / PIE executable), `ET_REL` (relocatable object), `ET_CORE` (core dump).
- `e_machine` — `EM_X86_64`, `EM_AARCH64`, `EM_ARM`, `EM_RISCV`, `EM_386`, etc.
### Program headers (`PT_*`)
- `PT_LOAD` — a chunk of the file to be mapped. The permissions are encoded in `p_flags` (`PF_R`, `PF_W`, `PF_X`).
- `PT_INTERP` — the dynamic linker to invoke (`/lib64/ld-linux-x86-64.so.2` on glibc).
- `PT_DYNAMIC` — points to the `.dynamic` section.
- `PT_GNU_STACK` — `PF_X` flag indicates the stack should be executable. If present and **without** `PF_X`, the kernel will refuse to make the stack executable (NX).
- `PT_GNU_RELRO` — read-only relocations; tells the loader which parts to make read-only *after* applying relocations. Critical for RELRO security.
- `PT_GNU_PROPERTY` — notes for the loader (e.g. CET, IBT flags).
- `PT_TLS` — thread-local storage template.
### The dynamic section
The `.dynamic` section is an array of `{d_tag, d_val}` entries. The loader walks it to set up the process:
- `DT_NEEDED` — required shared libraries (e.g. `libc.so.6`).
- `DT_SONAME` — the shared object's name.
- `DT_SYMTAB` / `DT_STRTAB` — dynamic symbol table and string table addresses.
- `DT_PLTGOT` — address of the GOT (used for PLT resolution).
- `DT_JMPREL` / `DT_PLTREL` / `DT_PLTRELSZ` — PLT relocations.
- `DT_RELA` / `DT_RELASZ` / `DT_RELAENT` — `.rela.dyn` relocations.
- `DT_FLAGS` / `DT_FLAGS_1` — `BIND_NOW`, `NOW`, `PIE`, etc.
- `DT_INIT` / `DT_FINI` / `DT_INIT_ARRAY` / `DT_FINI_ARRAY` — constructors and destructors.
### Relocations
A relocation is a *patch instruction* the loader applies to the binary. For `ET_REL` (object files), relocations are how the linker resolves symbol references; for `ET_EXEC`/`ET_DYN` (executables and shared libraries), they're the *remaining* patches that couldn't be done at link time (because the actual load address isn't known until run time).
The important relocation types you'll see:
- `R_X86_64_GOTPCREL` — patch a 32-bit PC-relative offset to a GOT entry.
- `R_X86_64_PLT32` — a call to a function in another shared object, resolved via the PLT.
- `R_X86_64_RELATIVE` — `*reloc_addr = base_addr + addend`. Used heavily in PIE binaries to fix up absolute addresses.
- `R_AARCH64_*` — the equivalent AArch64 relocations.
- `R_386_JMP_SLOT` / `R_X86_64_JUMP_SLOT` — entries in `.got.plt`, one per imported function. Hooking an imported function is just *changing the value in the GOT slot* to point at your wrapper.
### Symbol tables
`.symtab` (and `.strtab` for the names) is the *full* symbol table — every function, every global, every static — used by the linker and by debuggers. `.dynsym` (and `.dynstr`) is the *dynamic* subset: only the symbols that need to be visible to other shared objects. Stripping a binary with `strip` removes `.symtab` but typically leaves `.dynsym` intact (you still need to find `main`).
Symbol types: `STT_FUNC`, `STT_OBJECT`, `STT_SECTION`, `STT_FILE`, `STT_NOTYPE`. Bindings: `STB_LOCAL` (file-local), `STB_GLOBAL` (exported), `STB_WEAK` (overridable).
## Approach
For a first pass on an unknown ELF:
1. `file target` — sanity check (machine, class, type).
2. `readelf -h target` — ELF header, entry point, machine.
3. `readelf -l target` — program headers. Note `PT_INTERP`, the `PT_LOAD` map, `PT_GNU_STACK` (NX?), `PT_GNU_RELRO`.
4. `readelf -d target` — dynamic section. Note `DT_NEEDED` (linked libraries), `DT_FLAGS_1` (PIE/NOW).
5. `readelf -s target` — symbol table. If `.symtab` is present, locate the entry point symbol. If only `.dynsym`, find exported functions.
6. `readelf -r target` — relocations. The presence of `R_*_JUMP_SLOT` entries means the binary has a working PLT/GOT.
7. `objdump -d target` — disassembly. Or open in your disassembler of choice.
8. For PIE: `readelf -d target | grep FLAGS_1.*PIE` to confirm, and remember that `e_entry` is a *relative* address (added to the runtime base).
## Common pitfalls
- **Confusing file offsets and virtual addresses.** `objdump` and `gdb` use VAs; `hexdump` and `xxd` use file offsets. For a `PT_LOAD` mapping that starts at file offset 0 and VA 0x400000, the two are equal — but for most shared libraries, they're not.
- **Stripped symbols.** Many release binaries have `.symtab` removed. You can still recover symbols via `__ksymtab_*` (kernel modules), `.dynsym`, or by walking the `.symtab` in a matching debug-info package. `addr2line` and `eu-addr2line` use the debug-info package.
- **PIE binaries with absolute `0` addresses in static views.** `readelf -h` will show a tiny `e_entry`; the real address is base + entry. The loader picks the base; tools like `gdb` and `objdump` display the loaded address when you attach.
- **Forgetting that relocations happen at load time.** When you dump a running process, the relocations have been applied — the file offsets and the in-memory layout are *not* identical. To convert, you need the loader's base address and the `PT_LOAD` mapping.
- **Treating `.plt` as static code.** The PLT is dynamically rewired (lazy binding) the first time a function is called. After the first call, the `jmp *GOT[n]` slot points at the resolved function; before, it points back at the PLT's resolver stub.
## Tooling pointers
- `readelf` — comprehensive ELF inspector (in `binutils`).
- `objdump` — disassembler and section dumper.
- `nm` — symbol table lister.
- `ldd` — show `DT_NEEDED` resolution at runtime.
- `eu-readelf` / `eu-objdump` (elfutils) — faster, DWARF-aware variants.
- [`ghidra`](../tools/01-ghidra.md) / [`binary-ninja`](../tools/03-binary-ninja.md) / IDA — full decompilation.
## References
- [System V ABI — ELF Specification](https://refspecs.linuxfoundation.org/elf/gabi4+/contents.html) — the canonical reference.
- [ELF man page (`man 5 elf`)](https://man7.org/linux/man-pages/man5/elf.5.html)
- [Linux Foundation: ELF Handling for Thread-Local Storage](https://www.akkadia.org/drepper/tls.pdf) — Ulrich Drepper's TLS note, which is the de-facto reference for `PT_TLS`.
@@ -0,0 +1,165 @@
---
title: "PE Section Layout and W^X"
category: "native"
platforms: ["windows"]
difficulty: "intermediate"
tags: ["file-format", "pe", "static-analysis", "fundamentals", "protection"]
summary: "The PE section table — canonical section names (.text/.rdata/.data/.rsrc), the characteristics byte (R/W/X permissions, CNT_CODE, CNT_INITIALIZED_DATA), the virtual_size vs raw_size distinction, the entropy signal, and the W^X pattern that flags encrypted-VM bytecode containers and packers."
updated: "2026-06-06"
related: ["native/01-elf-format", "anti-analysis/02-vm-bytecode-interpreter", "anti-analysis/07-vm-bytecode-proprietary", "anti-analysis/05-launcher-activation-fingerprinting", "tools/01-ghidra"]
---
## Summary
The PE (Portable Executable) section table is a list of named, permissioned, sized memory regions the Windows loader maps into the process address space. Most binaries use canonical names and standard permissions; a small set of deviations from the canonical layout is the bulk of the static-detection signal for encrypted-VM bytecode interpreters, packers, and other anti-analysis wrappers. The two most informative deviations are **W^X** (a code-bearing section with read+write+execute simultaneously) and **`virtual_size >> raw_size`** (a packed-shape stub region the loader zero-fills at load time).
## Why this matters
Every static-analysis technique that works on a PE file (decompilation, function-boundary recovery, entropy visualization, W^X detection) is built on top of the section table. Misreading the section characteristics is a common analyst trap: an encrypted-VM bytecode interpreter's `.idata` can be 470 MB with R + W + X simultaneously, and a "just a large .idata" reading misses the category entirely.
The 5 minutes spent learning the section characteristics byte pays off across the rest of static analysis: every static-triage workflow on PE files starts with the section table.
## Mechanics
### Canonical section names and their roles
| Section | Typical permissions | What it holds |
|---|---|---|
| `.text` | RX | Compiled code |
| `.rdata` | R | Read-only data: string literals, vtables, const tables |
| `.data` | RW | Initialized read-write data |
| `.rsrc` | R | Resources (icons, manifests, version info) |
| `.pdata` | R | Exception tables (x64 unwind info) |
| `.reloc` | R | Base-relocation table |
| `.tls` / `.tls$` | RW | Thread-local storage (TLS callbacks live in `.tls$`) |
| `.debug` / `.debug$P` / `.debug$S` | R | Debug info (PDB data; large if PDB is embedded) |
| `.idata` | R | Import table (the "writable + executable" `.idata` is the encrypted-VM bytecode family; see below) |
| `.edata` | R | Export table |
The names are conventional but not mandatory. A compiler can use any name; tools key off the *characteristics byte*, not the name.
### Proprietary-engine encrypted-VM section set
In addition to the canonical names above, a *second* observable composition exists in some Windows binaries (typically AAA cross-platform titles using a proprietary engine) — the **proprietary-engine variant** of the encrypted-VM bytecode interpreter. The custom section set is:
| Section | Typical permissions | What it holds |
|---|---|---|
| `.arch` | R | Architecture / dispatch table (the encrypted-VM bytecode interpreter's handler table) |
| `.xcode` | RX | Encrypted bytecode + dispatch loop (the VM's code-bearing region) |
| `.xtext` | R | Encrypted strings / lookup tables (the "VM dictionary") |
| `.sbss` | RW (zero-fill expected) | **Encrypted VM context** — entropy 7.0+ (BSS that isn't BSS) |
| `.link` | R | Link table (the cross-handler jump graph) |
This is *not* a packer like UPX (which compresses the whole binary) — the proprietary engine replaces the game's code with its own VM, the same as the [Unity-IL2CPP encrypted-VM variant](../anti-analysis/02-vm-bytecode-interpreter.md), but with a different observable composition. The diagnostic signal is **the `.sbss` entropy**:
```python
# Quick triage — anomalous .bss entropy
import lief
b = lief.parse("target.exe")
for s in b.sections:
if s.name in {".bss", ".sbss"} and s.entropy > 4.0:
# BSS normally has entropy < 0.5 (zero-init memory).
# 7.0+ means the section is the encrypted VM context, not BSS.
print(f" {s.name}: entropy {s.entropy:.2f} ← BSS-shaped but high-entropy")
```
The proprietary variant is documented in detail in [`anti-analysis/07-vm-bytecode-proprietary`](../anti-analysis/07-vm-bytecode-proprietary.md); the Unity-IL2CPP variant is in [`anti-analysis/02-vm-bytecode-interpreter`](../anti-analysis/02-vm-bytecode-interpreter.md). Both entries share the same conceptual category (encrypted-VM bytecode interpreter) but have different observable compositions. Treat the entries as *complementary*.
### The characteristics byte
A 32-bit bitfield per section. The two most-significant groups of bits:
**Permission bits** (any combination of these three):
- `MEM_READ` = 0x40000000
- `MEM_WRITE` = 0x80000000
- `MEM_EXECUTE` = 0x20000000
**Content-class bits** (one or both):
- `CNT_CODE` = 0x00000020
- `CNT_INITIALIZED_DATA` = 0x00000040
Canonical combinations:
- `.text`: `CNT_CODE | MEM_READ | MEM_EXECUTE`
- `.rdata`: `CNT_INITIALIZED_DATA | MEM_READ`
- `.data`: `CNT_INITIALIZED_DATA | MEM_READ | MEM_WRITE`
- `.rsrc`: `CNT_INITIALIZED_DATA | MEM_READ`
The combination `MEM_READ | MEM_WRITE | MEM_EXECUTE` (W^X) on a code-bearing section (`CNT_CODE` also set) is the encrypted-VM bytecode container signal. The canonical W^X encrypted-VM shape is a 100+ MB `.idata` carrying all four bits. A 1-3 MB `.data` with `MEM_READ | MEM_WRITE` is normal and not the signal.
### `virtual_size` vs `raw_size`
Each section has two sizes:
- **`virtual_size`**: the in-memory size, the loader zero-fills the gap if `virtual_size > raw_size`.
- **`raw_size`**: the on-disk size, the number of bytes in the file.
For most sections these are equal or close. When `virtual_size >> raw_size` (e.g. 2.2 MB virtual, 512 raw), the loader zero-fills the gap at load time. This is the **"large section with tiny text"** packed-shape signal. The 512 raw bytes are a stub that the runtime expands.
A related shape: `raw_size == 0` with `virtual_size > 0`. The loader allocates memory but reads no bytes from the file. This is the empty-section pattern used by UPX (`UPX0` is raw-size 0, virtual-size > 0; it's the destination for the unpacked `.text`).
### Per-section entropy
Shannon entropy, 0.0 (all same byte) to 8.0 (random):
- Normal `.text`: 6.0-6.6 (compiled code with mixed instructions, jump tables, immediates)
- Normal `.rdata`: 4.5-5.5 (string literals, vtables)
- Normal `.data`: variable (depends on payload)
- High entropy (7.5+) on a code section: encrypted-TLS signal — the bytecode is dense, near-random-looking bytes
Per-section entropy is the cheapest static signal for "is this section encrypted or compressed?"
### Non-canonical section names by category
| Category | Section names |
|---|---|
| Encrypted-VM bytecode interpreter (Unity IL2CPP target) | `.xtls`, `.didata`, `.ecode`, `.xdata`, `.xpdata`, `.udata`, `.00cfg` |
| Encrypted-VM bytecode interpreter (alternative VM-pack family) | `.vmp0`, `.vmp1`, `.code` |
| Encrypted-VM bytecode interpreter (WinLicense-style family) | `.themida`, `.winlice` |
| Generic packer (UPX) | `.UPX0`, `.UPX1` |
| Manual / custom packing | single-character or empty section names |
The full regex list is in `data/drm-indicators.yaml::section_indicators.rules`.
### "Large section with tiny text" — the canonical packed shape
The classic packed-PE shape:
- `.text` has `virtual_size >> raw_size` (e.g. 2.2 MB virtual, 512 raw).
- A non-canonical section name holds the unpacker's code (`.UPX1`, `.vmp0`, etc.).
- The original `.text` content is gone — replaced with a stub.
This pattern alone is *strongly* suggestive of packing. Confirm with the entropy pattern (low entropy on `.text`, high entropy on the stub section) and the imports pattern (imports are often rebuilt at runtime by the unpacker).
## Approach
1. `re-lief.get_sections` returns the full table with permissions, entropy, and `virtual_size` / `raw_size` for each section.
2. Sort by entropy descending. Top-3 high-entropy sections are where you look first.
3. For any section with W^X, look up the section name against the section-name regex list. Hits confirm the category.
4. For any section with `virtual_size >> raw_size`, confirm the packed shape and skip the on-disk analysis (the loader will fill the gap).
## Common pitfalls
- **Confusing virtual size with raw size in entropy calculations.** Entropy is a per-byte statistic; use the *raw* bytes for entropy (which is what the LIEF / re-lief implementation does).
- **Trusting section names from a packed binary.** After packing, section names can be anything; rely on characteristics and entropy, not names.
- **Missing the W^X bit on a code-bearing section.** The combination of `CNT_CODE` + `MEM_EXECUTE` + `MEM_WRITE` is the headline signal for the encrypted-VM bytecode interpreter category.
- **Confusing `.rdata` entropy (which is normal at 4.5-5.5) with encrypted payload.** `.rdata` has lots of string literals; high-but-not-max entropy is normal. Only 7.5+ is the signal.
- **Ignoring the `.debug` and `.debug$P` sections.** A `.debug` section is 30+ MB in builds with full debug info. The size is a *signal* about how the binary was built (release with PDB symbols vs stripped release). The presence of `.debug$P` references a PDB file in the COFF debug directory; a non-matching tag in the PDB filename is the encrypted-VM vendor-tagged-PDB signal.
- **Treating "empty section" as a benign optimization.** Raw-size 0 with virtual-size > 0 is the unpacked-destination pattern; the binary expects the runtime to fill it.
## Tooling pointers
- [`re-lief.get_sections`](https://github.com/Heretek-AI/RE-AI) — canonical accessor for the section table.
- [`re-lief.categorize_strings`](https://github.com/Heretek-AI/RE-AI) — keyword-bucketed strings dump; pass `skip_sections=[".idata", ".xtls", ".xpdata", ".udata", ".xdata", ".didata", ".ecode", ".00cfg"]` to skip encrypted-VM bytecode regions on a 500+ MB binary.
- [`re-rizin.list_imports_exports`](https://github.com/Heretek-AI/RE-AI) — for the imports + exports surface.
- [`ghidra`](../tools/01-ghidra.md) — decompile after you've identified the category.
- [`anti-analysis/02-vm-bytecode-interpreter`](../anti-analysis/02-vm-bytecode-interpreter.md) and [`anti-analysis/05-launcher-activation-fingerprinting`](../anti-analysis/05-launcher-activation-fingerprinting.md) — for the protection-layer follow-up once the section shape is identified.
## References
- [Microsoft PE/COFF specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) — the canonical reference for the section table and characteristics byte.
- [Wine PE documentation](https://wiki.winehq.org/PE_Format) — a readable summary of the same format, useful for cross-checking the canonical names.
- [PE Format — section table (wiki.osdev.org)](https://wiki.osdev.org/PE) — an alternative walkthrough.
- [RE-AI: ANTI-TAMPER-TAXONOMY.md](https://github.com/Heretek-AI/RE-AI/blob/main/ANTI-TAMPER-TAXONOMY.md) — for the category vocabulary and the 7-observable composition.
@@ -0,0 +1,110 @@
---
title: "UPX Packing"
category: "packers"
platforms: ["linux", "windows", "macos"]
difficulty: "beginner"
tags: ["unpacking", "static-analysis", "tools"]
summary: "Detecting and unpacking the UPX packer — recognising the headers, the one-shot decompression, and how to recover the original entry point and import table without breaking the binary."
updated: "2026-06-04"
related: ["packers/05-custom", "native/02-pe-section-layout", "native/01-elf-format"]
---
## Summary
UPX (Ultimate Packer for eXecutables) is the most common packer you'll encounter. It supports PE, ELF, Mach-O, and a handful of other formats. It's free, open source, and trivially reversible with the same tool that packed it — but modified UPX builds and stripped UPX sections (used by some protectors as a thin obfuscation layer) require manual work.
## Why this matters
A packed binary is unreadable: `.text` is compressed bytes, the original imports are gone, the original entry point is in a small decompressor stub. The first thing almost every static analysis workflow does is unpack. UPX is the entry point for that workflow.
## Mechanics
### Detection
UPX leaves a few telltales:
- The ASCII strings `UPX!`, `UPX 0`, `UPX! Pro`, `UPX 3` somewhere in the binary (typically in a `UPX0` / `UPX1` section pair on PE, or in the `META` / `GNU_STACK` notes on ELF).
- A `UPX!` magic in the compressed payload header.
- The section names `UPX0` and `UPX1` (PE), or a custom `upx` PT_LOAD segment (ELF).
- The original `.text` / `.data` replaced with compressed bytes; `UPX0` is usually zero-sized (it'll be re-filled at runtime), `UPX1` is the compressed data plus the decompressor stub.
In practice, `Detect-It-Easy`, `exeinfo PE`, and `trid` all flag UPX correctly. `strings target | grep UPX` is the lazy-but-effective check.
### The structure of a UPX-packed PE
A packed PE looks like this in sections:
```
UPX0 empty (will be decompressed into here at runtime)
UPX1 compressed .text + .rdata + decompressor stub
.rsrc often preserved
```
The entry point is at the start of the decompressor stub inside `UPX1`. The stub calls `VirtualProtect` to make `UPX0` writable+executable, then walks the compressed payload (its own length and location are stored in a small header at the end of `UPX1`), decompresses, fixes the import table, and jumps to the original entry point (OEP).
### The structure of a packed ELF
For ELF, UPX overwrites the `PT_LOAD` segments: there is usually one large `PT_LOAD` with the decompressor and a separate region that holds the compressed payload, plus a `PT_LOAD` for the unpacked base. The dynamic section (`PT_DYNAMIC`) is preserved so the loader can still process the file, but the `.text` and `.dynsym` are gone.
### The structure of a packed Mach-O
Mach-O is similar; the original `LC_SEGMENT_64` is replaced with a single segment containing the decompressor and the compressed payload. The `LC_LOAD_DYLIB` commands are preserved, so the loader handles the binary, but the executable code and data are in a blob.
## Approach
### 1. Try the easy path first
```bash
upx -d target.bin -o target.unpacked
```
This works in maybe 70% of cases. It fails when:
- The UPX magic was stripped or modified (some protectors do this).
- The binary has a non-standard UPX version that the installed `upx` doesn't recognise.
- The binary has been tampered with after packing (e.g. encrypted headers).
If `upx -d` works, you're done. Verify the unpacked binary still runs and that imports are resolved (e.g. with `Imports` of `die`, or `objdump -R` for ELF relocations).
### 2. For modified UPX: emulate the decompressor
Run the binary under unicorn, dump the decompressed image, fix the entry point, fix the imports, and write the result back. This is the "I want the original binary" path. Tools that automate this:
- [`unipacker`](https://github.com/unipacker/unipacker) — for several packers including UPX.
- [`flare-capa`](https://github.com/mandiant/capa) — for static analysis after unpacking.
- `Cuckoo Sandbox` — for dynamic unpacking (run, dump, recover).
### 3. For deeper analysis: dump at OEP
Run the binary under a debugger, set a breakpoint on the jump to OEP, and dump the process memory. The trick is finding the OEP:
- **Hardware breakpoint on the original entry address** — read the PE/ELF/Mach-O *before* packing, note the OEP, and HBP it post-load. With a packed sample this requires a known good header.
- **Break on `VirtualProtect` + `LoadLibraryA` / `GetProcAddress` import resolution** — after the stub has finished setting up and the import table has been resolved, the OEP is about to be called.
- **Trace `ret` instructions and look for an address that lands in a newly-executed region.** A common signature of the OEP jump is a `ret` whose return address is in the freshly-unpacked code.
- **Use a tool that detects OEP for you.** `Scylla` (Windows) is the canonical one; for ELF, [`syms2elf`](https://github.com/mstczuo/syms2elf) and `r2dec` plugins can recover symbols from a live process.
### 4. For a stripped UPX: reconstruct manually
This is the worst case. You'll see a custom decompressor, no `UPX!` strings, possibly with the section names renamed. Identify the loop that copies and decompresses bytes (look for tight loops with LZ-decompressor signatures: short match-length reads, dictionary lookups, big switch tables). Dump the output of that loop and you've got the unpacked `.text`.
## Common pitfalls
- **Assuming `upx -d` failed means it's not UPX.** Modified UPX is very common. Check the *behaviour* — a small entry point that calls `VirtualProtect` and then jumps far is the giveaway, not the magic.
- **Re-running the unpacked binary in a sandbox without re-fixing imports.** The `upx -d` output runs, but the import table is sometimes only resolved for specific Windows versions. Always verify with `dumpbin /imports` or `objdump -p`.
- **Forgetting that the original timestamps in the PE header are *packed-time*, not *build-time*.** A binary packed in 2020 may have a 1995 timestamp from the original. Use the unpacked binary's timestamp for any forensic timeline.
- **Confusing the OEP with the section's start address.** The OEP is an RVA, not a section boundary. Many tools report the wrong one.
- **Treating the decompressor stub as "just dead code".** It often contains anti-debug, anti-VM, and other checks that fire *before* unpacking. If your sandbox can't get past the stub, you'll never see the real binary.
## Tooling pointers
- `upx -d` — the official tool. `upx --version` to see what formats you have.
- [`ghidra`](../tools/01-ghidra.md) — for the manual reconstruction path; the decompiler can recognise the LZ77-style loops that UPX uses.
- [`binary-ninja`](../tools/03-binary-ninja.md) — same, with better scriptability for the analysis loop.
- `Detect-It-Easy` (DIE) — packer/compiler detection.
- `Scylla` (Windows) — OEP detection, import reconstruction, memory dumping.
- For Mach-O: `dsc_extractor` and `jtool` to verify the unpacked output.
## References
- [UPX source code and docs](https://github.com/upx/upx) — the canonical reference for the headers and format.
- [PE Format (Microsoft docs)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) — for the import table reconstruction.
- [ELF Format (System V ABI)](https://refspecs.linuxfoundation.org/elf/gabi4+/contents.html) — for the PT_LOAD layout.
@@ -0,0 +1,24 @@
# Pattern A — encrypted-VM bytecode interpreter (Unity IL2CPP target)
**Status:** Stable in `ANTI-TAMPER-TAXONOMY.md` since v2.7.0. Vendor-named mirror entry here for cross-publisher views.
## Architecture
The Unity IL2CPP target compiles C# to native C++ and ships a metadata file (`global-metadata.dat`) alongside the launcher. The encrypted-VM bytecode interpreter is a separate protection layer (commercial variant) that wraps the IL2CPP code in a VM. Pattern A's section set per the taxonomy doc:
- `.xtls` family (`.xtls / .didata / .ecode / .xdata / .xpdata / .udata / .00cfg`)
## Empirical cases
- Representative IL2CPP target — full IL2CPP + Steam/Origin SKUs
- See `publishers/ea-originals/lost-in-random/`
- Representative IL2CPP target — Unity 6 (metadata v31)
- See `publishers/sports-interactive/football-manager-26/`
- Representative IL2CPP target — metadata stripped
- See `publishers/sunblink/hello-kitty-island-adventure/`
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A
- RE-AI `skills/re-il2cpp-static-triage/SKILL.md` (the workflow)
- RE-UNLEASHED `engines/unity-il2cpp/`
@@ -0,0 +1,34 @@
# Pattern A-DW — encrypted-VM bytecode interpreter (UE5 + third-party ATD)
**Status:** Stable in `ANTI-TAMPER-TAXONOMY.md` since v2.9.0 (the v2.9.0 WS-30 addition). Vendor-named mirror entry here.
## Architecture
The third-party-ATD-wrapped UE5 variant: a third-party anti-tamper layer (Denuvo ATD being the canonical case) wraps the UE5 binary with an encrypted-VM bytecode interpreter. Section set per the taxonomy doc:
- `.text / .rdata / .arch / .xcode / .xtext / .xtls / .trace`
The `-DW` suffix = A-DenuvoWrapped (project-internal mnemonic), not a vendor name. The differentiator from Pattern A is the section set: Pattern A has `.xtls / .didata / .ecode / .xdata / .xpdata / .udata / .00cfg`; Pattern A-DW has `.text / .rdata / .arch / .xcode / .xtext / .xtls / .trace`. The two sets are disjoint (no overlap except `.xtls` + `.xpdata`); the section table is the deciding signal.
## Detection
- `re-lief.classify_native_protection` returns `encrypted-vm-bytecode-interpreter` when the Pattern A-DW section set fires
- `re-lief.get_debug_directory` returns a POGO entry (kind: "POGO") — the third-party-ATD's trigger-arming metadata
- `re-rizin.search_bytes` for `denuvo` ASCII may confirm the specific ATD vendor
## Empirical cases
- Representative UE5 target — UE5 + third-party ATD confirmed
- 3 `denuvo` string hits (see `publishers/atlus-sega/persona-3-reload/`)
- Source data: `RE-AI/See the RE-AI output directory. (9-stage static ATD analysis)
- Representative UE5 target — Pattern A-DW-adjacent but NO third-party ATD string
- See `publishers/io-interactive/007-first-light/`
- Possibly a Denuvo-free IO build, OR Denuvo with the build-identifier stripped
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A-DW
- RE-AI See the RE-AI output directory.
- RE-AI `skills/re-drm-fingerprint/SKILL.md`
- RE-UNLEASHED `engines/unreal-engine-5/`
- RE-UNLEASHED `publishers/atlus-sega/persona-3-reload/`
@@ -0,0 +1,54 @@
# Pattern A-VMT — encrypted-VM handler-table dispatch (proprietary-engine variant)
**v2.9.1+ canonical entry.** This is the vendor-named companion to RE-AI's `ANTI-TAMPER-TAXONOMY.md` Pattern A-VMT. The empirical case is a handler-table-dispatch engine in a representative target binary.
## Architecture (canonical BlackSpace example)
The `.xcode` section has TWO sub-regimes (the diagnostic differentiator from Pattern A / A-DW):
- **Dispatch table** (192KB low-entropy 3.4-3.7): 36+ 16-byte big-endian entries
- **Encrypted metadata** (3.2MB high-entropy 6.3-7.2)
Handler targets point into `.link` where file bytes are zero (runtime-decrypted BSS). Handler implementations live in `.arch` (statically linked OpenSSL: AES-NI, SHA-512, Vector Permutation AES — 4012 crypto strings).
This is NOT a traditional bytecode interpreter. There is no per-instruction fetch+decode+dispatch loop in `.xcode`. Pattern A-VMT does not require a lifter to read the handlers; it requires a runtime trace to map handler IDs to call sites.
## Observable composition (6 items, any 4 = strong signal)
1. Section table contains `.arch` + `.link` + `.xcode` + `.xtext` + `.sbss` + `.rodata` (Pattern C's section set + a large `.rodata`).
2. The `.xcode` section has TWO sub-regimes: low-entropy head + high-entropy continuation at offset ~0x32000.
3. Dispatch-table handler IDs in range 0x01-0x55 with reserved-slot gaps; 16 < entry count < 256.
4. Handler targets point into `.link` where file bytes are zero (runtime-decrypted BSS).
5. The `.arch` section (50-100 MB) statically links a recognizable crypto library.
6. `mcp__re-lief__get_debug_directory` POGO check returns *no* POGO entry (distinguishing from Pattern A-DW).
## Differentiators
- vs Pattern A (Unity IL2CPP): no `GameAssembly.dll` sibling, no `.xtls` family
- vs Pattern A-DW (UE5 + Denuvo): no POGO entry, no `.trace` section
- vs Pattern C (proprietary-engine, encrypted body in `.rodata` only): Pattern A-VMT has the explicit dispatch table in `.xcode`
## Detection (RE-AI tools)
- `re-lief.classify_native_protection` returns `protection_class: "encrypted-vm-handler-table-dispatch"` when the section set + dual-entropy signature fires
- `re-lief.get_sections` shows the section set
- `re-anti-analysis.scan_anti_analysis_primitives` reports the kernel-active posture
## Empirical case
- Representative binary — handler-table-dispatch A-VMT case
- See `publishers/pearl-abyss/crimson-desert/`
- See `engines/blackspace-engine/`
- Source data: `RE-AI/See the RE-AI output directory.
## v3 project input
Pattern A-VMT is one of TWO distinct VM architectures the v3 "restore original x86" project must handle. The other is Denuvo ATD (Pattern A-DW). See `RE-AI/ANTI-TAMPER-TAXONOMY.md` Pattern A-DW for the sibling.
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern A-VMT
- See the RE-AI output directory for per-binary triage data. (canonical source)
- RE-AI `servers/re-lief/src/re_lief/protection_catalog.py` `_classify_avmt_signature` (the v2.9.1+ classifier)
- RE-UNLEASHED `publishers/pearl-abyss/crimson-desert/`
- RE-UNLEASHED `engines/blackspace-engine/`
@@ -0,0 +1,24 @@
# Pattern C — proprietary-engine encrypted VM
**Status:** Stable in `ANTI-TAMPER-TAXONOMY.md` since v2.7.0. Vendor-named mirror entry here.
## Architecture
The proprietary-engine category: a game studio's own engine ships its own encrypted-VM protection (no third-party ATD wrapping). Examples:
- Proprietary engine with dispatcher-table architecture (refined to Pattern A-VMT in the taxonomy)
- Proprietary engine with encrypted-VM-style section layout
- Custom game engine with light encrypted-VM variant
The Pattern C section set is a superset of the encrypted-VM family but lacks the specific ATD signals (POGO entry, .trace).
## Empirical cases
- Representative target — proprietary engine (refined to Pattern A-VMT)
- Representative target — proprietary engine (encrypted-VM-style sections)
- Representative target — custom game engine (light variant)
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern C
- See the RE-AI output directory for per-binary triage data.
- RE-UNLEASHED `engines/`
@@ -0,0 +1,28 @@
# Anti-debug
**Pattern:** Cross-cutting observation in the per-target analysis (the v2.9.0 stress test's per-binary anti_analysis JSON files all carry the same anti-analysis primitive counts).
## Status
**Placeholder.** VM-protected targets typically show:
- 200+ RDTSC matches (timing-trap)
- 200+ INT 2D / INT 3 matches (kernel debug exception + INT3)
- 200+ CPUID matches (VM detection)
- 200+ VMXON matches (hypervisor detection)
- 200+ INVD matches (emulator detection)
- 27-36 VMCALLs (hypervisor call)
IL2CPP launchers are typically clean at the launcher layer (the protection is in `GameAssembly.dll`, not the launcher).
## Detection
- `re-anti-analysis.scan_anti_analysis_primitives` returns per-primitive byte-hit counts
- `re-hypervisor-detect.classify_hypervisor_posture` returns "kernel-active" for the 4 VM targets
- `re-leak-scan` does not apply to anti-debug (no telemetry aspect)
## Cross-references
- RE-AI `ANTI-TAMPER-TAXONOMY.md` Pattern B
- See the RE-AI output directory for per-binary triage data. (the per-target anti-analysis data)
- RE-AI `servers/re-anti-analysis/` (the byte-sequence scanner)
- RE-AI `servers/re-hypervisor-detect/` (the posture classifier)

Some files were not shown because too many files have changed in this diff Show More