Type lock to lock your keyboard and mouse. Type unlock to unlock them.
A tiny Windows tool in C# / .NET 10: no NuGet packages, no runtime dependencies, no installer, no admin rights, no background service — one process, one config file, two words.
$ KeyboardLock
Keyboard Lock 1.0.0 - lock and unlock your input devices by typing a word
config C:\Users\me\.config\KeyboardLock\config.json
lock word "lock" unlock word "unlock" (case insensitive)
scope keyboard + mouse
key holding on (a matched sequence never reaches the active window)
timeout/grace 1500ms / 250ms
auto unlock disabled
force hotkey not configured
[10:31:02] [WARN ] 🔒 Input devices LOCKED - type "unlock" to unlock.
[10:31:07] [INFO ] 🔓 Input devices unlocked.
| Feature | Description |
|---|---|
| Word trigger | Type the lock word to lock the input devices, the unlock word to release them. |
| Keyboard + mouse | While locked, keys, mouse movement, clicks and wheel are swallowed (configurable). |
| Key holding | Keystrokes that could still become part of the sequence are held back. A match is swallowed as a whole, so typing lock never leaks loc into the window you were typing in; a mismatch is replayed with SendInput in the original order, so no keystroke is lost. |
| Word boundaries | deadlock, hellolock, deadlockunlock never trigger — the sequence must be a whole word. |
| Sequence timeout | A gap longer than sequenceTimeoutMs restarts the word. |
| Injected input | Keys synthesized by other apps are passed through by default, and Keyboard Lock's own replays are never re-captured (no feedback loop). |
| Optional safety nets | autoUnlockSeconds (auto-release) and forceUnlockHotkey (e.g. Ctrl+Alt+Shift+U), both off by default. |
| Tiny | Self-contained single-file release is ~11 MB (trimmed + compressed): no .NET runtime needed on the target machine, no ICU, no installer. |
- Windows 10 / 11, x64 or arm64
- .NET 10 runtime — or use the self-contained release build, which needs no runtime at all
# run from source
dotnet run --project src/KeyboardLock.App -- dry-run # simulate first: no hooks are installed
dotnet run --project src/KeyboardLock.App # run for real (config file is created on first run)
# or produce a single self-contained exe
dotnet publish src/KeyboardLock.App -c Release -r win-x64 --self-contained true -p:PublishSingleFile=trueKeyboardLock-win-x64.exe and KeyboardLock-win-arm64.exe are built by GitHub Actions and attached
to every artifact upload and tagged release.
KeyboardLock [command] [options]
Commands
run install the hooks and run (default)
dry-run install nothing; feed characters from stdin and print every decision
init write the default config file
print-config print the effective config and the config file path
version print the version
help show the help
Options
-c, --config <file> config file (default ~/.config/KeyboardLock/config.json)
--config-dir <dir> config directory (same as KEYBOARDLOCK_CONFIG_DIR)
--lock <word> override the lock word
--unlock <word> override the unlock word
--no-mouse let the mouse through while locked (escape hatch)
--no-keyboard let the keyboard through while locked
--no-suppress disable key holding/replay (the sequence prefix reaches the active window)
--unlock-timeout <s> auto-unlock after N seconds, 0 = disabled (default 0)
--hotkey <combo> force-unlock hotkey, e.g. Ctrl+Alt+Shift+U
--no-beep no beep on state change
--log-level <level> off/error/warn/info/debug
--no-log-file do not write a log file
--force with init: overwrite an existing config file
-h, --help show the help
-V, --version print the version
Exit codes: 0 ok · 1 bad arguments · 2 another instance is running · 3 invalid config ·
4 hook installation failed · 5 not running on Windows.
dry-run is a small REPL that feeds characters into the decision engine without touching the input
stack, which makes the behaviour easy to check (and to develop on macOS/Linux):
> lock
t=1040 'l' SWALLOW
t=1090 'o' SWALLOW
t=1140 'c' SWALLOW
t=1190 'k' SWALLOW -> LOCKED
state: LOCKED held keys: 0 word: ""
> unlock
...
t=1490 'k' SWALLOW -> unlocked
The first run creates ~/.config/KeyboardLock/config.json
(%USERPROFILE%\.config\KeyboardLock\config.json on Windows). It is JSON with // comments and
trailing commas:
Command line options win over the config file. An invalid config refuses to start and reports every
problem (sequence too short, lock and unlock words identical, unparsable hotkey, out-of-range
numbers, unknown log level). A corrupt file is backed up to config.json.bak-<timestamp> and rebuilt.
~/.config is used verbatim, as requested; on Windows it resolves to %USERPROFILE%\.config\KeyboardLock.
Set KEYBOARDLOCK_CONFIG_DIR or pass --config-dir to move it.
Every automatic safety net is off by default: while locked, the only way out is typing the unlock word.
- Emergency exit (always works): press
Ctrl+Alt+Del. The secure desktop is handled by the kernel and cannot be intercepted by low-level hooks — open Task Manager and end theKeyboard Lockprocess. - When the process exits (normally, killed, or its console window closed) the hooks disappear with it and input is immediately restored.
Ctrl+Cis swallowed while locked, so it cannot be used to stop the program from its own console.- Input aimed at elevated (administrator) windows cannot be intercepted: that is Windows UIPI, not a bug. Running as administrator is not required and usually not helpful.
- If you would rather not rely on the unlock word alone, enable a safety net:
--unlock-timeout 60,--hotkey Ctrl+Alt+Shift+U, or use words that are hard to trigger by accident:--lock kblock --unlock kbunlock. - With
--no-suppress, typinglocksendslocto the active window first — that is the known cost of that mode.
keyboard / mouse events
│
▼
WH_KEYBOARD_LL / WH_MOUSE_LL hooks (message loop on the main thread)
│ KeyStroke { virtual key, scan code, translated char, modifiers, injected? }
▼
LockEngine (src/KeyboardLock.Core — platform independent, covered by 71 checks)
│ EngineResult { swallow/pass, state change, strokes to replay }
▼
swallow → the callback returns 1, the event reaches no application
pass → CallNextHookEx
replay → SendInput, tagged with a custom dwExtraInfo so it is never re-captured
- Why can
unlockstill be typed while locked? A low-level hook sees events before they are delivered: swallowing them does not stop us from reading them. The engine still recognises the unlock sequence, while not a single character of it reaches any application. - Why hold keys at all? If only the last character were swallowed, the
locoflockwould already be in the active window. So the engine holds back any keystroke that could still become part of a sequence: a match is dropped as a whole, a mismatch is replayed in order (including key-up and modifier events) withSendInput. The checks assert exactly this through aDeliveredstring — "no keystroke lost, no character leaked".
src/KeyboardLock.Core,src/KeyboardLock.App,tests/KeyboardLock.Core.Tests: not a singlePackageReference— only the base class library plususer32.dll/kernel32.dllP/Invoke.- The test project ships its own ~300-line runner (
TestFramework.cs), so not even xunit is needed. That is why it is run withdotnet run, notdotnet test. InvariantGlobalizationkeeps ICU out of the self-contained build.System.Text.Jsonruns in source-generated mode (ConfigJsonContext), so no reflection is needed and the release can be trimmed: the self-contained single-file exe is ~11 MB instead of ~30 MB.
KeyboardLock.slnx
src/KeyboardLock.Core/ platform independent core (builds and tests anywhere)
AppConfig.cs config model + validation
ConfigPaths.cs ~/.config/KeyboardLock resolution
ConfigStore.cs JSON read/write, comment header, backup-and-rebuild
SequenceMatcher.cs word boundaries + sequence matching
LockEngine.cs lock state machine (holding/replay, timeout, grace, hotkey)
HotkeySpec.cs hotkey parsing and matching
Logging.cs console + file log
src/KeyboardLock.App/ Windows front end (net10.0-windows)
NativeMethods.cs user32/kernel32 P/Invoke
KeyTranslator.cs virtual key -> character (ToUnicodeEx + fallback)
LowLevelInputHook.cs low-level hooks, message loop, SendInput replay
CliOptions.cs / Program.cs command line, single-instance mutex, banner
DryRunRunner.cs dry-run simulator
tests/KeyboardLock.Core.Tests/ zero-dependency test runner (71 checks)
.github/workflows/build.yml build + test + publish win-x64/win-arm64 on GitHub Actions
dotnet build KeyboardLock.slnx -c Release
dotnet run --project tests/KeyboardLock.Core.Tests -c Release # 71 checks, no Test SDK required
dotnet run --project src/KeyboardLock.App -- dry-run # simulate keystrokesKeyboardLock.Core and the tests do not depend on Windows, so the logic can be developed and verified
on macOS/Linux; the Windows front end cross-compiles anywhere thanks to EnableWindowsTargeting.
中文文档见 README.zh-CN.md。
{ "lockSequence": "lock", // typing this word locks the input devices "unlockSequence": "unlock", // typing this word unlocks them "caseSensitive": false, "lockKeyboard": true, "lockMouse": true, // also swallow the mouse while locked "suppressSequenceKeystrokes": true, "sequenceTimeoutMs": 1500, // max gap between two characters "switchGraceMs": 250, // how long stray key-up events are swallowed after a switch "autoUnlockSeconds": 0, // 0 = never auto-unlock "forceUnlockHotkey": "", // "" = no hotkey "beepOnStateChange": true, "ignoreInjectedEvents": true, "logLevel": "info", "logToFile": true // write keyboardlock.log next to the config file }