▄▄▄▄▄▄▄▄▄▄▄▄▄
▄█▀ ▀█▄
▄█▀ ▀█▄
█▀ ● ● ▀█
██ ██
█▀▀▀▀▀▀▀▀▀▀▀█████████████████████████████▀▀▀▀▀▀▀▀▀▀▀█
█ █▀▀▀▀▀▀▀████ █▀▀▀▀▀▀█ █▀▀▀▀▀▀█ ████▀▀▀▀▀▀▀█ █
█ █ ████ █ █ █ █ ████ █ █
█ █ ◉◉◉ ████ █ JDI █ █ JVMTI█ ████ ◉◉◉ █ █
█ █ ████ █ █ █ █ ████ █ █
█ █▄▄▄▄▄▄▄████ █▄▄▄▄▄▄█ █▄▄▄▄▄▄█ ████▄▄▄▄▄▄▄█ █
█▄▄▄▄▄▄▄▄▄▄▄█████████████████████████████▄▄▄▄▄▄▄▄▄▄▄█
██ ██
██ ANDROID-DEBUGGER ██
██▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄██
▀▀▀█▀▀▀ ▀▀▀█▀▀▀
█ █
█ █
▄▄▄█▄▄▄ ▄▄▄█▄▄▄
Give Claude a real Android debugger so it can investigate bugs instead of guessing at them.
Your AI agent hits a bug. It reads the stack trace. It reads the source. It guesses. It adds Log.d. It rebuilds. It runs the app. It reads more logs. It guesses again. Half its "fixes" are made-up field names. The other half pass tests that don't actually exercise the failure path.
That's not debugging. That's the agent doing improv from memory of your codebase.
Android Debugger attaches Claude to your running Android app the same way Android Studio does — JDI over JDWP, plus a native JVMTI agent for the things JDWP can't do. Claude sets real breakpoints. It reads real variables off a real paused VM. It evaluates expressions inside a grammar that literally cannot mutate state. It patches method bodies live via HotSwap. It walks the heap in native code, not by re-reading the source.
You ask. Claude attaches. The VM answers. The agent reports.
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ IDEA │──►│ ATTACH │──►│ OBSERVE │──►│ PATCH │──►│ REPORT │
│ │ │ │ │ │ │ (live) │ │ │
│ you │ │ auto │ │ auto │ │ auto │ │ you │
│ ask │ │ │ │ │ │ │ │ approve │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
:investigate :attach :explain :patch (verdict)
Breakpoints, conditional breaks, logpoint sweeps, frame snapshots, FEEL expression eval, method invocation, HotSwap, heap walks, allocation tracing, session persistence — all driven by /android-debugger:* skills. No IDE. No browser tabs. No rebuilds for tracing. Just Claude and a live VM.
|
|
Every "AI debugger" today is a chat window staring at log output. Real debugging means pausing a VM, reading its state, forming a hypothesis, and proving it. That requires JDI, JVMTI, and a tool surface tuned for an agent's reading level — not a human's.
Android Debugger closes that gap with three structural commitments:
- Anti-hallucination by grammar. The
evaluatetool runs DMN-FEEL expressions over paused-frame state. FEEL has no method-call syntax, so it cannot mutate. Mutation goes through a separate, flagged tool that refuses likely-mutators unless opted in. The agent cannot accidentally change the app while inspecting it. - Snapshot-first, drill-down second. On every pause, the server eagerly bundles the top N frames + locals + watches + source ref into a
frame_snapshot, cached by(thread, vmStateVersion). The token-burning "re-read every frame after every step" anti-pattern is dead. - Explain before fix. The flagship workflow is
:explain— snapshot the pause, state a one-paragraph hypothesis, then decide whether to step, trace, or patch. Published research (AutoSD-style scientific debugging) puts this loop at ~71% Pass@1 on real failures.
The agent stops guessing. It starts proving.
Distributed through the Acendas marketplace. Two steps: add the marketplace, then install the plugin.
From inside a Claude Code session:
/plugin marketplace add Acendas/acendas-marketplace
/plugin install android-debugger@acendasOr from the CLI:
claude plugin marketplace add Acendas/acendas-marketplace
claude plugin install android-debugger@acendasThe marketplace pins to a released version tag — every install is deterministic. To pick up a new release, run /plugin marketplace update then /plugin install android-debugger@acendas again.
/android-debugger:ad-setup # one-time env probe — JDK + adb + ART capabilities
/android-debugger:ad-attach # pick a debuggable PID and connect
/android-debugger:ad-explain # snapshot + 1-paragraph hypothesis after a pause
/android-debugger:ad-catch <Exc> # break on uncaught exception, root-cause on hit
/android-debugger:ad-trace <sym> # logpoint sweep across suspect call graph
/android-debugger:ad-patch <goal> # HotSwap loop with verify_via
/android-debugger:ad-detach # clean shutdown
For "I'm stuck, please debug this for me," dispatch the orchestrator:
Agent({
subagent_type: "android-debug-orchestrator",
prompt: "Why does login crash on slow networks? App is com.example.app."
})
- Attach to any debuggable app — no rebuild required. JDI over
adb forward jdwp:. - One-shot pause snapshot — frame + locals + watches + source ref in a single cached payload.
- FEEL expression evaluator — pure-expression language; can't mutate state by grammar.
- Every breakpoint type — line, conditional, hit-count, logpoint, method, exception, field-watch, class-load.
- Native JVMTI agent — C++ helper loaded via
cmd activity attach-agent. Three ABIs ship pre-built. - HotSwap method bodies live — JVMTI
RedefineClasses+ d8 dexing. Pre-validated, revertible. - Heap walks at native speed — count instances, find referrers, walk chains to GC roots. 10–100× faster.
- Method + allocation tracing — filter, throttle, capture args + return values. Flight-recorder mode.
- Session persistence — breakpoints + watches saved on detach, rehydrated on reconnect.
- Workflow skills for an AI consumer —
:explain,:catch,:trace,:walk,:patch,:investigate. The agent picks the recipe.
Skills are short, composable recipes. Most debug sessions chain a handful.
/android-debugger:ad-attach
Lists debuggable PIDs, picks one (or accepts a serial: + package:), forwards a free local port to the app's JDWP port, attaches the JDI VirtualMachine, probes ART capabilities, optionally loads the JVMTI agent. Returns the capability map + any warnings (release-build detected, prior-session crash, etc.).
/android-debugger:ad-explain
The cheap, high-leverage default after every pause. Calls frame_snapshot, reads top N frames + locals + watches, writes a one-paragraph hypothesis. The agent decides what to do next from that hypothesis — not from guessing.
/android-debugger:ad-catch java.lang.NullPointerException
Sets an uncaught-exception breakpoint, resumes, reproduces, root-causes from the paused frame. Bread-and-butter workflow for "why does X crash."
/android-debugger:ad-trace "login fails sometimes"
Cursor-style logpoint sweep — drops 6–8 non-suspending logpoints across a suspect call graph, runs the failing path, harvests the timeline. Replaces Log.d archaeology entirely. No rebuild, no Gradle, no APK push.
/android-debugger:ad-walk MainActivity.onCreate
Sets an entry breakpoint, steps with a bounded step budget, narrates at frame boundaries. Onboarding workflow for "show me how login works."
/android-debugger:ad-patch "fix the off-by-one in the pagination"
Goal-driven HotSwap loop. Required verify_via: clause — the patch isn't done until the verify probe passes against the live VM. Pre-validates the shape diff (no field add/remove, no superclass change), dexes via embedded d8, sends to the JVMTI agent for RedefineClasses. Revertible.
/android-debugger:ad-detach
Disposes the JDI VM, releases the adb forward, drains JVMTI ref tables, persists breakpoints + watches to $CLAUDE_PLUGIN_DATA/android-debugger/sessions/<serial>_<package>.json for next attach.
| Skill | What it does | When to use |
|---|---|---|
:setup |
Env probe — JDK, adb, ART capabilities | After install or "isn't working" |
:attach |
Pick + connect to a debuggable PID | Starting any session |
:detach |
Clean shutdown, persist state | Ending a session |
:status |
Current attached app, breakpoints, watches | "Where are we?" |
:explain |
Snapshot + hypothesis after pause | Default after every pause |
:catch |
Uncaught-exception break + root-cause | "Why does X crash?" |
:trace |
Logpoint sweep, no rebuild | "Find where X happens" |
:walk |
Entry break + guided step-through | "Show me how X works" |
:bisect-flaky |
Loop instrumented test, narrow with conditional break | "Fails 1 in 10 runs" |
:patch |
Goal-driven HotSwap loop with verify_via: |
"Fix this and prove it" |
:patch-revert |
Roll back a HotSwap | Patch didn't work |
:patch-status |
Current HotSwap state | "What did we patch?" |
:investigate |
Top-level orchestrator — triage + dispatch | Catch-all "debug this for me" |
A Kotlin MCP server + a native JVMTI agent + a small set of workflow skills. No external runtime, no server process, no database.
┌────────────────────────┐ ┌────────────────────────┐
│ CLAUDE CODE SESSION │ │ ANDROID DEVICE │
│ │ │ │
│ /android-debugger:* │ │ ┌──────────────────┐ │
│ │ │ │ │ TARGET APP │ │
│ ▼ │ │ │ │ │
│ ┌──────────────────┐ │ │ │ ┌────────────┐ │ │
│ │ KOTLIN MCP SRV │◄─┼── adb fwd ──►│──┼──► JVMTI agent│ │ │
│ │ │ │ jdwp + UDS │ │ │ (C++) │ │ │
│ │ ┌────────────┐ │ │ │ │ └────────────┘ │ │
│ │ │ JDI client │◄─┼──┼── adb fwd ──►│──┼─► JDWP server │ │
│ │ └────────────┘ │ │ │ │ │ │
│ └──────────────────┘ │ │ └──────────────────┘ │
└────────────────────────┘ └────────────────────────┘
- JDI client — Oracle's Java Debug Interface over JDWP. Read surface (frames, locals, fields), narrow write surface (
setLocal,setField,invokeMethod), event stream. - JVMTI agent — small C++ binary loaded INTO the app process via
cmd activity attach-agent. Unlocks what JDWP can't do on ART:RedefineClassesfor HotSwap,IterateThroughHeapfor fast walks, line-rate method+allocation events. Three ABIs pre-built: arm64-v8a, x86_64, armeabi-v7a. - MCP tool surface — ~50 tools, snake_case
<area>_<verb>, all return{ ok, ... }or{ ok: false, code, message, hint }. The agent reasons over JSON, not stack traces.
Each /android-debugger:* command is a skill with imperative-form body. Skill bodies are written for the agent, not for human readers — that's the consumer model.
| Agent | Role |
|---|---|
| android-debug-orchestrator | Autonomous "debug this for me" loop — triages goal shape (crash / behavior / flaky / onboarding), dispatches to the matching skill, drives the iterative loop end-to-end, returns a structured findings report |
Persistent state (breakpoints + watches) lives at ${CLAUDE_PLUGIN_DATA}/android-debugger/sessions/<serial>_<package>.json. Atomic-write + 0600 on POSIX. The data dir is per-Claude-plugin-install, never inside your repo.
plugin-data/android-debugger/sessions/
└── <serial>_<package>.json Saved breakpoints + watches per (device, app)
The plugin assumes the AI will hallucinate and try to mutate things it shouldn't. Every safety net exists because we don't trust the model to police itself against a live VM.
- Grammar-level mutation refusal —
evaluateruns DMN-FEEL. FEEL has no method-call syntax. Side effects are impossible at the parser level. - Mutation-refusal regex on
eval_method— likely-mutator names (set*,add*,remove*,clear,put,delete, etc.) refuse unlessallow_mutation: trueis set explicitly. - Capability-aware errors — when the device's ART can't do something, the tool returns
code: capability_unavailablewith a hint, instead of pretending. The agent reacts on the code. evaluatesingle-flight — one invocation worker, 10s timeout, never invoked from the event-handler thread. ART's verifier is stricter than HotSpot; primitives are boxed viavm.mirrorOf. Deadlock onINVOKE_SINGLE_THREADEDis structurally prevented.- VmCoordinator mutex —
eval_methodandhot_swap_*can't run concurrently. Second caller getsvm_busyimmediately. No mid-swap evaluation surprises. - HotSwap shape-diff pre-validation — ASM-backed check refuses method-add/remove, field-add/remove, superclass / interface / access-flag changes. Structured
diffarray surfaces the specific violation. - HotSwap minify detection — single-letter-class-name heuristic at attach. Refuses upfront on R8-minified builds with
minified_build_unsupported. - Step budgets —
:walkandstep_until_method_changebound their step count. Defends against step-into loops, the top failure mode in the AI debugger landscape. - Snapshot cache invalidation — caches by
(thread, vmStateVersion). Any continue/step bumps the version. Stale snapshots can't be served. - Capability probe at attach — the full ART JVMTI capability map ships back in the
attachresponse so the agent knows up-front what's available on this specific device. - JVMTI agent crash diagnostics —
sigactionhandlers write/data/data/<pkg>/cache/amdb_agent_crash.txton SIGSEGV/SIGABRT/etc. Next attach picks it up and surfacescrashed_last_session. - Studio coexistence guard — probes
/proc/<pid>/mapsfor Android Studio's Apply-Changes agent (libperfa,studio_profiler). Refuses withagent_conflictrather than fighting Studio for control. - Strict protocol versioning — first message on the agent socket is
{method: hello, params: {protocol_version: 3}}. Mismatch returnsagent_version_mismatchwith both versions. No silent compat drift. - Clean detach —
vm.dispose()notvm.exit(). Shutdown hook on the JVM disposes + releases the adb forward on every exit path.
- Snapshot caching —
frame_snapshotkeyed by(thread, vmStateVersion). The single biggest token-saver. Re-reads cost zero. - Structured errors —
code+hintinstead of prose. The agent reacts oncode, doesn't re-read explanations. - Bundled tools —
frame_snapshotreturns frames + locals + watches + source in one call. The granular tools (get_locals,get_frames,inspect_object) are fallbacks for drill-down, not the default. exception_summary,framework_frame_filter,step_until_method_change— server-side bundles that collapse 5–10 round-trips into one. Less ceremony in the model's reasoning trace.- Polling, not streaming —
wait_for_event(timeout, types?)returns the next event or{ timed_out: true }. MCP doesn't stream; the polling shape is cheap and consistent.
The plugin runs anywhere Claude Code runs. Server is JVM/Kotlin — runs on macOS, Linux, Windows. Agent .so files are pre-built for the device-side ABIs. Skill bash blocks avoid POSIX-only builtins (no mktemp, no GNU realpath, no sed -i '') — anything beyond java -version and adb devices routes through MCP tool calls.
list_devices— enumerate connected devices/emulatorslist_debuggable_processes— discover debuggable PIDs with package labelsattach— connect to a PID, probe ART capabilities, optionally load the JVMTI agent, return the capability map + warningsdetach— clean shutdown, persist breakpoints + watches, release adb forwardsconnection_status— current attached app, session metadataagent_info— live JVMTI capability map + derived feature flags
add_line_breakpoint— line, conditional (FEEL), hit-count, logpoint (non-suspending tracepoint) — one tool, four modesadd_method_breakpoint— method entry / exit; auto-routes through JVMTI when used as a logpoint (line-rate)add_exception_breakpoint— caught / uncaught, per exception classadd_field_watchpoint— access / modification (when device supports)add_class_load_breakpoint— pause when a class matching a JDWP-glob pattern loadslist_breakpoints,remove_breakpoint,enable_breakpoint,disable_breakpoint- Deferred-breakpoint queue — set bps on not-yet-loaded classes (Kotlin lambdas, inline functions, inner classes)
resume,pausestep_over,step_into(with framework-frame skip filters),step_outrun_to_line— temp breakpoint + resumestep_until_method_change— server-side bundle, bounded step budget, returns when the frame's method changeswait_for_event— polled bridge over the JDI event queue (stopped,exception,class_prepare,exit)
frame_snapshot— bundled top-N frames + locals + watches + source ref, cached by(thread, vmStateVersion)list_threads,get_frames,get_locals— granular fallbacks for drill-downinspect_object— object fields, depth-limitedget_array_slice— sliced array accessexception_summary— bundled exception details (message, type, stack, framework-filtered)render_capabilities— human-readable capability mapframework_frame_filter— drop framework frames (android.*,kotlin.*,java.*) from a frame list
evaluate— DMN-FEEL expressions over paused-frame state. Binary ops,if/then/else,instance of, list comprehensions, ranges, three-valued null logic. Property access on pre-resolved object trees (depth 3 by default). String literals: single quotes.eval_method— explicit JDI method invocation. Mutation-refusal on likely-mutator names; opt in withallow_mutation: true. Single-flight, 10s timeout. Primitives boxed viavm.mirrorOffor ART's stricter verifier.add_watch,remove_watch,list_watches— re-evaluated on every pause, bundled intoframe_snapshot
hot_swap_class— single class. Server pre-validates shape diff (ASM), dexes JVM.classvia embedded d8, sends to agent for atomicRedefineClasses.hot_swap_classes— batch swap with per-entry validationhot_swap_revert— roll back using bytes cached byClassFileLoadHookforce_re_enter— capability-gatedPopFramefor paused-in-method swaps- Minify detection at attach — refuses upfront on R8 builds with
minified_build_unsupported - Shape-diff violations surface structured
diffarrays (method add/remove, field change, superclass / interface / access-flag change) - @Composable / coroutine state-machine warnings
android-debugger-classdiffcross-platform CLI — diffs build-dir hashes to identify changed classes for HotSwap
count_instances— auto-routes through JVMTI when agent is loaded; falls back to JDIfind_referrers— what's holding a reference to this object; works onvobj#(JVMTI) andobj#(JDI) refsiterate_heap_by_class— materialize up to N instances of a class asvobj#refs; reportstotalpastmaxfind_referrer_chain— BFS reverse-reference walk to GC roots, returnsroot_kind(jni_global,static_field,stack_local, etc.)- All gated single-flight via
VmCoordinator(30s timeout, blocks againsteval_method+ HotSwap)
start_method_trace/read_method_trace/stop_method_trace/list_method_traces— line-rate JVMTI MethodEntry/MethodExit. Filter modes:methods(exact list),class_pattern(literal prefix or wildcard),method_regex. Leaky-bucket throttling, per-session ring buffer, entry/exit symmetry under sampling, optional arg capture (realGetLocalVariableTablewalk), optional return-value capture withvoid+was_popped_by_exceptionflags.start_alloc_trace/read_alloc_trace/stop_alloc_trace/list_alloc_traces— class-allowlistedVMObjectAllocevents with optional stack capture (depth 0–10). No object retention (would survive GC).
dump_heap— shellsam dumpheap <pid>+adb pullfor full-heap HPROF analysis outside the live VM
tail_logcat— start a buffered tail with regex filter; returnsbuffer_idread_logcat— read entries from a buffer; merges logpoints withtag: "debugger:logpoint"stop_logcat— terminate the buffer'sadb logcatsubprocess
get_current_activity—dumpsys activity topdump_view_hierarchy—uiautomator dumpget_app_info— debuggable flag, target SDK, declared processes
- Breakpoints + watches save on
detachto$CLAUDE_PLUGIN_DATA/android-debugger/sessions/<serial>_<package>.json - Rehydrate on next
attachto the same(serial, package). Original IDs preserved soremove_breakpoint(id)keeps working. - Atomic-write + 0600 on POSIX. Filenames sanitized.
- C++ binary, three pre-built ABIs: arm64-v8a (~655 KB), x86_64 (~657 KB), armeabi-v7a (~401 KB) — fully stripped
- Loaded via
cmd activity attach-agent. Wire protocol: line-delimited JSON-RPC 2.0 over Unix abstract-namespace socket - Strict protocol-version handshake (v3); mismatch returns
agent_version_mismatch - Signal handlers write crash diagnostics to
/data/data/<pkg>/cache/amdb_agent_crash.txt; next attach surfacescrashed_last_session - Studio coexistence guard probes
/proc/<pid>/mapsfor Apply-Changes artifacts; refuses withagent_conflict - Eager capability acquisition at
Agent_OnAttach; deopt cost paid once - First-in-wins concurrency: second connect returns
agent_in_use - Auto-load on
attachby default; opt out withattach({ load_agent: false })
13 skills covering the canonical debug loops: :setup, :attach, :detach, :status, :explain, :catch, :trace, :walk, :bisect-flaky, :patch, :patch-revert, :patch-status, :investigate. See All Skills above.
android-debug-orchestrator agent — accepts a goal, triages the shape (crash / behavior / flaky / onboarding), dispatches to the matching skill, drives the iterative loop, returns a structured findings report. One prompt in, one report out.
- JDK 17 or newer. Android Studio bundles one — point
JAVA_HOMEat<AndroidStudio>/jbrif you don't have a system JDK. adbon PATH (orANDROID_HOME/ANDROID_SDK_ROOTset). Comes with Android SDK platform-tools.- A debuggable APK on a connected device or emulator. Debug variants expose JDWP via
adb jdwp. Release/R8-stripped builds attach withrelease_build_likelywarnings; locals will be mostly empty. - Android 8 (API 26) or newer for the target app. ART JVMTI was added in Oreo.
- Claude Code with
CLAUDE_PLUGIN_DATApropagation for v1.3+ persistence. Older versions still work — persistence silently disables.
cd server
./gradlew shadowJar # fat jar → dist/android-debugger-server.jar
./gradlew assembleAgent # native agent → dist/agents/<abi>/libamdb_agent.soThe fat jar and agent .so files are committed to the repo so users don't need to build. The agent has its own NDK/CMake path — see agent/README.md.
adb_status: not_found — set ADB_PATH, ANDROID_HOME, or add adb to PATH. macOS: brew install --cask android-platform-tools.
code: attach_failed — app isn't built debuggable, or Android Studio's debugger is already attached. Detach the IDE first.
code: already_attached — one VM per session. /android-debugger:ad-detach first.
code: agent_conflict (v1.4+) — Android Studio's Apply-Changes agent is in the process. Detach Studio.
code: agent_version_mismatch (v1.4+) — agent loaded with an older protocol than the server expects. Force-stop the app and re-attach.
code: minified_build_unsupported (v1.5+) — HotSwap refused — class names look R8-minified. Rebuild as a debug variant.
code: redefine_unsupported_shape_change (v1.5+) — HotSwap pre-validate caught a violation. data.diff shows the specific delta (method add/remove, field change, etc.). Restart the app to land structural changes.
evaluate says "Parse error in FEEL expression" — FEEL uses single quotes for strings ('hello'), single = for equality, and/or/not for boolean logic. Method calls route through the separate eval_method tool — FEEL has no obj.method() syntax.
App ANR-killed mid-debug — the system kills processes that block the main thread >5s. Set breakpoints off the UI thread, or use logpoints (non-suspending).
- ca.acendas:kfeel — Kotlin DMN 1.3 FEEL implementation, powers
evaluate. Bundled in the fat jar. com.android.tools:r8:8.7.18— d8 dexer for HotSwap. Bundled in the fat jar.- ASM 9.x — bytecode shape diff for HotSwap pre-validate. Bundled.
- JDI —
jdk.jdimodule, included with JDK 17+.
See CONTRIBUTING.md for development setup, end-to-end test loop against a public sample Android app, and conventions.
MIT — see LICENSE.