Skip to content

Repository files navigation

Keyboard Lock

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.

build license

$ 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.

Features

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.

Requirements

  • Windows 10 / 11, x64 or arm64
  • .NET 10 runtime — or use the self-contained release build, which needs no runtime at all

Quick start

# 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=true

KeyboardLock-win-x64.exe and KeyboardLock-win-arm64.exe are built by GitHub Actions and attached to every artifact upload and tagged release.

Commands and options

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

Configuration

The first run creates ~/.config/KeyboardLock/config.json (%USERPROFILE%\.config\KeyboardLock\config.json on Windows). It is JSON with // comments and trailing commas:

{
  "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
}

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.

⚠️ Safety and emergency exit

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 the Keyboard Lock process.
  • When the process exits (normally, killed, or its console window closed) the hooks disappear with it and input is immediately restored.
  • Ctrl+C is 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, typing lock sends loc to the active window first — that is the known cost of that mode.

How it works

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 unlock still 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 loc of lock would 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) with SendInput. The checks assert exactly this through a Delivered string — "no keystroke lost, no character leaked".

Zero dependencies

  • src/KeyboardLock.Core, src/KeyboardLock.App, tests/KeyboardLock.Core.Tests: not a single PackageReference — only the base class library plus user32.dll / kernel32.dll P/Invoke.
  • The test project ships its own ~300-line runner (TestFramework.cs), so not even xunit is needed. That is why it is run with dotnet run, not dotnet test.
  • InvariantGlobalization keeps ICU out of the self-contained build.
  • System.Text.Json runs 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.

Project layout

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

Development

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 keystrokes

KeyboardLock.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。

License

MIT

About

Type `lock` to lock your keyboard and mouse, type `unlock` to unlock. Zero-dependency .NET 10 tool for Windows built on low-level input hooks; config lives in ~/.config/KeyboardLock.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages