DEV Community

Ted
Ted

Posted on Originally published at tedagentic.com

REA: Giving My Coding Agent Real Reverse-Engineering Tools (and What I Had to Verify First)

Ask an AI coding agent how a program works and it will usually tell you. Fluently, in detail, and mostly from memory: what programs like that tend to do, reconstructed from training data. The actual file on your disk never gets opened.

REA changes that. This post covers what it is, what it's useful for, how I set it up, what I had to verify before I trusted it, and two tests that show it working.

What REA is

REA ("Reverse Engineer Anything") is a free, open-source, MIT-licensed tool that connects AI coding agents to real reverse-engineering software. It's published as the npm package rea-agents from the GitHub repo morluto/rea.

It works as an MCP server. MCP, the Model Context Protocol, is the plug standard coding agents like Claude Code, Codex, Cursor and Gemini CLI use to call outside tools. REA doesn't do the decompiling itself. It's a routing layer that picks the right engine for the target:

  • Native programs (Linux/Windows/macOS executables): Ghidra, the NSA's open-source reverse-engineering suite, or IDA/Hopper if you have them.
  • Android apps (APKs): JADX, the standard open-source Android decompiler.
  • JavaScript/Electron apps, .NET assemblies, websites, saved network captures: built-in static analysis, no extra engine needed.

What comes back isn't a chat answer. It's structured evidence: decompiled code, strings, addresses, cross-references, an ID for each finding, and a list of limitations, meaning what this analysis can't tell you. Everything runs locally.

The project moves fast and has attracted copycats, so its README says to install only from morluto/rea, rea.tools, or the rea-agents npm package. There's also an older @morluto/rea package on npm from the same author, stuck at version 0.2.1. The current one is rea-agents.

What it's useful for

  • Learning how software actually works: pick a feature and read the real code behind it.
  • Security practice: crackmes, CTF binaries, malware-analysis exercises.
  • Checking what an app really does: why does this app want location access? What does it send, and where?
  • Debugging your own builds: what actually ended up in the shipped bundle or binary.

One rule I hold to: only analyse software you own, open-source projects, or things you're explicitly allowed to examine. REA's own docs scope it to lawful research, education, interoperability and authorised analysis.

How I set it up

My goal was simple: type rea, land in an agent session with every tool ready, and keep all of it away from my everyday agent setup, which touches production scripts.

1. A local install, pinned

Rather than a global install, I installed it into its own folder at an exact version, with dependency install scripts disabled:

mkdir -p ~/rea-sandbox && cd ~/rea-sandbox
npm init -y
npm install --ignore-scripts --save-exact rea-agents@6.7.0
Enter fullscreen mode Exit fullscreen mode

It needs Node 22.19+ (or 24.11+, or 26+).

2. A separate agent profile

REA's installer registers itself with whichever agents you choose. Claude Code reads its configuration from a directory you can override with CLAUDE_CONFIG_DIR, and REA's setup respects that. So everything went into a second profile:

CLAUDE_CONFIG_DIR=~/.claude-rea ./node_modules/.bin/rea setup --client claude_code --dry-run
Enter fullscreen mode Exit fullscreen mode

--dry-run prints exactly which files it will write. It wanted two paths, both inside ~/.claude-rea: the tool registration and a workflow "skill". It also detected four other agents on my machine and left them alone, because only claude_code was selected. After reading the plan, I ran it again with --yes instead of --dry-run.

3. Ghidra, for native programs

Ghidra needs a Java 21 runtime. I took the release straight from the NSA's GitHub and checked the SHA-256 published in the release notes:

sudo apt install openjdk-21-jdk-headless
gh release download Ghidra_12.1.4_build -R NationalSecurityAgency/ghidra -p 'ghidra_*.zip'
sha256sum ghidra_12.1.4_PUBLIC_20260921.zip   # compare with the release notes
unzip ghidra_12.1.4_PUBLIC_20260921.zip
Enter fullscreen mode Exit fullscreen mode

Then I re-ran setup with GHIDRA_INSTALL_DIR pointing at the unpacked folder. REA checks the installation and saves the path into the agent registration. 12.1.4 is the version REA lists as supported.

4. JADX, for Android apps

This was the part that needed the most care, so it gets its own section below. The short version: REA talks to JADX through a small third-party wrapper called jadx-headless-mcp, pinned to version 0.7.1. I built it from source, confirmed my build matched the published release byte for byte, and pointed REA at it with REA_JADX_MCP_JAR.

5. One command: rea

A short script in ~/.local/bin/rea ties it together:

#!/usr/bin/env bash
# rea          → open the agent in the REA profile, all tools wired
# rea <args>   → run the REA CLI itself (rea doctor, rea analyze FILE, ...)
SANDBOX="$HOME/rea-sandbox"
export CLAUDE_CONFIG_DIR="$HOME/.claude-rea"
export GHIDRA_INSTALL_DIR="$SANDBOX/ghidra_12.1.4_PUBLIC"
export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"
export REA_JADX_MCP_JAR="$SANDBOX/jadx/jadx-headless-mcp-0.7.1-all.jar"

if [ $# -gt 0 ]; then
  exec "$SANDBOX/node_modules/.bin/rea" "$@"
fi
cd "$SANDBOX/targets" && exec claude
Enter fullscreen mode Exit fullscreen mode

The two-mode design matters. rea is also the name of REA's own CLI, and the agent inside the session calls it. If bare rea only launched the agent, the agent calling rea doctor would try to launch another agent inside itself.

Finally, a settings.json in the new profile pre-approves REA's tools ("allow": ["mcp__rea"]), so analysis doesn't stop for a permission prompt on every call. That approval exists in this profile only.

Health check:

rea doctor --client claude_code --skill --json   # healthy: true, ghidra + jadx available
Enter fullscreen mode Exit fullscreen mode

What I had to verify first

The install was an afternoon. Most of that afternoon went on one question I didn't expect to be the hard part: what does each check actually prove?

The profile isn't a sandbox

A separate profile felt like isolation. REA's own docs are blunt about what it is:

Both routes control which sessions discover REA's MCP server and skill. They leave shell commands, filesystem access and network access available.

The profile controls which tools the agent is offered, not what it can reach. It still runs as my user and can still read my files and use the network. A real boundary needs a separate OS account or a container. For learning and inspecting software I'm allowed to inspect, that's fine. But it's a menu, not a wall, and I wrote it down in those words.

The commit that "didn't exist"

REA's third-party notices pin the JADX wrapper to commit 5844800a, the source its authors say they audited. The wrapper's v0.7.1 release tag points to a different commit, 7dc6b5e3, and GitHub said the two have no common ancestor.

That reads like a supply-chain incident. It wasn't, and the way to check is worth knowing. Every git commit points to a tree, the hash of the complete file contents:

git clone --depth 1 --branch v0.7.1 https://github.com/1013503897/jadx-headless-mcp.git
cd jadx-headless-mcp && git rev-parse 'HEAD^{tree}'
# 0f280ac59d851c4a126f9f5efbe26366d2fd033c — same tree as REA's pinned commit
Enter fullscreen mode Exit fullscreen mode

Same tree, same date, same message. The only difference was the author name. Someone had rewritten the repo's history to change how their name appears, which gives every commit a new hash while leaving every file identical.

Commit hashes identify history. Tree hashes identify content.

The checksum proved the download, not the source

The release offers a prebuilt JAR and a .sha256 file. The hash matched. But both came from the same release page, published by the same account. A matching checksum proves the file wasn't corrupted on the way to my disk. It doesn't prove the JAR was built from the audited source. A tampered release would ship a tampered hash, and it would still match.

A checksum from the same source as the file proves integrity, not origin.

The Ghidra hash had the same shape: it sits on the same page as the download. I trust it because of who publishes it, which is a judgement about the publisher, not something the hash proves.

Build it yourself and compare bytes

The only real test was to build from source. First I read the build script: two well-known Gradle plugins and standard libraries from Maven Central and Google, with no custom download steps and no shell commands. Then I checked the bundled Gradle wrapper. That's the small JAR that fetches the build tool, and a classic place to hide something. Its hash matched the one Gradle publishes on its own site, a different publisher, so that check actually means something:

sha256sum gradle/wrapper/gradle-wrapper.jar
curl -sL https://services.gradle.org/distributions/gradle-9.7.1-wrapper.jar.sha256
Enter fullscreen mode Exit fullscreen mode

The build needs a JDK 17 toolchain:

sudo apt install openjdk-17-jdk-headless
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew --no-daemon \
  -Dorg.gradle.java.installations.paths=/usr/lib/jvm/java-17-openjdk-amd64 \
  shadowJar -x test
sha256sum build/libs/jadx-headless-mcp-0.7.1-all.jar
Enter fullscreen mode Exit fullscreen mode

My agent refused to run that build itself. Its safety layer treats executing a freshly cloned repo's build scripts as running outside code, and it held that line even after I approved it. I ran it by hand, which is the right division of labour.

The build is configured to be reproducible: timestamps stripped and file order sorted, so the same source gives the same bytes. Mine came out as:

6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75
Enter fullscreen mode Exit fullscreen mode

The release JAR's hash is exactly the same: 6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75.

Byte-identical. Now the prebuilt JAR is proven. That's not because its own hash file says so. It's because an independent build from audited source produced exactly the same file.

A reproducible build turns a checksum from a claim into a test.

Two real tests

A Linux program, with Ghidra

The danger with an AI explaining code is a fluent answer from memory. So the first test used a program every model already "knows": the base64 utility, copied from my own system. I asked how it decides between encoding and decoding, and to show the code behind the -d flag.

What came back was specific to the file, not to base64 in general:

  • The binary is stripped (no function names left), so it identified functions by address.
  • It found the string decode at file offset 0x71ff, referenced from the long-options table that's passed to getopt_long.
  • It showed the option loop comparing against 100 (the character code for d) and setting a one-byte flag. A single later branch reads that flag to choose between the encode and decode paths.
  • It decompiled the four-character decode routine and its 256-entry lookup table.

I checked what I could without trusting it. strings -t x base64 puts decode at 71ff and invalid input at 71f1, and the character codes are right.

What earned more trust was what it flagged. It said its function names were guesses, because they're inferred from behaviour and there are no symbols. It said its description of the decode loop was a cleaned-up reading of tangled decompiler output, not literal code. And every finding came with an evidence ID.

An Android app, with JADX

For the second test I used Binary Eye, an open-source barcode scanner from F-Droid. I compared the APK's hash with the one in F-Droid's repository index; I didn't verify the index's own signature, so by this post's standards that's a same-publisher check. Then I asked REA to inspect it. JADX loaded it in about seven seconds:

  • 2,800 classes and 930 resources. Only 20 classes still had readable names. The build's code shrinker had renamed the rest to things like bo and uu0, which is normal for release Android builds.
  • 11 permissions, including camera (obvious), plus Wi-Fi, internet, location and Bluetooth.

So the question was: why does a barcode scanner want all that?

  • Wi-Fi: explained. One of the readable classes manages Wi-Fi network suggestions. That's what runs when you scan a Wi-Fi QR code and tap "connect."
  • Internet: probably explained. A settings class stores a URL, which fits a "send scans to a server" option. The code that actually sends sits in the renamed classes and wasn't traced.
  • Location and Bluetooth: not explained in this pass.

That last line is the honest result of a seven-second look, and I'd much rather have "not explained yet" than a plausible story.

Honest limits

  • It isn't a sandbox. The separate profile hides the tools from your other sessions; it doesn't contain them. Use a separate user or a container if you need a real boundary.
  • The engines are bring-your-own. Native analysis needs Ghidra, IDA or Hopper, and Android needs the JADX wrapper. Several other targets need their own tools too.
  • It moves fast. REA's docs say to keep it updated, but I pinned an exact version on purpose. Updating is a deliberate step, with a --dry-run first.
  • Evidence still needs checking. REA is far better than an agent reciting from memory, but decompiler output is a reading, not the source. Spot-check the parts you can.

What each check was actually worth

Check What it looked like What it proved
Separate agent profile Isolation Which tools the agent is offered
Hash from the same release page Authenticity The download wasn't corrupted
Hash from a different publisher Authenticity The file matches what that publisher vouches for
Matching tree hash — Two commits contain identical files
Byte-identical rebuild — The binary came from that source
Agent's explanation Analysis Only as much as you can check against the file

None of these checks was wasted. Each one just had a narrower job than its name suggested. The setup became trustworthy at the point where I stopped asking "did it pass?" and started asking "what would have to be true for this to pass while being wrong?"

Top comments (0)