# SharpHook: global keyboard and mouse hooks for .NET on three operating systems

> SharpHook is a .NET wrapper over the native libuiohook library that gives a C# application global keyboard and mouse events, event simulation and text entry simulation, with separate NuGet packages for Reactive, ReactiveUI and R3. It is well documented on platforms where a hook is easy, and awkward on Linux Wayland, where it needs elevated privileges and lacks several features.

**TolikPylypchuk/SharpHook** — SharpHook provides a cross-platform global keyboard and mouse hook, event simulation, and text entry simulation for .NET

- Repository: https://github.com/TolikPylypchuk/SharpHook
- Website: https://sharphook.tolik.io
- Stars: 518 · Forks: 44
- Language: C#
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tolikpylypchuk-sharphook

## A managed wrapper over a native hook library

SharpHook provides a cross-platform global keyboard and mouse hook, event simulation, and text entry simulation for .NET. The architecture is two layers. At the bottom is libuiohook, a native C library that does the actual hooking and event synthesis. On top of it sits SharpHook, which the README describes as a wrapper that provides direct access to its features as well as higher-level types to work with it.

That split is the main design decision, and it explains both the strengths and the limits. Because the platform work happens in the native library, the same managed API can cover Windows, macOS and Linux, and the .NET side deals in ordinary events and handlers rather than platform specific callback conventions. Because the platform work still has to be done, the constraints of each operating system survive the abstraction, and SharpHook documents them rather than hiding them.

The project is written in C#, which is what the repository's primary language tag says, and the source lives in src/ with a SharpHook.slnx solution file. The libuiohook dependency is vendored as a git submodule, visible as libuiohook/ in the top level entries alongside .gitmodules, which means the native sources are built with the managed project rather than being fetched at install time.

The users are .NET developers who need input events that no application focus model gives them: automation of other applications, hotkey systems, input recording, remote control helpers and test harnesses. The description also names event simulation and text entry simulation, which are the write side of the same capability.

## Five packages, because the reactive stack is your choice

The NuGet table at the top of the README lists five packages rather than one, and the split is about how you consume the events.

SharpHook is the core package and carries the hook types themselves. SharpHook.Reactive, SharpHook.ReactiveUI and SharpHook.R3 each adapt the hook events to a different reactive library, so a project using Reactive, ReactiveUI or R3 does not have to write its own event to observable plumbing. SharpHook.Testing is the fifth package and is where the testing helper lives, which matters for anyone automating their own application or building a test harness around simulated input.

The installation section is a list of dotnet commands, one per package:

```bash
dotnet add package SharpHook
dotnet add package SharpHook.Reactive
dotnet add package SharpHook.ReactiveUI
dotnet add package SharpHook.R3
dotnet add package SharpHook.Testing
```

You add only the ones you need, and the README shows each as a separate command rather than a combined snippet, which matches the reactive adapter being an optional layer rather than a dependency of the core.

For anything beyond the quick start, the documentation site at sharphook.tolik.io carries the API reference. There is also a migration guide for upgrading between major versions, which matters because the release history shows the project is not shy about breaking changes: v7.1.3 in July 2026, then v8.0.0 in August 2026, then v8.0.1 on 2026-10-18. The repository also has a samples directory with a SharpHook.Sample project and a CHANGELOG.md.

## The platform table has holes, and Linux x86 is one of them

SharpHook targets .NET 10+, .NET Framework 4.7.2+, and .NET Standard 2.0, and the README publishes an availability matrix per architecture.

Windows is yes on x86, x64 and Arm64, and no on Arm32, with the note that support for Windows on Arm32 was removed in version 5.0 since it was removed in .NET 5. Windows 11 is supported, and the README adds that SharpHook should work on older versions of Windows as well.

macOS is yes on x64 and Arm64, with x86 listed as not applicable. macOS 10.15+ is required, though only the currently supported macOS versions are supported by SharpHook. Mac Catalyst is supported and requires version 13.1+. The permission requirement is the important operational detail: macOS requires that access to the Accessibility API be enabled for the application if it wants to create a global hook or simulate events. That is a user action in system settings, so a global hook on macOS is a prompt the end user has to accept before anything works.

Linux is yes on x64, Arm32 and Arm64, and no on x86, because Linux on x86 is not supported by .NET itself. Linux distributions supported by .NET are supported by SharpHook. Both X11 and Wayland are supported, but the README is explicit that Wayland support lacks several features and requires elevated privileges, and that on Linux multiple libuiohook backends are present and the backend choice can be customized.

So the matrix reads differently depending on which platform you are on. On Windows and macOS the question is permissions on macOS. On Linux it is architecture, plus a Wayland path that is documented as incomplete and privileged.

## Create the hook, subscribe, then run it

The usage example is the clearest statement of the API shape. SharpHook provides the IGlobalHook interface along with three default implementations you can use to control the hook and subscribe to its events:

```csharp
using SharpHook;

var hook = new EventLoopGlobalHook();

// Events of type EventHandler<HookEventArgs>:
hook.HookEnabled += OnHookEnabled;
hook.HookDisabled += OnHookDisabled;

// Events of type EventHandler<KeyboardHookEventArgs>:
hook.KeyTyped += OnKeyTyped; // Disabled by default
hook.KeyPressed += OnKeyPressed;
hook.KeyReleased += OnKeyReleased;

// Events of type EventHandler<MouseHookEventArgs>:
hook.MouseClicked += OnMouseClicked;
hook.MousePressed += OnMousePressed;
hook.MouseReleased += OnMouseReleased;
hook.MouseMoved += OnMouseMoved;
hook.MouseMovedRelative += OnMouseMovedRelative;
hook.MouseDragged += OnMouseDragged;
hook.MouseDraggedRelative += OnMouseDraggedRelative;

// Events of type EventHandler<MouseWheelHookEventArgs>:
hook.MouseWheel += OnMouseWheel;

hook.Run();
// or
await hook.RunAsync();
```

The order matters: you create the hook, then subscribe to its events, and then run it. The Run method runs the hook on the current thread, blocking it, while RunAsync runs the hook on a separate thread and returns a Task that finishes when the hook is stopped. You can subscribe to events after the hook has started, which is what makes late binding to a running hook possible.

The event surface is broad on purpose. Keyboard events cover typed, pressed and released, mouse events cover clicked, pressed, released, moved, moved relative, dragged and dragged relative, and there is a separate wheel event. The relative variants matter for automation, since they distinguish a mouse moved to a position from a mouse moved by an amount, which is what synthetic input usually needs.

One subscription is off by default: KeyTyped is disabled by default, which tells you that key typed is treated as a higher cost event than key pressed and key released.

## Stopping, disposing, and the one instance rule

The lifecycle rules are stated precisely, and two of them will bite you if you read them late.

IGlobalHook contains the Stop method to stop the global hook. After stopping, the hook can be started again by calling Run or RunAsync. Calling Stop when the hook is not running does nothing. So a stopped hook is not a dead hook, and restarting is the intended recovery path.

IGlobalHook extends IDisposable, and disposal is terminal. Calling Dispose on a hook disposes it and stops it if it was running, and once a hook has been disposed it cannot be started again, you have to create a new instance. Calling Dispose when the hook is not running does nothing other than marking the instance as disposed. That asymmetry between Stop and Dispose is worth internalising, because a helper that disposes a hook in a finally block will leave you with an unstartable instance.

Then there is the constraint the README marks as important: always use one instance of IGlobalHook at a time in the entire application, since they all must use the same static method to set the hook. That is a hard architectural limit, not a style hint. The hook is installed through a single static entry point in the native layer, so two hook objects in one process fight over the same global installation. A design that wants two independent hooks, for example one per feature module, has to multiplex through a single IGlobalHook instead.

That limit is also why there is a single global hook event pair, HookEnabled and HookDisabled, so a feature can know when the shared hook is actually capturing events rather than assuming its own subscription is live.

## Suppressing events and telling real input from simulated input

Two event properties make the library usable for more than logging.

SuppressEvent, set on the event arguments inside the handler, stops the event from propagating further. The constraints are tight and worth repeating: it must be done synchronously, and it is only supported on Windows and macOS. So the classic use case of a hotkey or a shortcut remapper, where the app swallows the original keystroke and does something else, works on two of the three platforms. On Linux the original event continues to the target application.

EventTime and IsEventSimulated let a handler know when an event happened and whether it was real or simulated. The second one is the more interesting of the pair for a library that also provides event simulation. If your application can both generate input and observe input, then without that flag your own simulated keystrokes come back as if a person typed them, and automation loops are hard to avoid. Being able to tell the two apart is what lets a recorder ignore its own output.

The README also lists hook events as being of type HookEventArgs or a derived type which contains more info, which is the pattern that keeps the handler signatures uniform across keyboard, mouse and wheel events.

## Where SharpHook stops being the right tool

Three boundaries stand out.

The first is Wayland on Linux. The README says outright that Wayland support lacks several features and requires elevated privileges. A product that has to run on a modern Linux desktop therefore has a Linux path that behaves differently from the Windows and macOS paths, and a Linux path that asks for more privilege than a user application usually wants.

The second is the one instance rule combined with the disposal semantics. A component based architecture where each feature owns its own hook will not work as written, and the fix is to push the single hook up to a shared owner and fan events out yourself.

The third is the version churn. v8.0.0 arrived in August 2026 shortly after v7.1.3 in July, and the project keeps a migration guide for upgrades between major versions. For an application pinned to a specific .NET target, that is a recurring maintenance item rather than a one time cost.

There is also what the documentation does not cover. The README section visible here documents global hooks in detail, and the description mentions event simulation and text entry simulation, but the API reference for the simulation side lives on the documentation site rather than in the README, and no simulation example appears in the README. Anyone adopting this primarily for synthetic input will spend their first hour in the docs rather than in the code.

## Conclusion

SharpHook suits a .NET developer building a Windows or macOS utility, an automation harness or a macro tool that needs every keystroke regardless of which window has focus, and who wants managed events plus a reactive adapter instead of raw native callbacks. It does not suit a Linux Wayland deployment that cannot grant elevated privileges, a design that needs more than one hook running at a time, or a project that must stay on .NET Framework 4.7.2 with no option to move. Verify first by installing the SharpHook package, running the sample application on the target platform, and confirming that the events your feature depends on actually arrive, since the README states that Wayland support lacks several features.

## FAQ

### How do I install SharpHook in a .NET project?

Add the package with dotnet add package SharpHook. The reactive adapters are separate packages, dotnet add package SharpHook.Reactive, SharpHook.ReactiveUI or SharpHook.R3 depending on the stack you use, and SharpHook.Testing holds the testing helpers.

### Which platforms and architectures does SharpHook support?

Windows is supported on x86, x64 and Arm64, macOS on x64 and Arm64, and Linux on x64, Arm32 and Arm64, with Linux on x86 unsupported because .NET itself does not support it. The target frameworks are .NET 10+, .NET Framework 4.7.2+ and .NET Standard 2.0.

### Why does my global hook not receive events on macOS?

macOS requires access to the Accessibility API to be enabled for the application before it can create a global hook or simulate events. That permission has to be granted by the user in system settings.

### Can I run more than one SharpHook instance in the same application?

No. The README states that you should always use one instance of IGlobalHook at a time in the entire application, since they all must use the same static method to set the hook. Multiple features have to share a single hook.

### How do I stop and restart a SharpHook hook?

Call Stop to stop the hook, after which it can be started again with Run or RunAsync. Dispose is different: it stops a running hook and marks the instance as disposed, and a disposed hook cannot be started again, so a new instance is required.

### Is event suppression supported on every platform?

No. Setting SuppressEvent inside the handler stops the event propagating further, but the README states it must be done synchronously and is only supported on Windows and macOS.

## Sources

- [License: MIT](https://github.com/TolikPylypchuk/SharpHook/blob/main/LICENSE)
- [Project website](https://sharphook.tolik.io)
- [README](https://github.com/TolikPylypchuk/SharpHook/blob/main/README.md)
- [Releases](https://github.com/TolikPylypchuk/SharpHook/releases)
- [TolikPylypchuk/SharpHook on GitHub](https://github.com/TolikPylypchuk/SharpHook)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tolikpylypchuk-sharphook
