skylot/jadx · error · JadxRuntimeException
Failed to save config file: {}
Error message
Failed to save config file: {} What it means
Thrown by SmaliDebugger.handShake when the 14-byte reply was read without exception but is null (short read) or does not equal the JDWP magic handshake bytes (JDWP.decodeHandShakePacket(buf) is false). The TCP peer answered but its answer is not the JDWP handshake string, meaning the connected endpoint is not a JDWP agent even though the socket itself is healthy.
Source
Thrown at jadx-cli/src/main/java/jadx/cli/config/JadxConfigAdapter.java:83
public @Nullable T load() {
if (!Files.isRegularFile(configPath)) {
// file not found
return null;
}
try (JsonReader reader = gson.newJsonReader(Files.newBufferedReader(configPath))) {
return gson.fromJson(reader, configCls);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to load config file: " + configPath, e);
}
}
public void save(T configObject) {
try {
String jsonStr = gson.toJson(configObject, configCls);
// don't use stream writer here because serialization errors will corrupt config
Files.writeString(configPath, jsonStr);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to save config file: " + configPath, e);
}
}
public String objectToJsonString(T configObject) {
return gson.toJson(configObject, configCls);
}
public T jsonStringToObject(String jsonStr) {
return gson.fromJson(jsonStr, configCls);
}
private Path resolveConfigRef(String configRef) {
if (configRef == null || configRef.isEmpty()) {
// use default config file
return JadxCommonFiles.getConfigDir().resolve(defaultConfigFileName);
}
if (configRef.contains("/") || configRef.contains("\\")) {
if (!configRef.toLowerCase().endsWith(".json")) {View on GitHub (pinned to e738a26571)
Solutions
- Point the ADB forward at the real jdwp process id (`jdwp:<pid>`) rather than an arbitrary port, then retry.
- Verify the target APK is debuggable and that you are attaching to its jdwp thread, not a different listener.
- Confirm no other debugger/proxy is intercepting the forwarded port.
- Check that readNBytes actually returned 14 bytes; if the peer closes early, the process is not a jdwp agent.
Example fix
// before
if (buf == null || !JDWP.decodeHandShakePacket(buf)) {
throw new SmaliDebuggerException("jdwp handshake bad reply");
}
// after (surface whether it was a short read vs wrong magic)
if (buf == null) {
throw new SmaliDebuggerException("jdwp handshake: peer closed before sending 14 bytes (not a jdwp agent?)");
}
if (!JDWP.decodeHandShakePacket(buf)) {
throw new SmaliDebuggerException("jdwp handshake: bad magic, first bytes=" + Arrays.toString(buf));
} Defensive patterns
Strategy: validation
Validate before calling
// Validate the endpoint speaks jdwp by checking the handshake magic before full attach:
try (Socket s = new Socket(host, port)) {
s.setSoTimeout(3000);
OutputStream out = s.getOutputStream();
out.write(JDWP.encodeHandShakePacket());
byte[] reply = IOUtils.readNBytes(s.getInputStream(), 14);
if (reply == null || !JDWP.decodeHandShakePacket(reply)) {
throw new IllegalStateException(host + ":" + port + " is not a jdwp agent (bad/short handshake)");
}
} Try / catch
try {
return SmaliDebugger.attach(host, port, listener);
} catch (SmaliDebuggerException e) {
if ("jdwp handshake bad reply".equals(e.getMessage())) {
// wrong forward target: re-point at the real jdwp pid and retry once
ensureJdwpForwardByPid(packageName, port);
return SmaliDebugger.attach(host, port, listener);
}
throw e;
} Prevention
- Forward to jdwp:<pid>, not to an arbitrary app port or local listener.
- Attach to a debuggable Android process only.
- Ensure no proxy/other client rewrites the forwarded stream.
- Confirm readNBytes returns the full 14 bytes; an early close means the peer is not jdwp.
When it happens
Trigger: After a clean write/read, buf is null or fails the magic check. Happens when the ADB forward points at a port serving something other than jdwp (e.g. an HTTP/proprietary service), when the first 14 bytes are garbage due to stream misalignment, or when readNBytes returns fewer than 14 bytes (null) because the peer closed early.
Common situations: Forwarded the wrong local port; the target is not a debuggable Android process; connected to the app's own listener instead of its jdwp thread; a man-in-the-middle/proxy rewriting the stream; partial data because the peer closed the socket after <14 bytes.
Related errors
- Failed to load config file: {}
- Found {} classes, single class output can't be used
- Input class can't be saved by current jadx settings (marked
- Class decompilation failed
- Unknown option: {}
AI-assisted analysis of skylot/jadx@e738a26571 (2026-08-14).
Data as JSON: /api/errors/bab4d34f331b6c0b.
Report an issue: GitHub.