Reverse engineering with a coding agent and headless Ghidra
A coding agent reads source, runs commands, and edits files. Static reverse engineering is a different job: the thing you are studying has no source, only a shipped binary or package, and the questions are about behaviour its authors never documented. This page describes giving an agent a decompiler it can drive as tools - a reverse-engineering MCP server backed by headless Ghidra - and the workflow that makes it answer questions like “which protocol messages does this client handle” and “does the public documentation miss anything”.
The framing is static analysis of software the analyst is entitled to study: interoperability, protocol documentation, and one’s own tooling. Every mechanism below is read from the harness source and the reverse-engineering skill as of 2026-10-20; versions and checksums are copied from the installer, not recalled. The worked example is read from the author’s Soulseek parity research notes of 2026-10-19.
TL;DR:
- Static reverse engineering is a question about a shipped artifact; a decompiler answers it. Wire the decompiler up as agent-callable tools rather than asking the model to explain a binary from a strings dump.
- The server exposes ~170 tools. They are
deferred, found via tool search when a task actually needs them, instead of being declared on every turn - the same reason a worker’s first request cost about 77.5k prompt tokens carrying 139 tool schemas (see Which model runs which agent work). - Ghidra is installed by a pinned, checksum-verified script: version 12.1.4 with a sha256 that is checked before the zip is unzipped, on top of a JDK 21 requirement. Re-running is idempotent.
- The build instructions ride along as a vendored skill, copied verbatim from the server’s package and bumped at the same time as the server version, so the guidance never describes capabilities the running server lacks.
- Headless analysis is the agent’s workflow:
analyzeHeadlessneeds no GUI, so an agent can bind a binary, pull pseudocode, symbols and cross-references, and summarise. - The worked example built a feature-parity matrix by static-reversing the official Soulseek client’s binary against two open-source clients, and found a server message code the public protocol documentation omits.
- A worker that must stop a helper process it started kills it by a recorded PID. A pattern kill (
pkill -f) matches the worker’s own command line and kills the worker.
Topology
Section titled “Topology”The same thing as text:
- The agent starts with no reverse-engineering tools loaded. When a task needs them, it finds them through tool search and reaches the MCP server.
- The MCP server wraps the REA package (
rea-agents@6.4.0) and exposes roughly 170 tools,deferredso they are not declared on every turn. - The server locates Ghidra through
GHIDRA_INSTALL_DIRand the JDK throughJAVA_HOME. - Ghidra 12.1.4 was installed by a script that pins a version and checks a sha256 before unzipping, and requires a JDK 21+ on PATH.
- A vendored skill carries the routing recipes; its server version is bumped at the same time as the MCP pin so the two stay in step.
- The agent drives Ghidra headlessly against the target artifact - static analysis only, the binary is never executed.
Which tool for which job
Section titled “Which tool for which job”| You have | Reach for | Why |
|---|---|---|
| A question about a shipped binary or package (what protocol codes it handles, what a function does), for software you may study | The RE MCP server over headless Ghidra | Pseudocode, assembly and cross-references are the raw evidence, and the agent can query them as tools |
| An ordinary source repository, asking how the code behaves | Normal repo tools: read, grep, LSP | Source is authoritative and cheaper; the skill’s own routing says to skip RE for complete source |
| A rarely-used tool capability that only some sessions need | exposure: deferred, found via tool search | Declaring ~170 schemas is a per-turn prompt cost paid by every request, not just the ones that use them |
| A worker that must stop a helper process it started (a headless analysis run gone long) | Record the PID to a file and kill by PID | pkill -f matches the worker’s own command line and kills the worker |
The problem static RE answers
Section titled “The problem static RE answers”Reverse engineering is the fallback when the artifact is the only source of truth. In the worked example the question was “what can the official Soulseek client do, and which protocol messages does each feature use” - none of which the vendor publishes. An agent without a decompiler can only read strings and guess; one with a decompiler can name the symbol a message code reaches and read pseudocode for what that code does.
The line this page draws is the same one the skill draws: static analysis of software the analyst is entitled to study. Never execute the target binary. No license-circumvention work, no copied or pirated commercial software. The worked example holds to this - the official client’s Linux build is analysed as a shipped artifact (it happens to ship unstripped, with debug symbols), and the binary is never run.
Why deferred: ~170 tools is a prompt cost
Section titled “Why deferred: ~170 tools is a prompt cost”The REA server exposes roughly 170 tools1. Every declared tool adds its name, description and parameter schema to each outgoing request’s prompt. A worker’s first request used to carry about 77.5k prompt tokens once 139 tool schemas were loaded across every MCP server, before any work happened - and a local single-lane engine re-prefilled all of that on every cache miss (measured in Which model runs which agent work).
Reverse engineering is a capability most turns do not need, so declaring it on every turn would tax every conversation to serve the few that do. The fix is "exposure": "deferred": the server’s tools stay out of the per-turn tool list, and the model fetches them through tool search when a task actually calls for them. The deferred entry still carries a description so the search can match it (“native binaries via Ghidra (pseudocode, asm, xrefs), JS/Electron, .NET, APKs, SQLite, websites”).
Installing Ghidra: pinned and checksum-verified
Section titled “Installing Ghidra: pinned and checksum-verified”A decompiler the agent will rely on should be a known version, installed reproducibly. The installer script pins both:
- Ghidra 12.1.42, matching the 12.1.x line the server’s provider accepts. The zip is
ghidra_12.1.4_PUBLIC_20260921.zip, with sha256ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db, checked before unzipping. - A JDK 21+ on PATH, enforced up front (
command -v javac).
The script downloads to a pinned directory, verifies the checksum, unzips into an install tree, and symlinks a stable path so the server’s GHIDRA_INSTALL_DIR points at whichever version is current. Re-running skips what is already in place, so the install is idempotent. The same script pins a second tool on the same pattern - a Lua 5.x bytecode decompiler, checksum-pinned the same way - which matters when the target bundles Lua plugins rather than native code.
The vendored skill, kept in step with the server
Section titled “The vendored skill, kept in step with the server”The server’s package ships a skill - a set of routing instructions telling the agent which tool to open for which target (native ELF vs APK vs managed assembly vs archive, and so on). The skill is vendored verbatim into the harness’s skill tree, with a provenance file recording where it came from and how to update it.
The step that keeps them from drifting: the skill and the server pin carry the same version, and bumping one means bumping both, then re-copying the skill. The provenance file says so explicitly - “bump the version in mcp.json, then re-run the copy”. The rationale is exactly the deferred-tools rationale from the other direction: a skill edited out of band can describe capabilities the running server lacks, and a stale skill quietly points the agent at tools that changed. Versioning them together makes the guidance describe the server that is actually connected.
Three things the skill’s routing makes explicit, worth carrying into any agent RE setup:
- Connect only when needed. Installing the skill supplies instructions; it does not register the server, install the engines, or add tools to a running session. Those are separate steps with separate failure modes.
- Route the target first. The first tool depends on the artifact:
open_binaryfor a native binary or archive, a JS/Electron path for an extracted app, a managed-assembly path for a .NET artifact. Target-free tools take their own explicit path. - Summarise before drilling. Big artifacts get a summary view first, then a focused follow-up on the specific item that answers the question, rather than a full dump.
The headless workflow
Section titled “The headless workflow”A GUI disassembler is a human’s tool; an agent’s tool is one it can invoke by command and read back as text. Headless Ghidra (analyzeHeadless) runs the analysis and dumps results with no window, so the whole loop fits inside the agent’s normal tools: bind the binary, ask for an overview, then pull pseudocode, symbols, strings and cross-references for the function a message code reaches.
For the worked example, the flow was:
- Bind the official client’s build through
open_binary, then take thebinary_overviewmetadata and symbol inventory. - Search symbols for the names the questions point at - in this case, the dispatch table entries each protocol message code reaches.
- Decompile the reached function to read what it does, and cross-reference callers to see who relies on it.
- Distil each finding into a row of a coverage matrix, with the evidence (symbol, code, offset) attached.
The one rule that stays constant: the binary is analysed, never executed. Every conclusion is marked as observation, inference, or unknown - static analysis establishes that a symbol exists and what its decompiled body does, not that a code path runs at a particular time.
Worked example: a Soulseek feature-parity matrix
Section titled “Worked example: a Soulseek feature-parity matrix”The author builds a Soulseek client, and wanted it to reach feature parity with the official desktop client rather than staying an automation-only tool. The reference is the official client’s shipped Linux build3 - a closed binary, analysed statically. Two open-source clients (Nicotine+4 and slskd5) were inventoried from source for the same feature list, to see what a complete client does with each protocol message.
Because the official build ships unstripped, the symbol table carries 10,894 symbols - names, not just addresses - which turns “what does code 128 do” into a direct symbol lookup. The result is a parity matrix with one column per client (the official reference, the two open-source clients, and the author’s own) and one row per feature, marked Y / partial / missing, with the protocol codes each feature still needs.
Two findings came from the binary alone, absent from the public protocol documentation6:
- Server code 128, Distributed Delivery Report. The public protocol document numbers the server messages it covers and skips 128 entirely. The binary has a
Server::DNetDeliveryReport(int)symbol that the dispatch table routes code 128 to, decoding a single 32-bit value. The reference client only logs it, so the author’s client can ignore it. - An obfuscated peer connection. Decompilation of the connect path recovered how peers advertise a second real port and XOR the traffic on it with a 4-byte key rotated per block. The open-source clients do not implement it. Recovered from decompilation but not tested against a live peer, so the implementation is gated on a live handshake test.
The matrix is the deliverable, because it is what an implementation can consume: each “codes / notes” cell names the exact server or peer message an incomplete feature needs next. The worked example also demonstrates the skill’s boundary discipline - the binary was downloaded for a pinned checksum and only strings-scanned or symbol-read, never executed.
The operational lesson: never kill by a pattern your own command line matches
Section titled “The operational lesson: never kill by a pattern your own command line matches”A worker running the headless analysis needed to stop an analyzeHeadless run it had started. It ran pkill -f analyzeHeadless. The worker’s own process command line contains its prompt - and the prompt named the Ghidra tool - so pkill -f matched the worker itself and killed it, exit 143 (SIGTERM). The work was lost.
The fix is to treat the process ID as an argument, not a search string: record the PID to a file when the helper starts (... & echo $! > pidfile), and kill by that PID. Pattern kills (pkill -f, killall) are safe only when the pattern cannot appear in the killer’s own prompt - which is almost never, since an agent’s prompt usually names the tool it is about to run.
This generalises: any background process a worker starts and later stops should be stopped by its recorded PID, because the worker’s prompt travels in its own argv and becomes part of what a pattern match scans.
What carries over to another harness
Section titled “What carries over to another harness”The server name, the 170-tool count, the Ghidra 12.1.4 pin and the Soulseek specifics belong to this setup. These properties do not:
- Give the agent the decompiler, not a strings dump. A decompiler as tools turns “what does this binary do” into a query the agent can run and read back. Prose about the binary is a poor substitute.
- Pin and check the toolchain. A reproducible install means a pinned version plus a checksum verified before use, and an idempotent script. The decompiler’s behaviour moves between versions; a recorded version is what lets the findings be re-checked.
- Defer rarely-used tool surfaces. A capability a minority of turns need should not tax every request’s prompt. Gate it behind search with a description the search can match.
- Keep the instructions in step with the server. Vendor the guidance alongside the server and bump both together, so the guidance never describes a server that is not connected.
- Prefer headless for agent work. A decompiler the agent can invoke by command and read back as text beats a GUI it cannot operate. Headless Ghidra’s
analyzeHeadlessis the difference. - Distil to a matrix. RE findings become actionable when they land as a coverage matrix with per-row evidence, not narrative. A “codes / notes” cell names the next concrete step.
- Stop started processes by PID. A background helper is killed by its recorded PID, never by a pattern that could match the killing agent’s own command line.
Decision guide
Section titled “Decision guide”The same decision as a list:
- Source is available: use normal repository tools (read, grep, LSP). Reverse engineering is the heavier fallback, reserved for when the artifact is the only source of truth.
- No source, and you are not entitled to study the artifact: stop.
- No source, and you may study it: load the RE tools through tool search and analyse the artifact statically on headless Ghidra installed from a pinned version plus a verified checksum. Overview first, then focused decompilation; the target is never executed.
- A worker that starts a helper process records its PID and kills by PID. A
pkill -fpattern matches the worker’s own argv and kills the worker instead. - Findings end as a coverage matrix with per-row evidence.
Evidence
Section titled “Evidence”| Claim | How checked | Status |
|---|---|---|
The REA server runs bunx -y rea-agents@6.4.0 --mcp and is deferred | Read from the MCP config’s rea entry (command, args, exposure) | Read from source |
| The server exposes ~170 tools | tool_count: 170 in the skill frontmatter, “170 tools” in the harness AGENTS.md rea entry | Read from source |
Ghidra is pinned to 12.1.4, zip ghidra_12.1.4_PUBLIC_20260921.zip, sha256 ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db, JDK 21+ required | Read from the installer script’s GHIDRA_VER, GHIDRA_ZIP, GHIDRA_SHA256 and the javac check | Read from source |
The skill is vendored verbatim from rea-agents@6.4.0 (morluto/rea, MIT) and bumped in step with the MCP pin | Read from the skill’s VENDORED.md provenance and update procedure | Read from source |
| The official Soulseek build ships unstripped with 10,894 symbols | Read from the parity research notes (sources table, SoulseekQt row) | Read from source |
| Server code 128 (Distributed Delivery Report) is absent from the public protocol documentation | Server::DNetDeliveryReport(int) symbol confirmed by nm at offset 0x2aa4a0; code 128 absent from the public SLSKPROTOCOL.md (checked against the Nicotine+ copy, which documents 120-127 and 129 onward) | Measured, and re-checked against the public doc 2026-10-20 |
| The obfuscated peer connection (second port, XOR 4-byte key rotated per block) is recovered from decompilation but untested against a live peer | Read from the parity research notes (open questions section) | Read from source; live test not yet run |
A pkill -f analyzeHeadless worker killed itself, exit 143 | Session history, 2026-10-19 incident | Recorded |
Related docs
Section titled “Related docs”- Which model runs which agent work: model routing in a coding-agent harness - the deferred-tools rationale is the same prompt-cost argument measured for the local engine.
- Subagents, worktrees, and parallel agent work - the worker that hit the self-kill is one of the worker kinds described there.
- GPU voltage-frequency curve control without Afterburner - reverse engineering of the driver’s NVAPI curve interfaces, from community findings rather than headless Ghidra.
- The pi harness journey - the hub that owns the MCP server registration and the deferred-exposure mechanism this page builds on.
References
Section titled “References”-
morluto, “REA (Reverse Engineering Agent),” MIT. https://github.com/morluto/rea ↩
-
National Security Agency, “Ghidra,” software reverse engineering framework. https://github.com/NationalSecurityAgency/ghidra ↩
-
Soulseek, official site and desktop client downloads. https://www.slsknet.org ↩
-
Nicotine+ contributors, “Nicotine+,” the open-source Soulseek client. https://github.com/nicotine-plus/nicotine-plus ↩
-
slskd, “A modern client-server application for the Soulseek file sharing network.” https://github.com/slskd/slskd ↩
-
Nicotine+ contributors, “Soulseek Protocol Documentation,” Nicotine+. https://github.com/nicotine-plus/nicotine-plus/blob/master/doc/SLSKPROTOCOL.md ↩