Install Ubuntu's squashfs-tools so the stock unsquashfs/mksquashfs exist next to sasquatch. As the non-root container user, unsquashfs and sasquatch can't restore security.* xattrs and exit 2 although every file was extracted; wrap them to pass -no-xattrs unless an xattr option is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 KiB
CLAUDE.md
Guidance for Claude Code when working in this repository.
Purpose
This repo provides a reproducible, Docker-managed reverse-engineering workstation on Linux, built around Claude Desktop with two MCP integrations:
- Reverse Engineer Anything MCP: general-purpose RE tooling exposed to Claude over MCP.
- Ghidra + GhidraMCP: Ghidra with the GhidraMCP plugin, so Claude can query and annotate an open program (functions, decompilation, xrefs, renames, comments).
The goal is that docker compose up gives you a working setup where Claude Desktop can drive both toolsets against binaries in a shared workspace, without installing Ghidra, a JDK or RE tooling on the host.
Status
Early stage. What exists: a single image (Dockerfile, launch-apps.sh) with Claude Desktop, Ghidra (plus GhidraMCP and an ESP32 loader), binwalk, unblob, esptool and REA, started by run.sh (or the app-menu launcher install.sh creates: ~/.local/share/applications/claude-re-setup.desktop, icons taken from the image, Exec pointing at this checkout's run.sh; run.sh shows errors in a zenity dialog when it has no terminal) as two containers (Claude Desktop + Ghidra, and the REA MCP server); README.md covers usage; Claude Desktop's MCP config is claude-desktop/claude_desktop_config.json; Ghidra config files are in ghidra/. No compose setup yet. The layout below is the intended structure. Update this file when the real structure differs.
Intended layout
.
├── CLAUDE.md
├── README.md # user-facing setup and usage
├── docker-compose.yml # orchestrates all services
├── .env.example # versions, ports, UID/GID, display settings
├── claude-desktop/
│ ├── Dockerfile # Claude Desktop (Linux build) + GUI deps
│ └── claude_desktop_config.json # MCP server definitions (template)
├── ghidra/
│ ├── Dockerfile # JDK + Ghidra + GhidraMCP plugin
│ └── scripts/ # plugin install / headless helpers
├── re-mcp/
│ └── Dockerfile # Reverse Engineer Anything MCP server
├── scripts/ # host helpers (xhost setup, start/stop, health checks)
└── workspace/ # shared, git-ignored: binaries + Ghidra projects
Architecture
-
Claude Desktop and Ghidra share one container (the root
Dockerfile, started byrun.sh), at the user's request. Ghidra is the official GitHub release (version, build date and SHA-256 are global build args, downloaded once in theghidra-diststage and copied into the final image and into build stages that compile extensions against it), is pinned to 12.1.4 because that's what the GhidraMCP release is built for (Ghidra only loads extensions built for its exact version), runs on Ubuntu'sopenjdk-21-jdk, lives in/opt/ghidraand starts together with Claude Desktop. The Developer plugin package is enabled in both the project window and the CodeBrowser. A bare<PACKAGE NAME="Developer" />only adds the package's RELEASED plugins, so the STABLE ones are listed with<INCLUDE CLASS=...>. CodeBrowser: the build insertsghidra/codebrowser-developer.xmlintodefaultTools/CodeBrowser.toolinsidePublic_Release.jar. Project window: it has no default to patch, soghidra/FrontEndTool.xmlis copied into the user settings, along withghidra/preferences(USER_AGREEMENT=ACCEPT, because a pre-created settings dir otherwise makes Ghidra ask for its user agreement). To check a config really loaded, close Ghidra cleanly and see whether the plugins survive in the file it rewrites; Ghidra drops what it didn't load. -
Model provider window.
run.shasks on every start (zenity form; terminal prompts without zenity) for a base URL, token, auth header and optional model list. Empty URL: normal claude.ai sign-in. Otherwise it writes Claude Desktop's third-party managed config (inferenceProvider: "gateway",inferenceGatewayBaseUrl,inferenceGatewayApiKey,inferenceGatewayAuthScheme,inferenceModels,disableDeploymentModeChooser; docs: https://claude.com/docs/third-party/claude-desktop) and Chat, Code and Cowork use that Anthropic-Messages-compatible endpoint, with no claude.ai account. Verified against Claude Desktop 4.4212.0:- On Linux the file is
/etc/claude-desktop/managed-settings.json, and Claude Desktop ignores it unless root owns it and it isn't group/world-writable. A bind mount from the host shows the user's UID, sorun.shcreates the container, copies the file in withdocker cpfrom a tar stream (owner 0, mode 0644), then starts it; the host copy (runtime dir, 0700) is deleted right away and on failure. The image provides/etc/claude-desktop. - Claude Desktop rejects
http://base URLs except loopback. Forhttp://host[:port]the container getsCLAUDE_RE_GATEWAY_FORWARD=host:port,launch-apps.shrunssocaton127.0.0.1:18080to it, and the config points athttp://127.0.0.1:18080(the token then travels unencrypted, as with any plain-HTTP server).https://URLs are used as given. localhost/127.0.0.1in the URL is rewritten tohost.docker.internal(--add-host host.docker.internal:host-gateway); a trailing/or/v1is stripped because Claude Desktop appends/v1/messages. An empty token becomesnone(the key is required in gateway mode).- Check what Claude Desktop made of it in
~/.config/Claude/logs/main.log([managedConfig],[custom-3p]).
- On Linux the file is
-
Shared workspace volume. Every container mounts the workspace at
/workspace, so a file path means the same thing to Claude, Ghidra and the RE MCP.run.shasks for the host folder on each start in the desktop's folder picker (zenity, orkdialogon KDE; terminal prompt if neither exists), opening at the last folder used, else./workspace. Skip it with--workspace DIRorWORKSPACE=DIR. It refuses/and$HOME. The image symlinks~/workspaceto/workspacefor Claude Desktop's file picker. -
GUI forwarding. Claude Desktop and Ghidra are GUI apps. Default to X11 forwarding (
/tmp/.X11-unix+DISPLAY). Support Wayland through XWayland. Keep a noVNC/VNC fallback in mind for headless hosts. Never solve display access with a blanketxhost +. Scope it (xhost +local:dockeror+SI:localuser:...). -
GhidraMCP wiring. LaurieWired's GhidraMCP (https://github.com/LaurieWired/GhidraMCP), release 2.0, installed from the official release zip (SHA-256 checked). It has two parts:
- a Ghidra extension, unpacked into
/opt/ghidra/Ghidra/Extensions/GhidraMCP. ItsGhidraMCPPluginis a RELEASED plugin in the Developer package, so the CodeBrowser config loads it. While a CodeBrowser is open it serves an HTTP backend on127.0.0.1:8080(port: Edit → Tool Options → GhidraMCP HTTP Server). - the
ghidra-mcpPython MCP server (2.0 replacedbridge_mcp_ghidra.py), installed from the release wheel into the venv/opt/ghidra-mcp. Its dependencies are pinned to GhidraMCP'suv.lockat the release commit viascripts/uvlock-to-constraints.py. CLI:ghidra-mcp --ghidra-server http://127.0.0.1:8080/(stdio by default; also--transport streamable-http,--headless).
A program must be open in the CodeBrowser for the tools to return data. Claude Desktop only starts MCP servers after sign-in.
- a Ghidra extension, unpacked into
-
ESP32 Flash Loader (https://github.com/dynacylabs/ghidra-esp32-flash-loader): a Ghidra loader ("ESP32 Flash Image" in File → Import File) for ESP32 app images and flash dumps. It reads the partition table at
0x8000, maps segments to their addresses, adds peripherals from Espressif's SVD files and loads the chip's ROM ELF for symbols. A flash dump is only recognized when it starts with the bootloader (ESP32-S/C layouts). Original-ESP32 dumps (bootloader at0x1000) are rejected, and trimming the first0x1000bytes doesn't help: the loader then finds no partition table at0x8000, loads only the bootloader and throws a NullPointerException while still reporting success. Workaround: cut the app partition out of the dump (offsets from the partition table) and import it as an app image. Its tags ship no binaries and the older forks (tslater2006, Ebiroll, saibotk v1.1.0) don't compile against Ghidra 12, so theesp32-loader-buildstage builds dynacylabs' master at a pinned commit, with itsdata/svdandghidra_scriptssubmodules fetched over HTTPS at pinned commits and Gradle checked against gradle.org's SHA-256. It is unpacked into/opt/ghidra/Ghidra/Extensions/esp32; loaders need no tool config. -
REA ("Reverse Engineer Anything"): https://github.com/morluto/rea (MIT, TypeScript), the
rea-agentsnpm package, pinned to 6.8.0 with the tarball checked against the registry's integrity hash; its dependencies are locked by thenpm-shrinkwrap.jsonit ships. It needs Node.js ≥24.22.1 (official tarball, SHA-256 checked, in/opt/node) and procps'ps. It uses Ghidra 12.1.x headless throughGHIDRA_INSTALL_DIR(its own temporary projects, separate from the GUI's) binwalk 3.1.x throughREA_BINWALK_COMMAND(inspect_firmware_regions) and unblob 26.6.x throughREA_UNBLOB_COMMAND(extract_firmware); Hopper, IDA, Kuna, JADX, tshark, pwntools and a browser are not installed, and frida (optional, runtime instrumentation) stays unbuilt because npm blocks its install script. Check withrea doctor --provider ghidra --json. REA's bundled skill (reverse-engineer-anything) is copied from the package into~/.claude/skillsin the image, for Claude Code sessions in Claude Desktop's Code tab (they load user skills from there). Claude Desktop's chat doesn't read that folder: its skills come from the account (Settings → Capabilities → Skills) and are cached by the app. Some REA tools run the target (process capture, runtime/Electron observation), so it does not run in the Claude Desktop container.run.shstarts a second container from the same image (claude-re-mcp):rea mcp --http --host 0.0.0.0 --port 8765, only the workspace mounted, no display,--cap-drop ALL,no-new-privileges, and only on the--internalnetworkclaude-re-setup-internal(no route out) under the aliasrea. The Claude Desktop container is created on the default network plus that internal one, so it can sign in and reachhttp://rea:8765/mcp. REA's HTTP endpoint has no authentication and serves one client session at a time; the internal network is what limits who can reach it. A detacheddocker waitstarted byrun.shstops REA when Claude Desktop's container ends. -
binwalk, unblob and esptool are CLI tools in the same image (no MCP server of their own; REA wraps binwalk and unblob). The
Dockerfilehas abinwalk-buildstage adapted from binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk) to Ubuntu 24.04, with every source pinned: binwalk at a master commit (3.1.1; the last tag, v3.1.0, is from 2024), Rust toolchain,dumpifs/lzfse/dmg2imgcommits, and the Python extractors (jefferson,ubi-reader,uefi_firmware,vmlinux-to-elf, ...) in/opt/binwalk-py. esptool is in/opt/esptool. unblob (https://github.com/onekey-sec/unblob) 26.6.4 is installed from its PyPI wheel (SHA-256 checked) into/opt/unblob, with dependencies pinned to itsuv.lockat the release commit viascripts/uvlock-to-constraints.py;/usr/local/bin/unblobis a wrapper that puts that venv first onPATH, so unblob uses its own lockedjefferson/ubireaderrather than binwalk's. Its extractors follow upstream'sinstall-deps.sh: Ubuntu packages, sasquatch v4.5.1-6 (shared with binwalk) and onekey-sec's e2fsprogs fork (v1.47.0-3.ok2, replacing Ubuntu's 1.47.0), whosedebugfs rdumpoptions unblob's ext2/3/4 handler needs. The build fails unlessunblob --show-external-dependenciesfinds every extractor. unblob's Landlock sandbox works in the locked-down REA container (status=FullyEnforced). The venvs' entry points are symlinked into/usr/local/bin(not theirpython/pip). The final stage adds upstream's runtime extractors from apt, sasquatch from onekey-sec's.deb, Ubuntu'ssquashfs-tools(plainunsquashfs/mksquashfs, the commands Claude tries beforesasquatch).unsquashfs,sasquatchandsasquatch-v4bein/usr/local/binare wrappers (scripts/squashfs-no-xattrs.sh) that add-no-xattrsunless an xattr option is given: as non-root they can't restoresecurity.*xattrs and exit 2 although every file was extracted, which looked like a failed extraction, and Kaleido forbinwalk -E -p(PNG entropy graphs), found viaKALEIDO_PATH. Upstream publishes no checksums for sasquatch, onekey's e2fsprogs or Kaleido; their SHA-256s were recorded when pinned, amd64 only.BINWALK_RM_EXTRACTION_SYMLINK=1avoids dangling symlinks to container paths in extraction output. esptool needs the serial device passed into the container (--device) to talk to hardware; that isn't set up. -
MCP transport. Claude Desktop starts local MCP servers as stdio subprocesses. GhidraMCP runs in the same container, so
ghidra-mcpis a plain stdio entry inclaude_desktop_config.jsonthat reaches Ghidra on127.0.0.1. Servers in other containers (so far REA) serve streamable HTTP and are bridged into the stdio config withmcp-remote(--allow-http --transport http-only), installed in/opt/mcp-bridgefrom the committedclaude-desktop/mcp-bridge/package-lock.jsonand started with/opt/node/bin/nodeby absolute path. Use the same pattern for future servers. Avoid mounting the Docker socket into the Claude Desktop container just todocker execMCP servers.
Conventions
- Pin versions. Ghidra version, GhidraMCP release, JDK version, Claude Desktop build and RE MCP commit go in
.env/ build args, notlatest. The GhidraMCP release must match the Ghidra version it was built for. - Run as non-root inside containers. Map the host UID/GID (via
.env) so files inworkspace/stay owned by the user. - Persist state in named volumes or
workspace/: Ghidra user settings and projects. Rebuilding an image must not lose analysis. - Claude Desktop sign-in is not persisted. No keyring is installed and
~/.config/Claudeis not mounted, so the user logs in on every start. Don't add a keyring or config volume back without being asked. The same goes for the model-provider token (below): it's asked for on every start and never stored on the host. - No secrets in the repo. Credentials, session data and
workspace/contents are git-ignored. Ship.env.example, never.env. - Untrusted binaries. Samples under analysis may be malicious. Never execute them on the host or in the Claude Desktop container. Containers that might run samples (dynamic analysis) should have no host mounts beyond
workspace/and should allow network isolation (network_mode: noneor an internal-only network). - Linux only. Target recent Docker Engine + Compose v2 (
docker compose, notdocker-compose). Make the setup work under both Docker and rootless Podman where practical. On SELinux hosts such as Fedora, bind mounts need:z/:Z.
Common commands
These are expected once the compose setup exists. Keep this list in sync with reality.
cp .env.example .env # then adjust UID/GID, versions, DISPLAY
xhost +local:docker # allow containers to use the X server
docker compose build
docker compose up -d
docker compose logs -f <service>
docker compose down
Things to verify, not assume
- Claude Desktop on Linux. Check whether an official Linux build is available at the time of work. If not, use a maintained community repackaging and document which one and why. Confirm the config path, usually
~/.config/Claude/claude_desktop_config.json. - REA. Get it only from morluto/rea, rea.tools or the
rea-agentsnpm package (copycat repos exist). Before bumping it, check its accepted Ghidra/binwalk/Node versions (docs/installation.md,docs/firmware-analysis.mdat the release tag) and updateREA_INTEGRITYfromhttps://registry.npmjs.org/rea-agents/<version>(dist.integrity). - GhidraMCP. Before bumping GhidraMCP or Ghidra, confirm the release's supported Ghidra version (
version=in the extension'sextension.properties; the build checks it), theghidra-mcpCLI flags and its default ports from the upstream README. The Developer plugin lists inghidra/were checked against 12.1.4 and 12.2; re-check them for other versions.
When making changes
- After changing a Dockerfile or compose file, run
docker compose configto validate it, then build the affected service. - When adding an MCP server, update
claude-desktop/claude_desktop_config.json,docker-compose.yml,README.mdand the Architecture section above together.