Fixes RDCMan's broken Focus Release hotkeys (Ctrl+Alt+Left/Right) on Windows 11 22H2+.
Place RDCManHotkeyFix.Injector.exe in the same directory as RDCMan.exe and run it.
It will automatically find and launch RDCMan with the fix applied.
Or you can run the command:
RDCManHotkeyFix.Injector.exe [path\to\RDCMan.exe] [-- RDCMan args...]
| Before (broken) | After running with fix (fixed) |
|---|---|
![]() |
![]() |
Windows 11 22H2 changed the RDP ActiveX default behavior:
AllowAxToContainerEvents was changed from true to false,
causing Ctrl+Alt+Arrow to be sent directly into the remote session
instead of triggering the OnFocusReleased event.
Injector.exe (Launcher)
+-- Sets DOTNET_STARTUP_HOOKS environment variable
+-- Launches RDCMan.exe
+-- CoreCLR loads StartupHook.Initialize()
+-- Native inline hook: patches Server.ConnectAs()
+-- Prefix: InitClient() -> set AllowAxToContainerEvents = true
+-- Original ConnectAs (including Connect)
Technical Details
A .NET Core 3.0+ mechanism that loads custom managed assemblies before the app's Main() runs.
- CoreCLR looks for
internal static class StartupHook(no namespace) - Calls
static void Initialize() - The hook assembly's target framework must be compatible with the host app
Why not Harmony?
Harmony 2.3.3's underlying MonoMod does not work in .NET 8 single-file apps:
- MonoMod needs to load
clrjit.dllfrom the filesystem for JIT operations - .NET 8 single-file apps bundle
clrjit.dllinside the exe, loaded from memory - MonoMod can't find
clrjit.dlland throwsPlatformNotSupportedException: Could not locate clrjit library - Even preloading a system-installed
clrjit.dllcausesNullReferenceExceptiondue to version mismatch
Related issues:
- Harmony is not compatible with .NET 8 - pardeike/Harmony#543
- .NET 8 support - MonoMod/MonoMod#149
- Packaging clrjit.dll in a Single File Executable - dotnet/runtime#107377
Alternative: x64 Native JMP Hook
Writes a 14-byte absolute jump at the beginning of the target method's JIT'd machine code:
FF 25 00 00 00 00 ; JMP [RIP+0]
XX XX XX XX XX XX XX XX ; 8-byte target address
How is the JIT'd code address found?
GetFunctionPointer() may return a precode stub, not the actual JIT'd code.
Virtual method dispatch bypasses the precode and jumps directly to the JIT'd code,
so patching the precode alone won't intercept virtual calls.
Inspired by MonoMod's approach:
starting from GetFunctionPointer(), the code detects x64 JMP instruction patterns
(E9 rel32, FF 25 [RIP+disp32] abs64) and follows the JMP chain
until it reaches a non-JMP instruction — the actual method body.
This approach depends only on CPU instruction encoding, not on .NET runtime internal struct layouts.
The original method is called via a restore-invoke-repatch pattern (with a lock to prevent race conditions).
Known limitations:
- Tiered compilation: .NET Core's tiered compilation may re-JIT methods, potentially overwriting the hook
- RIP-relative instructions: If using a trampoline, copied instructions may contain RIP-relative addressing that needs offset adjustment after relocation
dotnet build Payload.csproj -c Release
dotnet publish Injector.csproj -c Release
Payload must be built first — the Injector embeds it as a resource.
Output: bin/Release/net8.0-windows/win-x64/publish/RDCManHotkeyFix.Injector.exe
- RDCMan 2.93+ (.NET 8 single-file)
- Windows 11 22H2 / 23H2 / 24H2

