Remote JVMs#
--url connects to a JVM that has remote JMX turned on: on another host, in a container, or on
your machine. For a local JVM without remote JMX, use --pid.
The URL#
service:jmx:rmi:///jndi/rmi://<host>:<port>/jmxrmi
<host> and <port> are where the JVM's JMX agent listens.
Terminal
ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi read java.lang:type=Runtime VmName VmVersion | jq .
{
"schemaVersion": 1,
"ok": true,
"result": {
"mbean": "java.lang:type=Runtime",
"attributes": {
"VmName": "OpenJDK 64-Bit Server VM",
"VmVersion": "25.0.4.1.1+1-jvmci-25.4-b23"
}
},
"durationMs": 41
}
ajmx needs the whole URL. --url localhost:7199 fails with INVALID_ARGUMENT.
Each command opens its own connection. To send several requests over one connection, use
batch.
Turn on remote JMX#
Start the target JVM with these system properties.
Terminal
java -Dcom.sun.management.jmxremote.port=7199 \
-Dcom.sun.management.jmxremote.rmi.port=7199 \
-Dcom.sun.management.jmxremote.host=127.0.0.1 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=localhost \
-jar app.jar
Warning
These flags turn off authentication, so anyone who can reach the port can read and change the JVM. Use them on your own machine for development. Elsewhere, turn on credentials.
| Property | Value |
|---|---|
com.sun.management.jmxremote.port |
The port in the URL |
com.sun.management.jmxremote.rmi.port |
The same port. Without it, the JVM also listens on a random port, which a firewall or a container does not let through |
java.rmi.server.hostname |
The host name that ajmx uses to reach the JVM. The JVM hands it to ajmx, and ajmx connects to it for every call |
com.sun.management.jmxremote.host |
127.0.0.1 to accept connections from the same machine only. Leave it out to accept them from other hosts |
com.sun.management.jmxremote.authenticate |
true to require a username and password. See Credentials |
com.sun.management.jmxremote.ssl |
false. ajmx has no TLS settings, such as a trust store |
A JVM in a container#
Publish the JMX port on 127.0.0.1 only, and set java.rmi.server.hostname to localhost, the
name that ajmx uses to reach the published port. With Docker Compose:
services:
app:
# image, build, and the rest of the service
ports:
- "127.0.0.1:7199:7199"
environment:
JAVA_TOOL_OPTIONS: >-
-Dcom.sun.management.jmxremote.port=7199
-Dcom.sun.management.jmxremote.rmi.port=7199
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Djava.rmi.server.hostname=localhost
Terminal
ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi ping | jq .
{
"schemaVersion": 1,
"ok": true,
"result": {
"connected": true
},
"durationMs": 48
}
- Do not set
com.sun.management.jmxremote.host=127.0.0.1in the container. Docker forwards the port to the container's own address, where the JVM then does not listen, and ajmx fails withCONNECTION_FAILED. - Without
java.rmi.server.hostname, the JVM hands out the container's own address. When your machine cannot reach it, as with Docker Desktop, ajmx waits until--timeoutand fails withCONNECTION_TIMEOUT.
Credentials#
-
Write a password file and an access file, and make them readable by their owner only. The JVM refuses to start if other users can read the password file.
Terminal
printf 'operator s3cret-pw\nviewer v1ew-pw\n' > jmxremote.password printf 'operator readwrite\nviewer readonly\n' > jmxremote.access chmod 600 jmxremote.password jmxremote.access -
Start the JVM with authentication turned on.
Terminal
java -Dcom.sun.management.jmxremote.port=7199 \ -Dcom.sun.management.jmxremote.rmi.port=7199 \ -Dcom.sun.management.jmxremote.authenticate=true \ -Dcom.sun.management.jmxremote.password.file=jmxremote.password \ -Dcom.sun.management.jmxremote.access.file=jmxremote.access \ -Dcom.sun.management.jmxremote.ssl=false \ -Djava.rmi.server.hostname=localhost \ -jar app.jar -
Give ajmx the username and password in
JMX_USERNAMEandJMX_PASSWORD.Terminal
export JMX_USERNAME=operator read -rs JMX_PASSWORD && export JMX_PASSWORD ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi pingOr pass them as JSON on stdin with
--credentials-stdin.Terminal
ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi --credentials-stdin ping < credentials.json{"username": "operator", "password": "s3cret-pw"}
ajmx has no option that takes a password, so it never shows up in your shell history or the
process list. batch reads its requests from stdin, so it takes credentials from the environment
variables only.
When an AI agent runs ajmx, export the variables yourself rather than paste the password into the conversation. See Agent skill.
A wrong or missing password fails with AUTH_FAILED. Retrying does not help.
Terminal
ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi ping | jq .
{
"schemaVersion": 1,
"ok": false,
"error": {
"code": "AUTH_FAILED",
"message": "Access denied by the JMX server",
"retryable": false,
"details": {
"url": "service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi"
}
},
"durationMs": 19
}
A readonly user, such as viewer above, can search, describe and read. A write or
invoke fails with AUTH_FAILED and does not run. This includes operations that only read, such
as findDeadlockedThreads. Here JMX_USERNAME is viewer:
Terminal
ajmx --url service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi write java.lang:type=Memory Verbose=true | jq .
{
"schemaVersion": 1,
"ok": false,
"error": {
"code": "AUTH_FAILED",
"message": "Access denied by the JMX server",
"retryable": false,
"details": {
"mbean": "java.lang:type=Memory",
"attribute": "Verbose"
}
},
"durationMs": 18
}