Quick answer
Set the hook with a dwThreadId of 0 (global) instead of a specific thread, or change to a hook type that supports per-thread hooks (like WH_CALLWNDPROC). If you must use a global hook, your hook procedure must live in a DLL, and that DLL must match the bitness of every process you want to hook.
What's actually happening here is that the Windows hook manager in user32.dll enforces a rule: certain hook types are strictly global. WH_KEYBOARD_LL and WH_MOUSE_LL are the usual culprits — they can't be scoped to a single thread. When you call SetWindowsHookEx with a dwThreadId that isn't 0, the system returns ERROR_GLOBAL_ONLY_HOOK (1429 decimal, 0x595). It's not a permission issue or a broken DLL — it's simply that the hook type doesn't allow thread-level scoping. I've seen this trip up plenty of developers when they try to make a low-level keyboard hook apply only to a specific window. The system won't let you, and it tells you exactly why.
Why this happens
Low-level hooks (WH_KEYBOARD_LL, WH_MOUSE_LL) are implemented by injecting a callback into the message loop of the calling thread. That callback needs to see every input event, regardless of which process generated it. If you tried to attach it to one thread, the hook would only receive events when that thread is active — but the system can't guarantee that, and it would break the global input stream. So Microsoft hard-coded a restriction in the SetWindowsHookEx internals. The error is by design, not a bug.
Note that this error also appears when you try to set a global hook from a 32-bit process on a 64-bit system, but that's a different scenario — the error code is the same, but the fix is different. The error message says “only be set globally” which is very specific, so first check your thread ID.
Fix steps
- Confirm the hook type. Look at your code. If you're using
WH_KEYBOARD_LL(13) orWH_MOUSE_LL(14), those are global-only. AlsoWH_JOURNALPLAYBACK(1) andWH_JOURNALRECORD(0) are global-only, but they're rarely used anymore. - Set
dwThreadIdto 0. This tells the system you want a global hook. Example:
That's the immediate fix if you're okay with hooking all threads.HHOOK hook = SetWindowsHookEx(WH_KEYBOARD_LL, proc, hInstance, 0); if (!hook) { DWORD err = GetLastError(); // 0x595 if you still pass a thread ID } - If you need a thread-specific hook, change the hook type. For example, use
WH_CALLWNDPROC(4) orWH_GETMESSAGE(3). These support per-thread hooks. But watch out: your hook procedure must be in a DLL, not in your EXE. The system needs to inject the DLL into the target process. If your proc is in the EXE, you'll get a different error (usually 0x57, invalid parameter). - For global low-level hooks, you can keep the proc in your EXE. Low-level hooks are called in the context of the thread that set them, so the proc only needs to be in the module that calls
SetWindowsHookEx. That's why you can passGetModuleHandle(NULL)forhMod. But if you switch to a non-low-level hook, you must move the proc to a DLL.
Alternative fixes if the main one fails
If setting dwThreadId to 0 still returns 0x595, then you're on a 64-bit system and your hook DLL is 32-bit. The system loads a 64-bit version of the DLL into 64-bit processes, and a 32-bit version into 32-bit processes. If you only have one version, you'll get a failure — but the error might be 0x595 or something else. The real fix is to compile two versions of your DLL (x86 and x64) and install both. Or compile your entire app as 64-bit if you only target 64-bit processes.
Another edge case: if you're calling SetWindowsHookEx from a service process, the hook may still fail because the service runs in a different session. That usually gives a different error, but if you see 0x595, double-check that your service isn't passing a thread ID by accident.
Prevention tip
Before writing code, decide whether you truly need a global hook. If you're only tracking keyboard input in a single window, use SetWindowsHookEx with WH_KEYBOARD (2) and the window's thread ID. That works per-thread. Or better, use RegisterHotKey or a plain WM_KEYDOWN handler. Low-level hooks are powerful but expensive — they run on every keystroke system-wide, and they can cause performance issues if your callback is slow. I've seen apps that lag because they did heavy I/O in a hook proc. So keep the callback lean, and only go global when you actually need to see input from all apps.
If you're building a macro recorder, a global keyboard hook is the way to go. Just remember: set dwThreadId to 0, keep the proc in your EXE, and make sure your EXE bitness matches the target OS (or use a helper DLL for non-low-level hooks). That'll get you past 0x595 without surprises.
Always checkGetLastError()afterSetWindowsHookExfails. The error code tells you exactly what the system choked on, and 0x595 means it's strictly a thread-scope issue.