# Handle obfuscated input

> Stage 1 workflow for packed, encoded, or Obfuscator.IO input: detect, unpack, string-array, decode-strings, simplify, control-flow-report, and critical ordering before Stage 2 rename.

- Repository: JimLiu/decode-codex
- GitHub: https://github.com/JimLiu/decode-codex
- Human docs: https://grok-wiki.com/public/docs/jimliu-decode-codex-1a3a0c425b33
- Complete Markdown: https://grok-wiki.com/public/docs/jimliu-decode-codex-1a3a0c425b33/llms-full.txt

## Source Files

- `.agents/skills/deobfuscate-javascript/workflows/full-obfuscation.md`
- `.agents/skills/deobfuscate-javascript/stages/stage-1-deobfuscate.md`
- `.agents/skills/deobfuscate-javascript/scripts/detect.ts`
- `.agents/skills/deobfuscate-javascript/scripts/unpack.ts`
- `.agents/skills/deobfuscate-javascript/scripts/string-array.ts`
- `.agents/skills/deobfuscate-javascript/scripts/decode-strings.ts`
- `.agents/skills/deobfuscate-javascript/scripts/deobfuscate.ts`

---

---
title: "Handle obfuscated input"
description: "Stage 1 workflow for packed, encoded, or Obfuscator.IO input: detect, unpack, string-array, decode-strings, simplify, control-flow-report, and critical ordering before Stage 2 rename."
---

Stage 1 in `.agents/skills/deobfuscate-javascript/scripts/` unwinds obfuscator transformations with pure Babel passes—no LLM dependency. The `deobfuscate.ts` orchestrator runs `detect` → `unpack` → `string-array` → `decode-strings` → `simplify` → `control-flow-report` in a fixed order, re-running `detect` after `unpack` when unpacking reveals hidden techniques. Output is syntactically normal JavaScript with cryptic names intact; Stage 2 rename and polish must not start until Stage 1 completes because `extract.ts` byte offsets and `renames.json` ids are invalid across Stage 1 rewrites.

<Note>
Run `sourcemap-check.ts` before committing to Stage 1. If a usable `.map` exists, sourcemap recovery is strictly better than Stage 1 + Stage 2.
</Note>

## When to run Stage 1

| Input signal | Action |
|---|---|
| `detect.ts` reports techniques with confidence ≥ 0.5 | Run Stage 1 (one-shot `deobfuscate.ts` or step-by-step scripts) |
| `detect.ts` returns empty `techniques` | Skip Stage 1; go straight to Stage 2 (`wakaru-normalize` → `extract` → rename) |
| Packer / AAEncode / Obfuscator.IO `_0x` arrays / hex escapes / opaque predicates / control-flow flattening | Stage 1 required |
| Pure minification (short identifiers, single-line uglify, no obfuscation techniques) | Skip Stage 1 |

`detect.ts` recognizes: Packer, AAEncode, URL-encoded payloads, Obfuscator.IO `_0x` identifiers, string-array declarations and rotation IIFEs, hex and unicode escapes, `String.fromCharCode`, `atob`, control-flow flattening (`while(true){switch(...)}` and split-string dispatch), opaque predicates, dead-code injection, webpack signatures, and single-line uglify.

## Pipeline architecture

```mermaid
sequenceDiagram
  participant Input as original.js
  participant Detect as detect.ts
  participant Unpack as unpack.ts
  participant SA as string-array.ts
  participant DS as decode-strings.ts
  participant Sim as simplify.ts
  participant CFR as control-flow-report.ts
  participant Out as stageA.js

  Input->>Detect: techniques + recommendation
  Detect->>Unpack: packed layers
  Unpack->>Detect: detect-after-unpack (if changed)
  Unpack->>SA: AST with _0x arrays
  SA->>DS: inlined string literals
  DS->>Sim: decoded literals + normalized escapes
  Sim->>CFR: folded constants, removed dead code
  CFR-->>Out: mutated code + read-only report JSON
```

Mutating steps rewrite the AST. `control-flow-report` is read-only—it emits JSON for manual unflattening and does not change the file.

## Workspace setup

Stage 1 intermediates live under a per-chunk workspace nested in the target directory:

```bash
INPUT=ref/webview/assets/sample-Ab12Cd34.js
TARGET=restored
SKILL=.agents/skills/deobfuscate-javascript
WS="$TARGET/.deobfuscate-javascript/$(basename "$INPUT" .js)"

mkdir -p "$WS"
cp "$INPUT" "$WS/original.js"
```

Typical Stage 1 artifacts in `$WS/`:

| File | Produced by |
|---|---|
| `original.js` | Archived pristine input |
| `stageA.js` | `deobfuscate.ts --out` |
| `stageA.json` | `deobfuscate.ts --report` |

## Standard run

<Steps>
<Step title="Detect obfuscation">

```bash
cd "$SKILL"
bun scripts/detect.ts "$WS/original.js"
```

Read the JSON `recommendation` field. If `techniques` is empty, skip Stage 1 and continue with [Deobfuscate a single file](/deobfuscate-single-file).

</Step>
<Step title="Run the orchestrator">

```bash
bun scripts/deobfuscate.ts "$WS/original.js" \
  --out "$WS/stageA.js" \
  --report "$WS/stageA.json"
```

Stderr logs per-step summaries. The report JSON records `originalSize`, `finalSize`, `reduction`, and a `steps` array with per-step stats or errors.

</Step>
<Step title="Verify output">

Confirm in `$WS/stageA.json`:

- `string-array`: `referencesReplaced > 0` when Obfuscator.IO arrays were present
- `decode-strings`: `fromCharCode` / `atob` counts or `escapesNormalized: true` when encodings were present
- `simplify`: `deadIfsRemoved > 0` or `constantsFolded > 0` when dead code or opaque predicates were present
- `control-flow-report`: `flatteners` length is 0, or plan a manual rewrite (see below)

Read `$WS/stageA.js`—no `_0x` identifiers, no hex escape walls, no `eval(function(p,a,c,k,e,d)`.

</Step>
<Step title="Continue to Stage 2">

Feed `$WS/stageA.js` (not `original.js`) into Stage 2: `wakaru-normalize` → `extract` → `smart-rename` → `apply` → `polish`. See [Restoration pipeline](/restoration-pipeline) and [Deobfuscate a single file](/deobfuscate-single-file).

</Step>
</Steps>

## Step reference

### detect

```bash
bun scripts/detect.ts <input.js|-> [--out report.json]
```

<ResponseField name="techniques" type="array">
Each entry: `{ name, confidence, evidence }`. Sorted by confidence descending.
</ResponseField>

<ResponseField name="recommendation" type="string">
Ordered script list or guidance to skip Stage 1 when no obfuscation is found.
</ResponseField>

### unpack

Unpacks layered wrappers: Dean Edwards Packer → AAEncode → URLEncode, up to `--max-iterations` (default 5).

```bash
bun scripts/unpack.ts <input.js|-> [--out output.js] [--max-iterations 5] [--no-eval]
```

| Layer | Detection | Eval required |
|---|---|---|
| Packer | `eval(function(p,a,c,k,e,d){...})` signature | Yes (`new Function` for arg parsing) |
| AAEncode | Emoji glyphs (`゜-゜`, `ω゜`, `(ﾟДﾟ)`) | Yes (`new Function`) |
| URLEncode | >10 `%XX` sequences | No (`decodeURIComponent`) |

<Warning>
`unpack.ts` executes input via `new Function`. Sandboxed JS cannot read your filesystem, but the payload still runs. On untrusted input, gate with `--no-eval` first to confirm a Packer wrapper without executing it.
</Warning>

<ParamField body="--no-eval" type="boolean">
Refuses Packer and AAEncode evaluation. Exits 0 with input unchanged and `evalRefused: true` in the result. URLEncode still runs.
</ParamField>

### string-array

Inlines Obfuscator.IO `var _0x123 = ['log','hi',…]` arrays, applies rotation IIFE correction, replaces indexed refs, and removes dead array declarations.

```bash
bun scripts/string-array.ts <input.js|-> [--out output.js]
```

<ResponseField name="decoderIndirection" type="boolean">
`true` when arrays were collected but zero references were replaced—access is wrapped behind a decoder function. Run `simplify.ts` first (inlines small constant functions), then re-run `string-array`.
</ResponseField>

### decode-strings

Literal-only decoder: `String.fromCharCode(72,101,…)` → `"He"` (all-numeric args), `atob("…")` with string-literal arg, and `\xNN` / `\uNNNN` escapes normalized to characters. Variables and non-literal args are left alone.

```bash
bun scripts/decode-strings.ts <input.js|-> [--out output.js]
```

### simplify

Loops to a fixed point (default `--max-passes 10`). Per pass: constant folding, dead-code removal, logical short-circuit, identity ops, sequence expansion, computed-to-dot, and scope-aware literal inlining.

```bash
bun scripts/simplify.ts <input.js|-> [--out output.js] [--max-passes 10] [--no-inline]
```

<ParamField body="--no-inline" type="boolean">
Skips `var k = 5; …k` → `…5` literal inlining. Constant folding and dead-code removal still run. Use when you want binding names preserved for Stage 2's renamer.
</ParamField>

### control-flow-report

Read-only analysis. Does not mutate code.

```bash
bun scripts/control-flow-report.ts <input.js|-> [--out report.json]
```

Detects:

| Pattern kind | Shape | Action |
|---|---|---|
| `while-switch-flattening` | `while(true){switch(state){case 0:…}}` | Manual rewrite using `dispatchVariable`, `caseLabels`, `containingFunction` |
| `split-string-dispatch` | `"0\|1\|2".split("\|")` | Find corresponding switch and inline in dispatch order |
| `opaque-predicate` | `if(1===2)`, `if(!![])`, `if(void 0)` missed by simplify | Apply `rewriteHint` (replace with then/else branch) |

Automatic CFG reconstruction is unreliable. The report lays out the dispatch graph; you trace state transitions and inline cases in execution order, then re-run `simplify.ts` on the rewritten code.

## Orchestrator flags

```bash
bun scripts/deobfuscate.ts <input.js|-> \
  [--out output.js] [--report report.json] \
  [--skip step1,step2] [--stop-after step] \
  [--no-eval] [--no-inline] \
  [--max-iterations 5] [--max-passes 10]
```

Valid step names for `--skip` and `--stop-after`:

`detect`, `unpack`, `string-array`, `decode-strings`, `simplify`, `control-flow-report`

| Behavior | Detail |
|---|---|
| Per-step try/catch | One failing AST pass does not abort the rest; errors appear in `--report` JSON |
| Re-detect after unpack | When `unpack` changes the code and `detect` is not skipped, runs `detect-after-unpack` |
| `--stop-after control-flow-report` | Useful to inspect flatteners before continuing to Stage 2 |

## Critical ordering

Out-of-order runs silently no-op or mangle output:

```
unpack  →  string-array  →  decode-strings  →  simplify  →  control-flow-report
                                                              ↓
                                                    Stage 2 (wakaru → extract → rename)
```

| Rule | Reason |
|---|---|
| `unpack` first | Packed input is one giant `CallExpression`; AST passes cannot parse it |
| `string-array` before `decode-strings` | Inlining first limits decode work to used literals only |
| `simplify` after `string-array` + `decode-strings` | Folding needs resolved literals; running simplify first splits rotation IIFEs that `string-array` won't match |
| `control-flow-report` last | Opaque predicates fold out in simplify first; report captures remaining CFG |
| Stage 1 before Stage 2 | `extract.ts` offsets and `renames.json` ids are stale after Stage 1 rewrites |
| Stage 1 → wakaru → extract | wakaru is byte-rewriting; it runs after Stage 1 but before any extract/apply |

## Untrusted and Packer-wrapped input

<CodeGroup>

```bash title="Gate with --no-eval"
cd "$SKILL"
bun scripts/unpack.ts "$WS/original.js" --no-eval
# Visually verify the packer wrapper looks legit
```

```bash title="Allow eval after inspection"
bun scripts/deobfuscate.ts "$WS/original.js" --out "$WS/clean.js"
```

</CodeGroup>

With `--no-eval`, stderr shows `eval refused` and the result includes `evalRefused: true`. Without it, stderr warns before each `new Function` evaluation.

## Decoder indirection recovery

When `string-array` reports `DECODER-INDIRECTION` or `decoderIndirection: true` in the orchestrator report:

<Steps>
<Step title="Run simplify on current code">

```bash
bun scripts/simplify.ts "$WS/stageA-partial.js" --out "$WS/after-simplify.js"
```

</Step>
<Step title="Re-run string-array">

```bash
bun scripts/string-array.ts "$WS/after-simplify.js" --out "$WS/after-string-array.js"
```

</Step>
<Step title="Continue the pipeline">

Resume with `decode-strings` → `simplify` → `control-flow-report`, or re-run `deobfuscate.ts` from the simplify output.

</Step>
</Steps>

If references are still zero after simplify, the rotation pattern may be non-standard—manual decoder rewrite is required.

## Troubleshooting

<AccordionGroup>
<Accordion title="unpack says no packer detected but eval wrapper is visible">

The detector requires the exact `(p, a, c, k, e, d|r)` parameter signature. Custom or renamed parameters won't match. Rename wrapper params to the standard signature, or execute the inner body manually in a JS REPL.

</Accordion>
<Accordion title="string-array replaced 0 references">

Check for `decoderIndirection`. Run `simplify.ts` first, then re-run `string-array`. See [Pipeline caveats](/pipeline-caveats).

</Accordion>
<Accordion title="control-flow-report shows flatteners">

Read each `flatteners[i]` entry: note `dispatchVariable`, `caseLabels`, `containingFunction`, and `loc`. Open source at `loc.start..loc.end`, trace `case → next-state → next case`, inline reachable statements, skip unreachable cases. Re-run `simplify.ts` afterward.

</Accordion>
<Accordion title="Orchestrator step errored but kept going">

By design. Read `--report` JSON for the error. Fix input (often a syntax error from a prior step), or `--skip` the failing step and continue.

</Accordion>
<Accordion title="simplify inlined too aggressively">

Pass `--no-inline` to preserve binding names for Stage 2 while keeping constant folding and dead-code removal.

</Accordion>
</AccordionGroup>

## Expected verification signals

After a successful Stage 1 run on Obfuscator.IO composite input:

- `$WS/stageA.js` contains readable string literals (`"hello world"`, `"Hi"`)
- No `_0xc0de` identifiers, no `fromCharCode` calls, no `if (false)` or `if (5 > 3)` dead branches
- `finalSize < originalSize` in the report
- Stderr ends with `deobfuscate: done — <original> → <final> bytes`

## Related pages

<CardGroup>
<Card title="Stage 1 scripts reference" href="/stage-1-scripts">
CLI signatures, flags, and per-script behavior for every Stage 1 script.
</Card>
<Card title="Restoration pipeline" href="/restoration-pipeline">
Where Stage 1 fits between sourcemap check, Stage 2 rename, and Stage 3 finalize.
</Card>
<Card title="Pipeline caveats" href="/pipeline-caveats">
Ordering rules, eval safety, decoder indirection, and sourcemap invalidation recovery.
</Card>
<Card title="Deobfuscate a single file" href="/deobfuscate-single-file">
Continue from `$WS/stageA.js` through wakaru, extract, rename, and polish.
</Card>
</CardGroup>
