disrobe is a deterministic multi-language deobfuscator and decompiler suite. The same input bytes produce the same output bytes on every machine and every run. Keep that one rule in mind and the rest of this document is detail.
git clone https://github.com/1-3-7/disrobe
cd disrobe
cargo build --workspace
cargo test --workspace
rust-toolchain.toml pins the exact Rust version so your local rustfmt and clippy match CI byte-for-byte. Builds that touch the pyo3 bindings need a Python on PATH, and PYO3_PYTHON pins a specific one. Install the git hooks once with:
cargo xtask setup-hooks
That points git at lefthook, which runs the pre-commit checks and a pre-push pre-mortem (cargo xtask prepush). The pre-push gate is the single source of truth shared with CI: it formats every changed crate (cargo fmt -p <crate> -- --check), re-checks generated-artifact freshness when the change touches a generator or its inputs, and runs cargo clippy --workspace --all-targets -- -D warnings, failing fast and printing the exact fix for whatever drifted. Run cargo xtask prepush --full before a release to gate every crate unscoped. LEFTHOOK=0 git push or git push --no-verify skips it in an emergency.
The workspace has 53 crates, layered core -> ir -> passes -> surfaces.
Core and plumbing
disrobe-core- shared traits (Pass,Detector), error types, the chain detector/registry/precedence machinery, and pass-dispatch primitives.disrobe-ir- the five-rung ladder IR (Raw / Disasm / MIR / HIR / Surface), the.drenvelope codec, and the transcode registry.disrobe-binfmt- container and archive detection and extraction across 98 in-tree formats.disrobe-py-marshal- the CPythonmarshalcodec and.pycencoder/decoder, spanning Python 1.0 through 3.15.disrobe-llm-metadata- the versioned deterministic metadata envelope and the per-pass emitter trait behind--metadata-pack-*/--llm.disrobe-validator- the corpus walker and benchmark harness; asserts byte-level determinism and exports HTML/JSON reports.
Python passes
disrobe-pass-pyarmor- PyArmor v6/v7 dynamic-hook plus v8/v9-pro static unpack.disrobe-pyarmor-cextract- C-levelPyEval_EvalCodeintercept (PEP 669 / settrace) for v6/v7 user-code capture.disrobe-pyarmor-pytrace- Python-level audit-hook andsys.settracecompanion to cextract.disrobe-pass-pyinstaller- PyInstaller 2.1 through 6.x extract plus AES-CTR/CFB decrypt.disrobe-pass-pyfreeze- cx_Freeze / py2exe / shiv / pex / PyOxidizer / Briefcase detect and extract.disrobe-pass-nuitka---onefileand--standalonepayload extract plus symbol scan.disrobe-pass-py-deob- obfuscator peel plus AST constant-fold and dead-branch cleanup.disrobe-pass-py-disasm- the.pycdisassembler (CPython 1.0 through 3.15 plus PyPy / MicroPython / Jython / IronPython).disrobe-pass-py-decompile- the native.pyc-to-source engine with frame-tree reconstruction and round-trip verification.disrobe-pass-sourcedefender-.pyeenvelope structure parse and detect (the KDF is unvalidated against the commercial tool, so it stays detect-only until a known-answer vector lands).
Other ecosystems
disrobe-pass-js-deob- JS/TS string-array decode, unminify, scope-aware rename, and bundle splitting.disrobe-pass-wasm-deob- WebAssembly analyze and lift (Rust / TS / WAT / C) plus 5 obfuscator families.disrobe-pass-jvm- in-house classfile and dex parse, ProGuard/R8 mapping replay, and protector detect and structural peel, with headless CFR / Vineflower / Procyon / jadx wraps.disrobe-pass-dotnet- in-house PE/CLR/table-stream parse, obfuscator reversers (ConfuserEx2 constant decrypt, ...), and R2R header classify, with headless ILSpy / dnSpyEx / de4dot wraps.disrobe-pass-native- PE / ELF / Mach-O symbol recovery and in-house packer decoders (UPX / Petite / kkrunchy / MEW via a stub emulator), with a headless Ghidra wrap.disrobe-pass-go- Go binary recovery: pclntab, moduledata, garble report, andembed.FSextraction.disrobe-pass-swift-objc- Swift and Objective-C class-dump, SwiftShield undo, and Confidential XOR-decrypt.disrobe-pass-mobile- React Native / Hermes / Flutter / Cordova / Capacitor / NativeScript / Xamarin.disrobe-pass-lua- Lua 5.1 through 5.4 / LuaJIT / Luau / GLua decompile and obfuscator peel.disrobe-pass-php- encoder decode (phar / ionCube / SourceGuardian / Zend Guard) and eval-chain peel.disrobe-pass-ruby- MRI / YARV / mruby / JRuby / TruffleRuby / Ruby2Exe / Ocra analysis.disrobe-pass-beam- BEAM (Erlang / Elixir) IFF chunk parse, Core Erlang lift, and Code chunk disasm.disrobe-pass-as3- ActionScript 3 SWF parse and DoABC tag disasm.disrobe-pass-shell- shell-script deobfuscation.
Surfaces
disrobe-cli- thedisrobebinary: every subcommand, the HTTP/gRPC/LSPservedaemon, and the chain orchestrator.disrobe-python- pyo3 bindings exposing the library as the importabledisrobemodule.
main is always releasable. Work in feature branches, open a PR, and squash-merge when green. Branch names: short and descriptive, like pass/<name> or <area>/<slug>. Keep them short-lived.
- Decide which IR rung your pass targets (Raw=0 through Surface=4) and what
REQUIRES/PRODUCESmean in the capability system; seecrates/disrobe-ir/src/lib.rs. - Create
crates/disrobe-pass-<name>/. The minimum is aCargo.tomlwith workspace dependency inheritance and alib.rsthat registers aPassDescriptor. Add achainfeature and achain_detectormodule if it should join the auto-chain. - Wire it into
disrobe-cli: add the crate as anoptional = truedependency, add a matching<ecosystem>feature under[features], declare the CLI sub-module behind#[cfg(feature = "...")], gate theCmdvariant and dispatch arm the same way, and register it in the chain registry (cli/chain_v1.rs) under that cfg. Document the new flag in the README and the relevant docs page. - Emit at least
--emit sourceand--emit report. Output must be deterministic: the same input bytes produce the same output bytes. - Add a fixture under
corpus/(next section) and a test that pins behavior against it. - Benchmark with
cargo bench -p disrobe-pass-<name>and note the baseline in the PR.
Grey-zone protectors (commercial obfuscators with active legal programs: VMProtect, Themida, certain DRM stacks) require an issue first. A research review runs before any pass targeting one of these ships. This is not about discouraging the work; it documents the statutory basis before code lands.
Fixtures live under corpus/<ecosystem>/. Real-tool-generated artifacts are preferred over synthetic ones. When a fixture is heavy or platform-built it stays gitignored and a regeneration recipe ships instead.
- Add a generator: extend
corpus/generate.sh/corpus/generate.ps1. - Record provenance in the ecosystem's
MANIFEST.toml:schema_version, adescription, and one[[fixtures]]block per artifact withpath,format,tool, andprovenance. Byte-identical reproducibility is the goal; note where a fixture cannot be reproduced cross-host. - Small reproducible binaries may be committed (un-ignore with a
!negation in.gitignore); large or non-reproducible ones stay ignored. Tests that depend on a gitignored fixture are marked#[ignore]with a regeneration hint in the message.
Every pass can emit a versioned metadata sidecar alongside its normal output. The schema is disrobe.metadata.llm.v1 (see disrobe-llm-metadata). The schema id is a stable API name; the payload is deterministic metadata, not model output. Selection is additive:
- Packs:
--metadata-pack-1(ast + disasm + symbols + strings) through--metadata-pack-4(pack-3 + confidence + opcode-coverage + pii-map + decryption-keys, auth-gated).--llmaliases pack-4. - Categories: granular toggles like
--ast,--disasm,--cfg,--dfg,--symbols,--strings,--types,--imports,--constants, and--signaturescompose with the packs.
A pass implements LlmMetadataEmitter::emit_metadata(&selection); the CLI writes the bundle next to the primary artifact with a per-pass PipelineStep provenance record (input hash, consumed and produced rungs, duration). The bundle is deterministic and never phones home.
The docs live under docs/src and are the single source of truth, rendered by mdBook to the docs site. The GitHub wiki is generated from docs/src by scripts/wiki_sync.py and the wiki-sync workflow on every push to main that touches docs/. Do not edit wiki pages directly; they are overwritten on the next sync. Edit the matching file under docs/src instead, and regenerate locally to preview:
python scripts/wiki_sync.py --out ./.wiki-build # build the wiki tree
python scripts/wiki_sync.py --check --out ./.wiki-build # fail on drift
Run the full chain before you push; CI runs the same with RUSTFLAGS=-D warnings:
cargo check --workspace --all-features
cargo check --workspace --no-default-features
cargo clippy --workspace --all-features --all-targets -- -D warnings -W unreachable_pub -W missing_debug_implementations -W unused
cargo fmt -p <crate> -- --check
cargo test --workspace # run per-crate if RAM is tight
cargo deny check
typos
cargo run -p xtask -- regen --check # every generated artifact must be byte-fresh
Determinism rules the linter enforces:
HashMapandHashSetare banned from emit paths viaclippy.toml. UseIndexMap/IndexSet, orBTreeMap/BTreeSetfor sorted output.- No
SystemTime::now()or unseeded randomness inside a pass. Route throughdisrobe_core::timeanddisrobe_core::rng. - No
.unwrap()or.expect()in production library code. Tests may use them when they make the failing condition clearer than manual error plumbing. - The source carries no comments. Names and structure do the explaining; durable why and context lives in the crate's
INFORMATION.md, not inline.
Found a vulnerability (a sandbox escape, a path traversal in an extractor, a pass that writes outside its declared output directory)? Do not open a public issue. Follow the private disclosure process in SECURITY.md. Include a minimal reproducer (input bytes, command line, expected versus observed behavior), the disrobe --version output, and the OS and architecture.
Lowercase, specific, plain words. Say what changed, no type: prefixes. The body is optional and only for non-obvious motivation, never a restatement of the diff. Use a noreply email so your address stays private:
git -c user.email="<id>+<handle>@users.noreply.github.com" commit -m "add pyarmor v9 co_code reconstruction"
Do not attribute authorship to anyone not listed in NOTICE, and do not add a co-author trailer.
Releases are prebuilt binaries only, cut by .github/workflows/release.yml on a vX.Y.Z tag or a manual dispatch. The workflow cross-builds per-target artifacts with build attestations and publishes a GitHub release. disrobe is not published to crates.io, PyPI, Homebrew, or winget; there is nothing to install from a registry. To run a release build from source: cargo build --release --bin disrobe.
- Any pass that produces non-deterministic output.
- Code with
// TODO,// FIXME,unimplemented!(), ortodo!()in a non-stub position. - Anything that phones home, writes outside the declared output directory, or modifies the input.
- Attribution of authorship to anyone not listed in
NOTICE.