DEV Community

I Built a Fully Autonomous AI Reverse-Engineering Agent in Go

Architecture

┌─ agent loop ─┐
plan → ask scope → recon → locate → dynamic → report
│              │
│ LLM router   │ anthropic · openai-compat (openrouter/ollama/kimi/qwen) · claude-cli
│              │
│ 25+ tools    │ jadx · apktool · radare2 · ghidra · frida · adb · proxy
│              │ + secret_scan · url_extract · manifest · elf/pe_info · search_code
│              │
│ TUI          │ animated ghost header · code cards · model picker · NET panel
└──────────────┘

One wire format, many providers

The whole thing speaks the Anthropic Messages protocol internally. A router adapts it to each backend:

  • anthropic - native Messages API.
  • openai - an adapter that translates Messages ↔ OpenAI Chat Completions (tool calls included). This single adapter covers OpenRouter, Ollama, Kimi (Moonshot), and Qwen (Alibaba DashScope) - they're all OpenAI-compatible.
  • claude-cli - shells out to the claude binary for subscription use.

Switching model is live in the TUI (Ctrl+O):

func (r *Router) SetSelection(provider, model string) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    if _, ok := r.providers[provider]; !ok {
        return fmt.Errorf("unknown provider %q", provider)
    }
    r.active = config.Selection{Provider: provider, Model: model}
    return nil
}

Tools are just Go

A tool is an interface. The agent loop calls the model with the tool schemas, gets back tool_use blocks, dispatches them, and feeds results back - the classic loop, but every tool is native:

type Tool interface {
    Name() string
    Description() string
    Schema() map[string]any
    Run(ctx context.Context, input json.RawMessage, env *Env) (string, error)
}

The high-signal ones matter most. Instead of letting the model read files one by one (it will burn 60 steps doing that), I gave it recon tools:

  • manifest - package, permissions, exported components
  • url_extract - dedup every URL into a host/API map
  • secret_scan - AWS/GCP/Stripe/JWT/private-key/AES-literal regexes
  • search_code - regex grep across decompiled sources

The system prompt then enforces search before read. Efficiency went from "hit the step limit" to "done in ~10 steps".

The bugs that fought back

1. jadx NullPointerException on Java 21

First real APK, and jadx 1.5.1 face-planted:

java.lang.NullPointerException
    at jadx.plugins.tools.JadxPluginsTools.getEnabledPluginJars(...)

user.home was set. The plugin loader in 1.5.x just breaks on Java 21 / Windows.

Fix: pin jadx 1.4.7 (pre plugin-loader). Decompiled 13k classes on the first try. Sometimes the fix is a version number.

2. Kimi rejects "required": null

Switched a run to Kimi and every request 400'd:

tools.function.parameters is not a valid moonshot flavored json schema, details: <At path 'required': required must be an array>

My schema helper used a Go variadic. Called with no required fields it's nil, which marshals to JSON null. Anthropic and OpenAI tolerate it; Kimi/Moonshot don't.

One line, plus a regression test so it never comes back:

func schema(props map[string]any, required ...string) map[string]any {
    if required == nil {
        required = []string{} // never marshal to null - Kimi rejects it
    }
    return map[string]any{"type": "object", "properties": props, "required": required}
}

3. The TUI box kept shattering

adb output would tear the bordered viewport apart - stray border chars scattered mid-screen. I first blamed word-wrapping and hard-wrapped everything. Still broke.

The real culprit: Windows adb emits CRLF. After splitting on \n, the leftover \r yanks the cursor back to column 0 and the next characters overprint the border.

Sanitizing tool output fixed it instantly:

func sanitize(s string) string {
    s = strings.ReplaceAll(s, "\r\n", "\n")
    s = strings.ReplaceAll(s, "\r", "")
    return strings.Map(func(r rune) rune {
        if r == '\n' || r == '\t' {
            return r
        }
        if r < 0x20 || r == 0x7f {
            return -1 // drop control bytes
        }
        return r
    }, s)
}

Lesson: when a TUI "randomly" corrupts, suspect control characters before layout.

Making it feel alive

A few touches that turned a functional tool into one people actually want to run:

  • Animated ghost header - the mascot blinks, glances around, and a neon color shimmer travels across the wordmark. It's pinned; the transcript scrolls beneath it.
  • Code cards - when the agent reads a source file, the TUI shows a small syntax-highlighted (Chroma) preview. The model still gets the full file; your eyes get 12 tidy lines.
  • Ask menu - when the agent needs scope ("secrets? auth flow? may I run Frida on a device?"), a multi-select checkbox menu pops up (↑/↓, Space, Enter).
  • Esc to interrupt - stop the agent mid-run and steer it, Claude-Code style.
  • Live NET panel - start the built-in MITM proxy and captured request/response pairs stream into a side panel. The layout is responsive: wide terminal splits, narrow terminal hides the panel.

The native proxy (no Burp)

Dynamic network capture is a first-class tool, built on elazarl/goproxy - a real in-process MITM proxy with an exported CA:

proxy start → adb set http_proxy + install CA → frida_preset ssl_unpin → drive the app → proxy flows

The agent orchestrates all of that; you just watch the requests land.

Dynamic instrumentation, presets included

The model shouldn't have to hand-write Frida JS every time, so frida_preset ships battle-tested scripts:

  • ssl_unpin - universal Android pinning bypass (TrustManager + OkHttp + Conscrypt)
  • hook - hook every overload of Class!method, dump args + return
  • dump_class, list_classes, trace_http

Combined with the proxy, that's a full static → dynamic → network pipeline in one binary.

Try it

Grab a build from the releases page:

jurig doctor        # check providers + toolchain
jurig               # first run launches an interactive setup wizard

First run walks you through provider + model, drops you into the TUI, and auto-installs missing tools (jadx, apktool) on demand.

⚠️ For authorized security research and education only. Analyze apps you own or have explicit permission to test.

What's next

  • ACP backend so the Claude subscription path drives tools natively
  • Ghidra decompile scripting for deeper native analysis
  • Linux/macOS builds + CI

If you build agents, write Go, or do mobile RE - I'd love feedback. Star the repo and tell me what tool you'd add next.

GitHub: https://github.com/ReverserID/JURIG

Built with Go, Bubble Tea, Lipgloss, Glamour, Chroma, and goproxy.

Comments

No comments yet. Start the discussion.