On all my computers, I use some kind of solution to swap Esc and Caps Lock. I spend a lot of time in the terminal, and having Esc in an easy place for my pinky to reach is just much more ergonomic. On Ubuntu, I use GNOME Tweaks to enable the built-in Swap Esc and Caps Lock option under Keyboard → Additional Layout Options. On Windows, I used to use a utility called uncap, but have since switched to PowerToys.
The problem is: Minecraft does not respect this. My free time is very limited these days, so when I picked up Minecraft after many years for an evening or two of digging, I really didn't want to have to find out why. Yes, this is yet another post about LLMs. I fired up Claude and asked this:
You're in my ~/.minecraft folder. The launcher starts it like this (minus secret tokens)
/home/jack/.minecraft/runtime/java-runtime-epsilon/linux/java-runtime-epsilon/bin/java -Xss1M --sun-misc-unsafe-memory-access=allow --ena [...] necraft --assetsDir /home/jack/.minecraft/assets --assetIndexOn Ubuntu I have remapped caps lock to escape, but minecraft doesn't seem to respect it. Can you figure out why? Can I patch the code? What can I do?
Claude started chugging, running strings on things and checking my system setup. Eventually, it offered this:
Here's the full picture:
Root Cause
This is a GLFW design issue, not an XKB or Wayland bug. I extracted the GLFW native library from LWJGL and confirmed it contains a hardcoded table of XKB physical key names:
...SPCE.ESC.RTRN.TAB.BKSP...CAPS.SCLK.NMLK...GLFW maps
ESC(physical Escape position) →GLFW_KEY_ESCAPEandCAPS(physical Caps Lock position) →GLFW_KEY_CAPS_LOCK. It does this by reading XKB key names, which identify physical locations — not the keysyms those keys produce.
caps:swapescaperemaps the keysym (what symbol the key produces) but doesn't rename the physical key. So GLFW completely ignores it. This is by design — GLFW intentionally uses physical positions so games behave consistently across keyboard layouts.This affects both X11 and Wayland backends. Even forcing X11 mode (
WAYLAND_DISPLAY=) won't help.
Solutions
Option 1:
keyd— remap at the kernel evdev level (recommended)
keydintercepts/dev/inputevents before any XKB/GLFW translation, so GLFW literally sees the physical Escape scancode from your Caps Lock key. It's not in Ubuntu's main repos, but easy to install:git clone https://github.com/rvaiya/keyd cd keyd && make && sudo make install sudo systemctl enable --now keydConfig at
/etc/keyd/default.conf:[ids] * [main] capslock = esc esc = capslocksudo systemctl restart keydThis fixes Minecraft (and everything else), since the key swap happens before any app sees the event.
Option 2:
input-remapper— same evdev level, with a GUIIt's in your Ubuntu repos:
sudo apt install input-remapperThen use the GUI to set
KEY_CAPSLOCK→KEY_ESC(and optionallyKEY_ESC→KEY_CAPSLOCK).Option 3: Patch Minecraft with a Java agent
If you only want the remap for Minecraft, you can write a small Java agent that hooks
GLFW.glfwSetKeyCallback()and swaps the key codes before Minecraft sees them:// In a premain agent: // Intercept all key callbacks and remap // GLFW_KEY_CAPS_LOCK (280) ↔ GLFW_KEY_ESCAPE (256)This requires writing and compiling a Java agent JAR, then adding
-javaagent:/path/to/agent.jarto the launch command. I can build this for you if you want.Recommendation: Option 1 (
keyd) is the cleanest — a 10-line config fixes it permanently at the hardware level for all applications. Want me to write the install steps or build the Java agent?
I did not want to install keyd (I might experiment with it in the future, it seems like an interesting project), and I figured it would have been fun to get Claude to do something in an ecosystem I have absolutely zero knowledge of. I know next to nothing about Java -- definitely not enough to know about agents or how to modify the bytecode to swap the key definitions, so of course I said
Let's do option 3 for funsies.
This is an abridged version of what Claude did:
Claude: Let me first check the Java version and explore the LWJGL API to plan the agent correctly.
(Bash: ran
java -versionon Minecraft's bundled JRE → discovered it's OpenJDK 25.0.1)(Python: listed GLFW-related classes inside the LWJGL jar to find the right callback class)
Claude: Java 25! That means we can use
java.lang.classfile— the modern bytecode API built into the JDK, no external deps needed. Let me inspect the actual GLFW method to plan the transform precisely.(Python: extracted
GLFW.classand ranjavapon it to enumerate the key constants)(javap on GLFW.class: dumped the verbose constant pool to find numeric key values)
(grep on the javap output: confirmed
GLFW_KEY_CAPS_LOCK = 280)(javap -p on GLFW.class: got the exact signature
glfwSetKeyCallback(long, GLFWKeyCallbackI))Claude: Good, I have all the info I need:
- Java 25 →
java.lang.classfileAPI built in (no external deps)GLFW_KEY_ESCAPE = 256,GLFW_KEY_CAPS_LOCK = 280glfwSetKeyCallback(long window, GLFWKeyCallbackI cbfun)→cbfunis at local variable slot 2 (after the 2-slotlong)Let me set up the project and write the agent.
(Bash: confirmed Minecraft's JRE ships with
javacandjar)(Bash: checked the system Java too — found OpenJDK 21, decided to use the bundled 25 instead)
Claude: The Minecraft JRE has
javacandjar. Let me build the whole thing.(Bash: created
~/.minecraft/keyremap-agent/{src,build/classes}directory structure)(Write: first draft of
KeyRemapAgent.java— aClassFileTransformerthat rewritesglfwSetKeyCallbackto wrap incoming callbacks with a CapsLock↔Escape swap)(Write:
MANIFEST.MFdeclaringPremain-Class: KeyRemapAgent)(Write:
build.shto compile against the LWJGL jar and package the agent)(Bash: ran build.sh — failed. Two errors:
org.lwjgl.system.CallbackI not found, andcannot find symbol: method transform(ClassModel, ClassTransform))(Bash: located the base
lwjgl-3.4.1.jarto add to the compile classpath, fixing the CallbackI error)(javap on
java.lang.classfile.ClassFile: discovered the method is actuallytransformClass, nottransform)Claude: In Java 25 it's
transformClass, nottransform. Let me also check the other APIs changed:(javap on
ClassTransform: confirmed factory method names)(javap on
MethodBuilder: confirmedtransformCodesignature)Claude: Good, I know the exact API now.
transformClass,transformingMethodBodies(note singular "Body"). Let me fix both issues:(javap on
ClassFile.Option: investigated whether stack map generation needed an explicit option)(javap on
ClassFile$StackMapsOption: confirmed defaults are fine)(javap on
CodeTransform: confirmed how to express the bytecode insertion)Claude: All the pieces are confirmed. Now let me rewrite with the correct Java 25 API:
(Write: rewrote
KeyRemapAgent.javausing the correcttransformClass/transformingMethodBodiesAPI)(Edit: added the base LWJGL jar to the classpath variable in
build.sh)(Edit: updated the
-cpflag inbuild.shto use both jars)(Bash: re-ran build.sh — succeeded, produced
keyremap-agent.jar)Claude: It compiled. Let me verify the JAR looks right and do a quick sanity check on the transform:
(Python: cracked open the agent JAR — confirmed it contains the two expected classes and a manifest with the right
Premain-Classentry)(Bash: tried to inline a smoke-test program via heredoc — failed because of a shell-quoting issue with the JVM args)
(Bash: wrote the smoke test to a real
.javafile, compiled and ran it against the actual GLFW class, then verifiedglfwSetKeyCallbackstill existed in the transformed bytecode → original 65,185 bytes, transformed 65,305 bytes, +120 bytes)Claude: +120 bytes — exactly the 3 inserted instructions plus regenerated stack maps. It works. Now just hook it into the launcher.
How to wire it in
Add this JVM flag to your Minecraft launch config:
-javaagent:/home/jack/.minecraft/keyremap-agent/keyremap-agent.jarIn the official Minecraft Launcher:
- Go to Installations → click
...on your install → Edit- Click More Options
- Paste the flag at the end of the JVM Arguments field (before anything else, just make sure it's before the main class)
When Minecraft starts you'll see this in the log confirming it's active:
[KeyRemapAgent] Active: CapsLock <-> Escape swap for GLFW [KeyRemapAgent] Patched GLFW.glfwSetKeyCallback
What it does, end to end
Physical CapsLock key → evdev KEY_CAPSLOCK (scancode 58) → GLFW reads XKB name "CAPS" → GLFW_KEY_CAPS_LOCK (280) → glfwSetKeyCallback fires with key=280 → [agent wrapper] remaps 280 → 256 (GLFW_KEY_ESCAPE) → Minecraft sees: Escape pressed ✓The agent uses
java.lang.classfile(built into Java 25) to rewriteglfwSetKeyCallbackat load time — it inserts a singlewrapCallback()call before the existing bytecode, so Minecraft's registered callback always receives the remapped key value. No JARs bundled, no external dependencies.
It worked.
For all its downsides, when AI does something like this it feels downright magical. I basically told the computer "Hey, make this program do this instead" and the computer... did it. Three and a half years after ChatGPT launched, it can be very easy to forget how insane all of this is.
Anyway, the code is here in case it ends up being useful to anybody else: https://github.com/iamjackg/minecraft-remap-escape
Happy mining!