If you find a security issue, please open a GitHub issue (for non-sensitive matters) or contact the maintainer privately at taiman.jp@gmail.com for anything that should not be public. Reports are best-effort triaged; this is a small independent project.
Just ScreenTime is a passive, read-only monitoring tool. It observes the foreground window and idle time; it does not read memory from other processes, does not inject code, does not hook input, and does not load kernel drivers. This document codifies the invariants that keep it compatible with anti-cheat systems (Riot Vanguard, Easy Anti-Cheat, BattlEye, VAC) and with standard endpoint AV heuristics.
These invariants are non-negotiable. Any future change that would violate them must either be rejected or be gated behind an explicit, documented exception with the reason recorded here.
Scope. They govern the shipped binaries — everything inside the MSIX
package (JustScreenTime.UI, JustScreenTime.Agent, JustScreenTime.Core).
Scripts under scripts/ and tools/ are maintainer-side development aids that
are never packaged, never installed, and never run on a user's machine; some of
them (for example tools/Watch-ProcessStarts.ps1, a WMI process-start tracer
used to diagnose console-window flashes) deliberately do things the app itself
must not do.
The shipped 1.2 binaries must not contain an application feature that sends usage history, diary text, settings, diagnostic logs, or exports to an external service. They also must not add a server/listener, cloud sync, telemetry, analytics, advertising, or remote crash-reporting SDK.
Local bounded error logging is allowed: ErrorLog writes exception details to
per-user LocalApplicationData, rotates each log at approximately 512 KiB, keeps
one previous generation, and never uploads it automatically.
The absence of internetClient in the MSIX manifest is useful package metadata,
but it is not a security boundary for a runFullTrust desktop process.
Therefore, privacy claims must be verified against shipped code and
dependencies, not inferred from capability declarations. Any future network
feature requires explicit user-facing consent, an updated privacy policy, and a
review of the Store listing before release.
When opening a handle to another process, Just ScreenTime must use only
PROCESS_QUERY_LIMITED_INFORMATION (0x1000).
Forbidden access masks:
PROCESS_QUERY_INFORMATION(0x0400) — the older, more privileged formPROCESS_VM_READ(0x0010)PROCESS_VM_WRITE(0x0020)PROCESS_VM_OPERATION(0x0008)PROCESS_CREATE_THREAD(0x0002)PROCESS_DUP_HANDLE(0x0040)PROCESS_SET_INFORMATION(0x0200)PROCESS_SUSPEND_RESUME(0x0800)PROCESS_ALL_ACCESSSYNCHRONIZE
PROCESS_QUERY_LIMITED_INFORMATION is the Microsoft-recommended form for
monitoring tools; it is designed to be permitted against protected processes
and is generally tolerated by user-mode anti-cheat components.
Documented exception — own Agent child process only:
The UI may call Process.WaitForExit / Process.Kill against
JustScreenTime.Agent.exe that this package spawned, which requires
SYNCHRONIZE and PROCESS_TERMINATE on that specific process. This is
allowed solely for graceful shutdown / restart of our own tracker worker.
It must never be used against third-party processes, games, or any PID
obtained from foreground probing.
Never call, P/Invoke, or dynamically load any of these:
Memory / code injection
ReadProcessMemory,NtReadVirtualMemoryWriteProcessMemory,NtWriteVirtualMemoryVirtualAllocEx,VirtualProtectExCreateRemoteThread,NtCreateThreadEx,RtlCreateUserThreadQueueUserAPCagainst another processSetThreadContextagainst another process
Hooks / input interception
SetWindowsHookEx(any WH_* constant)SetWinEventHookwithWINEVENT_INCONTEXTflagsRegisterRawInputDeviceswithRIDEV_INPUTSINK- Low-level keyboard/mouse hooks of any kind
GetLastInputInfo is allowed — it reads a system counter, does not hook,
and is the Microsoft-documented way to measure user idle time.
Debugger APIs
DebugActiveProcessWaitForDebugEvent- Any adjustment of
SeDebugPrivilege
Process enumeration
NtQuerySystemInformationwithSystemProcessInformationEnumProcesses/EnumProcessModulesCreateToolhelp32SnapshotwithTH32CS_SNAPPROCESSorTH32CS_SNAPMODULE
The tracking path — everything that decides what gets recorded — only ever
looks at the foreground PID returned by GetWindowThreadProcessId. It must
never walk the global process list, and the Agent (the process that does the
tracking) contains no enumeration of any kind.
Documented exception — the UI's Agent supervisor. AgentProcessManager
(UI process only) calls System.Diagnostics.Process.GetProcessesByName, which
internally uses NtQuerySystemInformation(SystemProcessInformation). It runs
when the UI starts, on the 30-second watchdog tick, and on stop/restart. It is
needed to answer one question — "is our own Agent alive in this logon
session?" — because a tray-resident UI must notice a dead Agent without
waiting for the next logon. Constraints that keep it inside the spirit of this
section:
- it matches only
JustScreenTime.Agentand only in the current session ID; results for other processes are discarded without any property being read - it never opens a process handle, reads memory, or touches
MainModule - it never runs in the Agent, and never on the path that decides what to record
Process.Kill()is used withoutentireProcessTree, which would build a machine-wide parent/child map
Kernel / driver
- No
NtLoadDriver, noCreateServicewithSERVICE_KERNEL_DRIVER, noZwLoadDriver, no.sysfiles shipped or installed.
Other flagged patterns
LoadLibrary/LdrLoadDllagainst DLLs inside another process- Anything that looks like anti-debug / anti-VM detection
(
IsDebuggerPresentloops,CheckRemoteDebuggerPresent, CPUID VM detection) — anti-cheats flag these even when the caller is benign
Every UWP app's foreground window belongs to ApplicationFrameHost, so
attributing time to the actual app requires looking one level in:
EnumChildWindows on the frame window plus GetClassName to find the
Windows.UI.Core.CoreWindow child, then GetWindowThreadProcessId on that
child. These are window-level APIs — no process handle, no memory access, no
global process list — and the resolved PID goes through the same
PROCESS_QUERY_LIMITED_INFORMATION path as any other. Enumeration is scoped to
the children of one window we are already allowed to see.
Background-session tracking requires listing open top-level windows to decide which apps are "still open but not in the foreground". The following user32 APIs are allowed for this purpose:
EnumWindows— iterates top-level windows by HWND only.IsWindowVisible/IsIconic— query window state (shown / minimized).GetWindowThreadProcessId— returns the owning PID (already permitted in §2).
None of these read process memory or open handles to target processes. Task Manager, accessibility tools (screen readers, magnifiers), and Alt-Tab alternatives call them continuously; anti-cheat systems do not flag them.
Constraints (see JustScreenTime.Core/Win32/BackgroundWindowTracker.cs):
- Enumeration runs at most once per
ProbeEveryNTicksagent ticks (currently every 5 seconds), not every tick. - PID → exe-path resolution still routes through
ProcessInfoCache, so §3's once-per-60-secondsOpenProcessbudget is unchanged by window scanning. - The agent's own exe (any path containing
"JustScreenTime") is filtered from the result set before it reaches the tracker, so Just ScreenTime cannot record itself.
Even with permitted APIs, repeated probing of the same protected process looks like monitoring malware to heuristic scanners. Just ScreenTime enforces:
- At most one
OpenProcessper PID per 60 seconds for successful lookups, viaProcessInfoCache(seeJustScreenTime.Core/Win32/ProcessInfoCache.cs). - At most one
OpenProcessper PID per 30 seconds for failed lookups (negative cache). APROCESS_QUERY_LIMITED_INFORMATIONdenial fromOpenProcessis treated as a permanent-enough signal that the process is protected; we stop asking. - The polling tick itself (currently 1 second) only calls
GetForegroundWindow+GetWindowThreadProcessId+GetLastInputInfo, which do not open handles to any target process.
The agent must not run with elevated privileges it does not need.
- When running interactively (Phase 1 / current): runs as the logged-in user.
- When running as a Windows Service (Phase 4, planned): target is
LocalService, notLocalSystem. This is a Phase 4 decision point and must be revisited before MSIX packaging. SeDebugPrivilegemust never be enabled on the agent's token.
- Unsigned binaries doing process introspection are flagged more aggressively by SmartScreen and Defender. Phase 4 (Store submission) is the point at which an Authenticode signature becomes available via MSIX.
- Until then, users installing from source should expect SmartScreen warnings. This is explicitly not worked around by any "bypass" technique.
The Phase 3 settings page will add an explicit exclusion list: processes
the user names are tracked only by their window title (GetWindowText), with
no OpenProcess call at all. This is the last-resort escape hatch for users
who want to run Just ScreenTime alongside any anti-cheat whose behavior is unknown.
Before merging any change under JustScreenTime.Core/Win32/ or anything that adds
new P/Invoke signatures, verify:
- No new access masks beyond
PROCESS_QUERY_LIMITED_INFORMATION— see Section 1 for the exhaustive list, which has no exceptions. - No APIs from the forbidden list in Section 2, except the one documented exception recorded there (the UI-only Agent supervisor). New exceptions must be written into Section 2 with their scope and constraints before the code merges.
- No new code path opens a handle to the same PID more than once per
minute without going through
ProcessInfoCache. - No new dependency pulls in
System.Diagnostics.Process.MainModule, which internally usesPROCESS_QUERY_INFORMATIONon older frameworks. - Class-level documentation in
WinApi.csis still accurate.