`make check-updates` failed with BEHIND-REA on both Hopper rows: the vendor's
publisher-signed release feed moved its Ubuntu/Mint entry from Hopper 6.6.0 to
6.6.1 on 2026-10-29. REA 6.8.0 admits a Linux Hopper only when the installed
launcher is the feed's *current* main_exe_hash, so the old pin was already
wrong - and the build layer would have refused outright ("Hopper feed moved:
Hopper-6.6.1-Linux-demo.deb is no longer Hopper-6.6.0-Linux-demo.deb").
rea-agents itself is current (6.8.0 is the newest npm release *and* the newest
github tag), so nothing else moved: same REA_VERSION, same Node 24.22.1, same
bundled skill (v49 / 229 tools), same Ghidra.
Re-pinned to the feed's row:
HOPPER_DEMO_VERSION 6.6.0 -> 6.6.1
HOPPER_DEB_SHA1 575efccf.. -> a2c28339b768609dc7766869f51d25a38b49fd2a
HOPPER_DEB_BYTES 35748722 -> 35749234
HOPPER_LAUNCHER_SHA256 b2271008.. -> 74b51599fa2b72cf927aa9d089c7046ee0942fb1c29eb0750ac166b3dff97999
The launcher digest was not taken on trust: the package was downloaded and
unpacked and opt/hopper/bin/Hopper hashed out of it (74b51599...), which is what
the feed claims. The build then re-checks all four against the live feed,
verifies bytes + SHA-1 and the installed launcher, and bakes the signed evidence
under /home/rea/.cache/rea/hopper-releases/74b51599.../ so admission works
offline; `rea doctor --provider hopper --json` reports hopper-version healthy,
reported_release_version 6.6.1, source signed_package_cache.
Also: .env.example carried NODE_VERSION=24.22.0, which 6.8.0's engines.node
(^24.22.1) refuses - compose-based builds would have produced a runtime REA
will not start. Aligned with the Dockerfile at 24.22.1.
Verified: scripts/build-all.sh (all four), scripts/verify-all-engines.sh -
all 121/0/3, ghidra 109/0/8, hopper 102/0/7, ida 94/0/11, 0 failures, exit 0 on
every image; runs 20261031T092059Z / 20261031T093304Z / 20261031T094453Z /
20261031T094650Z. Counts are identical to the 6.8.0 round; docs/verify-engines.txt
regenerated. `make check-updates` is back to 0 actionable / 0 unknown.
12 KiB
The four images: pick an engine, not a toolchain
Most people use exactly one native disassembler. Installing all three engines in every container means carrying Ghidra and Hopper and whatever licence-free corner of IDA you can arrange, and paying for two of them in image size and build time. So this project builds one image per engine, from one Dockerfile:
| Image | REA_ENGINE |
Native engine | Auxiliary toolchain | Use it when |
|---|---|---|---|---|
rea-agent:6.8.0-linux-amd64 |
all |
Ghidra + Hopper | full | you want everything in one container, or you want to compare engines on one target |
rea-agent-ghidra:6.8.0-linux-amd64 |
ghidra |
Ghidra 12.1.4 | full | you analyse many architectures (the usual case) |
rea-agent-hopper:6.8.0-linux-amd64 |
hopper |
Hopper (demo or your licensed DEB) | full | you prefer Hopper's output, or you own a Hopper licence |
rea-agent-ida:6.8.0-linux-amd64 |
ida |
none bundled - bring your own IDA | full | you own IDA and want REA to drive it (see ida-variants.md) |
The auxiliary toolchain is identical in all four. Firmware extraction (binwalk, unblob, 7z/cpio/squashfs/sleuthkit/mtools, binfmt+qemu), capture analysis (tshark, capinfos, sigrok, mitmdump, Chrome CDP, Playwright), managed code (.NET + ilspycmd), Android (adb, Apktool, JADX), JS/Electron, debuggers (gdb-multiarch, pwndbg), pwntools/pyelftools/unicorn, cross toolchains and wine are all present regardless of engine. That is deliberate: those are what REA's non-native families need, and they are engine-independent.
What follows from that: the ida image is still useful with no IDA at all.
Artifacts, firmware, captures, .NET/JS/Android targets, binary layout and
everything else REA does without a disassembler work out of the box; only the
native analysis commands need IDA registered.
How an image knows what it is
Each image carries three things that replace the old "assume Ghidra and Hopper are both here" behaviour:
| Path | Content | Read by |
|---|---|---|
/usr/local/share/rea/engine |
all / ghidra / hopper / ida |
healthcheck, entrypoint, verification harness |
/usr/local/share/rea/engine.env |
only the paths this image needs (GHIDRA_INSTALL_DIR, HOPPER_LAUNCHER_PATH, IDADIR, JAVA_HOME) plus REA_ANALYSIS_PROVIDER for single-engine images |
the rea wrapper |
/usr/local/bin/rea |
wrapper that sources engine.env, then execs the real shim |
every rea invocation |
The wrapper is what makes docker exec -i <container> rea mcp work with no
compose environment at all - and what keeps a single-engine image honest: an
image without Ghidra never advertises GHIDRA_INSTALL_DIR, so REA never sees a
stale path to a missing engine. The healthcheck and the verification harness read
/usr/local/share/rea/engine and skip (rather than fail) on checks for engines the image
does not ship, which is why one harness serves all four.
Pull instead of build (GHCR)
You do not have to build anything. All four images are published to the GitHub
Container Registry by CI on every push to main, and the packages are public
(no login, no token):
| Image | Engine | Tags |
|---|---|---|
ghcr.io/alexmi256/rea-agent |
Ghidra + Hopper | 6.8.0-linux-amd64, latest |
ghcr.io/alexmi256/rea-agent-ghidra |
Ghidra | 6.8.0-linux-amd64, latest |
ghcr.io/alexmi256/rea-agent-hopper |
Hopper | 6.8.0-linux-amd64, latest |
ghcr.io/alexmi256/rea-agent-ida |
no engine (BYO IDA) | 6.8.0-linux-amd64, latest |
# the compose defaults are already these names, so:
docker compose pull # only the active profiles' services (here: rea-agent)
docker compose --profile ghidra up -d rea-ghidra # pulls that image on demand
# or, explicitly per engine
docker pull ghcr.io/alexmi256/rea-agent-ghidra:6.8.0-linux-amd64
make pull ENGINE=hopper # same thing via the Makefile
make pull-all # all four (~8 GB each)
make sizes # what you now have locally, with sizes
A pulled image is a normal rea-agent image: make verify ENGINE=ghidra runs the
same harness against it (the resolver in scripts/image-name.sh picks the pulled
name), and docker compose exec rea-ghidra rea doctor --json works immediately.
Notes:
latestis per image, not per repo. There is no "one latest": each engine image tracks its own newest build, and the version tag (6.8.0-linux-amd64) is what you should pin in anything reproducible.- Air-gapped transfer.
docker save ghcr.io/alexmi256/rea-agent-ghidra:6.8.0-linux-amd64 | gzip > rea-ghidra.tgzthendocker load < rea-ghidra.tgzon the other side. - The images are ~8 GB each and most of that is the auxiliary toolchain, not the engine — see the size table below for the per-engine deltas.
- Rebuilds are unnecessary but supported: the published names are exactly what
make build-ghidratags, so building from source replaces the pulled image rather than creating a second one.
Publishing your own fork
The workflow publishes ghcr.io/<repo-owner>/... automatically — on a fork, the
owner is your account, so the images appear under your namespace and the compose
defaults (which point at alexmi256) will no longer match. To publish under your
own account:
- Update the four
REA_*_IMAGEnames in.env.example, and the fallbacks indocker-compose.yml/compose.http.yml, to your ownghcr.io/<you>/...names (the CIvalidatejob fails when these disagree withscripts/image-name.sh's resolution and thedocker-compose.ymldefaults - it only checks that the docs still mentionghcr.io, so renaming the docs to match is on you). - The
buildjob declarespermissions: packages: write, and that alone is enough even when the repository's default workflow permission is only read — verified on this repo, whose default isreadand whose push succeeded, because an explicitpermissions:block overrides the default. You only need Settings → Actions → General → Workflow permissions if you remove that block and then seedenied: permission_denied: write_package. - Keep the
org.opencontainers.image.sourcelabel pointing at your repo (it is built fromgithub.server_url/github.repositoryin CI). It is what links the package to the repository, and what makes the package inherit the repository's visibility — drop it and your images stay private and unlinked in the UI. - Push to
main; the packages appear under your profile → Packages. To make them public (if your repo is public and they did not inherit it): package → Settings → Change visibility → Public.
Building
make build # the all-engines image (= scripts/build.sh --engine all)
make build-ghidra # Ghidra only
make build-hopper # Hopper only
make build-ida # engine-less, bring your own IDA
make build-all # all four, in order
scripts/build.sh --engine ghidra --verify-quick # build + smoke-verify in one go
scripts/build.sh --engine ghidra --no-cache # force a full rebuild
Build them in the order make build-all uses (all, ghidra, hopper, ida)
and only the engine layers are compiled each time: every other layer is identical
across the four images, so Docker serves it from cache. Building ghidra
directly on a cold cache works fine, it is just slower for the second and third
engine afterwards.
Running
One service per engine in docker-compose.yml. Name the service explicitly - a
bare --profile ghidra up -d would also start the default rea-agent service,
because a service without a profile always starts:
docker compose up -d # rea-agent (Ghidra + Hopper)
docker compose --profile ghidra up -d rea-ghidra # Ghidra only
docker compose --profile hopper up -d rea-hopper # Hopper only
docker compose --profile ida up -d rea-ida # IDA, bring your own
make up ENGINE=hopper # the same thing, with the service name handled
make up-ghidra make up-hopper make up-ida
Each service has its own container name (rea-agent, rea-agent-ghidra,
rea-agent-hopper, rea-agent-ida) and its own /home/rea volume
(rea-agent-home, ...-ghidra, ...-hopper, ...-ida). They all share
./work, ./targets and ./config, so evidence and targets are in one place
whichever engine produced them - but only run one engine container at a time
against the same workspace, or two analyses will race over the same files.
Talking to the container's MCP surface
# stdio - what Hermes/Claude Code/Codex spawn (container name, not service name)
docker exec -i rea-agent-ghidra rea mcp
# Streamable HTTP bridge on 127.0.0.1:8811
REA_IMAGE=rea-agent-ghidra:6.8.0-linux-amd64 REA_ENGINE=ghidra \
docker compose -f docker-compose.yml -f compose.http.yml --profile ghidra up -d rea-mcp-http
make http-up ENGINE=hopper # same thing, for the image you built
For a Hermes MCP entry, only the container name changes between engines:
mcp_servers:
rea:
command: docker
args: [exec, -i, rea-agent-ghidra, rea, mcp] # rea-agent-hopper / rea-agent-ida
timeout: 1800
connect_timeout: 120
See HERMES-MCP.md for the full block and the reasoning behind those timeouts, and ida-variants.md for IDA licensing.
Sizing and slimming
Every image is large on purpose (the brief was "a heavy base with a pile of tools built in"), and the auxiliary toolchain dominates the size - the engine is a minority of it. Measured on this project's build host:
| Image | Size (docker image inspect .Size) |
|---|---|
rea-agent (Ghidra + Hopper) |
8.64 GiB — 9,281,121,905 bytes |
rea-agent-ghidra |
8.52 GiB — 9,153,212,432 bytes |
rea-agent-hopper |
7.80 GiB — 8,375,230,062 bytes |
rea-agent-ida |
7.68 GiB — 8,247,320,521 bytes |
Measured with make sizes on this build host (amd64); the byte counts move by a
few tens of KB between rebuilds even with identical inputs (layer timestamps), so
treat the GiB figures as the stable number. Read the deltas rather than the
totals: Ghidra costs ~906 MB and Hopper ~128 MB on top of the engine-less
ida image (Hopper's Qt6/X11 runtime is already in the shared base layers, which
the browser tooling needs anyway; the ~36 MB the Hopper delta grew over 6.5.0 is
the retained signed package evidence REA 6.6.0+ needs, which duplicates the
installed .deb inside the image). The auxiliary toolchain is the rest, so the
engine choice is a smaller lever on size than you might expect — what you really
buy with a single-engine image is a smaller surface (no second engine's
libraries, licence files or runtime in the container), a quicker build, and a
rea providers listing that can only say one thing. If size is the goal, switch
components off instead:
scripts/build.sh --engine ghidra \
--arg INSTALL_CHROME=0 --arg INSTALL_PLAYWRIGHT=0 \
--arg INSTALL_WINE=0 --arg INSTALL_CROSS_TOOLCHAINS=0 --arg INSTALL_PWNDBG=0
INSTALL_CHROME, INSTALL_PLAYWRIGHT, INSTALL_PWNDBG, INSTALL_WINE,
INSTALL_CROSS_TOOLCHAINS and INSTALL_IDA are all 1/0 (IDA also accepts
auto: install vendor/ida/*.run if present, never download). Turning one off
only removes that tool's own checks from the harness - REA will report the
corresponding family as unavailable, which is honest and expected.
When one engine is not enough
- The
allimage exists for exactly that, and itsrea providers --jsonlists both engines; pass--provider ghidra/--provider hopperper command, or setREA_ANALYSIS_PROVIDER(auto-selection refuses an ambiguous choice and names the candidates rather than guessing). - To compare engines without rebuilding, run two containers with explicit names
and separate home volumes, e.g.
docker run --name rea-g -v ./work:/work … rea-agent-ghidra:6.8.0-linux-amd64 sleep infinityalongside the Hopper one. - Hopper's architecture coverage is narrower than Ghidra's (Ghidra ships MIPS, RISC-V, PowerPC, SuperH, 6502, Z80, DOS MZ, and more in the same image). If architecture breadth is the reason you are shopping, the Ghidra image is the one you want.