Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ These are the working rules for agents in this repo. Staypaw is a Windows tray a

Staypaw watches the keyboard. People run it only if they can trust it, so never break these, not even in dev tooling that ships.

- **No code in other processes.** No DLL injection, no `CreateRemoteThread`, no hook DLLs loaded into other apps. Use only out-of-context WinEvent hooks, Raw Input and low-level hooks (`WH_KEYBOARD_LL`, `WH_MOUSE_LL`), which all run in Staypaw's own process. Work laptops ban injection, and anti-cheat flags it.
- **Never touch a game's process.** Identify a window's app with `PROCESS_QUERY_LIMITED_INFORMATION` and nothing more: no memory reads and no input sent to it. Held keys aimed at a game are dropped, never replayed. Game terms of service forbid tools that read or drive the client, and replayed keys are fake input.
- **No code in other processes.** No DLL injection, no `CreateRemoteThread`, no hook DLLs loaded into other apps. Use only out-of-context WinEvent hooks, Raw Input and low-level hooks (`WH_KEYBOARD_LL`, `WH_MOUSE_LL`), which all run in Staypaw's own process. A test fails on any other hook type passed to `SetWindowsHookEx`. Work laptops ban injection, and anti-cheat flags it.
- **Never touch a game's process.** Identify a window's app with `PROCESS_QUERY_LIMITED_INFORMATION` and nothing more: no memory reads and no input sent to it. Held keys aimed at a game are dropped, never replayed. A game is the protected app, an exe that Windows' Game Bar lists, or an app Staypaw can't name, because anti-cheat can hide even a process's name. Game terms of service forbid tools that read or drive the client, and replayed keys are fake input. One exception, the owner's call: to finish a restore that left no window in front, Staypaw's thread shares the input of your window's thread for about a millisecond, even a game's (see `ROADMAP.md`).
- **No networking code.** No `HttpClient`, sockets, updater or telemetry, and no package that brings them in. A test fails on any `System.Net` use in Staypaw's assemblies. A keyboard watcher that talks to the network can't be trusted.
- **Never record which key was pressed.** The log and memory keep only the time of a keypress. The one exception is held keys (M3): kept in memory until they're delivered or dropped, and never written anywhere. Anything more makes Staypaw a keylogger.
- **Never record which key was pressed.** The log and memory keep only the time of a keypress. The one exception is held keys (M3): kept in memory until they're delivered or dropped, and never written anywhere. Never trace or log a `HeldKey`; it has no `ToString` beyond its type name for that reason. Anything more makes Staypaw a keylogger.
- **Run unelevated.** Don't set `requireAdministrator` in the manifest. An elevated mode is a Later item with its own decision.
- **Code ported from NoFocusSteal keeps its MIT notice** in `THIRD-PARTY-NOTICES.md`. The license requires it.

Expand All @@ -25,6 +25,7 @@ Staypaw watches the keyboard. People run it only if they can trust it, so never
- `Staypaw.Core` holds the watcher and the rules and never references Avalonia; a test enforces it. Rules take the event and the time as inputs and call no Win32, so tests run without real windows.
- Win32 calls go through CsWin32 (`NativeMethods.txt`), not hand-written `DllImport`. The generated code is typed and NativeAOT-safe.
- NativeAOT must stay free of warnings: no reflection-based serialization. Use source generators.
- The keyboard hook runs on a thread of its own, and its callback only compares and copies. Every key on the system waits for it, and Windows quietly removes a hook that answers too slowly.

## Brand

Expand Down Expand Up @@ -54,8 +55,8 @@ One meaning per term. Use these words in code, docs and commits.
## Commands

- `dotnet build` builds everything. `dotnet test` runs the tests.
- `tests/Staypaw.Tests/bin/Release/net10.0-windows/Staypaw.Tests.exe -explicit only` runs the explicit tests, which steal focus on the real desktop. Run them only when the owner isn't typing.
- `tools/Staypaw.StealTest` takes focus the way misbehaving apps do: `Staypaw.StealTest.exe <delaySeconds> [alt|attach|both] [repeatMs]`. It prints how many keys reached it.
- `tests/Staypaw.Tests/bin/Release/net10.0-windows/Staypaw.Tests.exe -explicit only` runs the explicit tests, which steal focus on the real desktop, and one types an unused key (0x97) into its own test windows. Run them only when the owner isn't typing.
- `tools/Staypaw.StealTest` takes focus the way misbehaving apps do: `Staypaw.StealTest.exe <delaySeconds> [alt|attach|both] [repeatMs]`. It also steals whenever its thread gets `WM_APP`, and it prints how many keys reached it.
- `dotnet publish src/Staypaw.Desktop -c Release -r win-x64` builds the native exe. With Visual Studio 18, put `%ProgramFiles(x86)%\Microsoft Visual Studio\Installer` on `PATH` first: its `VsDevCmd.bat` calls `vswhere.exe` without a path, and the error text breaks the NativeAOT link step ("'vswhere.exe' is not recognized").
- `dotnet run scripts/make-icons.cs` regenerates `docs/brand/staypaw.ico` and `app-icon-256.png` from `app-icon.svg`.
- `uv run --no-project --with fonttools --with uharfbuzz python -I scripts/make_lockup.py <Fredoka-SemiBold.ttf> docs/brand` rebuilds the lockups. Its header says where to get the font.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Windows lets apps pull focus to themselves: a chat popup, an installer, a browse

<img align="right" width="150" src="docs/brand/calla/slow-blink.png" alt="Calla, a calico cat-eared homebody with warm white hair, slow-blinks with her arms folded, as Staypaw does when all is quiet.">

> **Status:** in development. Staypaw already logs every steal and protects the app you pick, but there's no release to install yet. See [ROADMAP.md](ROADMAP.md). Until Staypaw ships, use one of the tools under [Alternatives](#alternatives).
> **Status:** in development. Staypaw already logs every steal, protects the app you pick and holds stray keys, but there's no release to install yet. See [ROADMAP.md](ROADMAP.md). Until Staypaw ships, use one of the tools under [Alternatives](#alternatives).

<br clear="right">

Expand All @@ -31,17 +31,17 @@ Planned for 1.0:

- **Protects an app you pick.** Nothing takes focus from it unless you click or Alt+Tab away, even while you're idle.
- **Keeps focus where you're typing.**
- **Holds stray keys.** A key you press during a steal never reaches the app that stole focus.
- **Holds stray keys.** A key you press during a steal never reaches the app that stole focus. It goes to your window once focus is back, or is dropped if that window is a game.
- **Tells you which app did it,** in a log you can open.
- **Installs signed,** through winget.

## The trust contract

Staypaw watches your keyboard, so it has to be easy to trust.

- **Nothing injected.** Staypaw never loads code into another app, and never touches a game's process.
- **Nothing injected.** Staypaw never loads code into another app, and never touches a game's process. Its keyboard hook is the low-level kind, which runs inside Staypaw itself.
- **No network.** There is no networking code, no updater and no telemetry.
- **No keys stored.** It keeps the time of each keypress, never the key. Keys held during a steal stay in memory for a moment, then go to your window or are dropped. Nothing about them is written to disk.
- **No keys stored.** It keeps the time of each keypress, never the key. Keys held during a steal stay in memory for a moment, then go to your window or are dropped. They never go to a game. Nothing about them is written to disk.
- **Open source,** so you can check all of this instead of trusting it.

## Alternatives
Expand Down
17 changes: 14 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ What the design must protect. Everything else gets ordinary good design.

The tray menu costs as much as the window: the first thing Avalonia draws starts its renderer, which isn't released until Staypaw quits. The welcome pays at first launch what the first tray click pays anyway. This gives up M1's idle figure as the whole story: it holds only until the menu or the window first opens.

## Decisions (2026-10-24)

- **Held keys are replayed into your window, never into a game.** A window counts as a game when it's the protected app, an exe that Windows' Game Bar lists (read from the registry, so the game itself is never touched), or an app Staypaw can't name, since anti-cheat can hide even a process's name. Replayed keys are input the game didn't get from you. This gives up the keys typed in a steal's first milliseconds when the protected app isn't a game, and leaves a game that Windows doesn't list, and that you didn't protect, open to replayed keys.
- **Keys wait until Staypaw has judged a focus change and finished any restore.** A `WH_KEYBOARD_LL` hook on a thread of its own catches them. Every key on the system waits for that hook, and Windows quietly removes one that answers too slowly, so it can't share the UI thread, which may be busy drawing. Keys held for a change that stands go to the new window only if it was your switch; otherwise they're dropped, never sent to a stealer. This gives up keys typed in the first milliseconds of a change Staypaw lets stand, such as a link you opened.
- **A hold that makes no progress for 500 ms drops its keys and lets the keyboard go,** so a stuck watcher or a bug can't take the keyboard. This gives up those keys.
- **A restore calls `SetForegroundWindow` alone, without NoFocusSteal's `BringWindowToTop`.** Its extra activation could leave no window in front: in a steal test that typed without pause, about 40 of 1,000 keys reached no window, and each steal logged two `Restored` rows and counted twice toward the fight limit. `SetForegroundWindow` raises the window anyway. This gives up NoFocusSteal's exact restore.
- **A restore that leaves no window in front is finished from your window's thread.** After a steal, `SetForegroundWindow` can return true and still leave no window in front, for good: your keys go nowhere and the stealer can't even steal again. A plain retry never fixed it. While it lasts, for up to 500 ms, the 15 ms check retries with Staypaw's thread sharing the input of your window's thread (`AttachThreadInput`), which finished 17 of 17 such restores. It's the one time Staypaw shares a game's input state: for about a millisecond, with no process handle, no memory read and no input sent. A hung window is skipped, since it would hang Staypaw too. This gives up keeping Staypaw's input apart from a protected game's at all times.
- **Keys already in your window when a stealer takes focus can't be held.** A stealer that joins your window's input with `AttachThreadInput` and then takes focus gets the keys your window hasn't read yet, or they're lost when it lets go. The hook let them through rightly, because your window had focus: tagged keys put each one 0–3 ms before the steal. So the key test goes quiet for 30 ms before each steal it asks for, and checks the keys typed after it. This gives up keys typed in the last milliseconds before a steal, and a test of them.

## M0: Placeholder (as soon as possible)

- [x] Add `LICENSE` (Apache-2.0), `README.md`, `ROADMAP.md` and `AGENTS.md`.
Expand Down Expand Up @@ -93,12 +102,14 @@ What the design must protect. Everything else gets ordinary good design.

## M3: Hold stray keys

- [ ] A `WH_KEYBOARD_LL` hook in Staypaw's own process that holds keys typed between a steal and the restore.
- [ ] Deliver held keys to your window after the restore, and drop them when that window is a protected game.
- [ ] Scan the published exe with Microsoft Defender, since a global keyboard hook can look like a keylogger.
- [x] A `WH_KEYBOARD_LL` hook in Staypaw's own process that holds keys typed between a steal and the restore.
- [x] Deliver held keys to your window after the restore, and drop them when that window is a protected game.
- [x] Scan the published exe with Microsoft Defender, since a global keyboard hook can look like a keylogger.

**Done when:** the steal test counts 0 keys reaching the stealer, and a Defender scan of the published exe is clean.

**Done 2026-10-24:** `StealTests` passed three runs in a row on the real desktop. In each, 45 keys typed during 15 steals were held and replayed, the stealer got none, and your window got all 120. With key holding turned off, the stealer got 36 and the test failed. A Defender custom scan of the published exe (signatures 1.461.184.0) found no threats. The exe is 17,896,448 bytes, 29 KB more than before M3. Idle in the tray, a build from before the last fixes used 40.8 MB working set and 65.1 MB private, against 40.9 and 64.4 before M3. `GuardrailTests` fails on any hook that isn't low-level; a deliberate `WH_KEYBOARD` failed it.

## M4: 1.0

- [ ] A Velopack per-user Setup and a portable zip, built by a release workflow.
Expand Down
Loading