Skip to content

Oathkeeper

Beta v1.0.0Report bugs · Discussions

CI License Platform .NET C++ WiX Inno Setup

Self-applied behavioral accountability for Windows. Block websites, apps, and keywords. Configure escalating punishments. Optionally lock yourself into a time-bound commitment you can't undo.

Think of it as a parental control system you install on your own machine, with the key thrown away for a set period.

Built by Lidprex Labs, a subsidiary of Lidprex. Licensed under GPL v3.


Architecture

Oathkeeper runs as two separate Windows processes:

  • oathkeeper_srv.exe — C++20 engine. Runs as a scheduled task at user logon (/sc onlogon /rl HIGHEST) in the user's interactive session. Handles all low-level OS interception: keyboard hook (WH_KEYBOARD_LL), window title polling (EnumWindows), hosts file manipulation, network interface disabling, screenshots, email sending. Deployed to C:\Windows\System32. Exposes a named pipe for the UI.

  • Oathkeeper.exe — C# WPF frontend (asInvoker, no UAC). Provides the user-facing interface: blocked item lists, settings, pledge setup, email config, system tray. Communicates with the engine via named pipe IPC (\\.\pipe\OathkeeperPipe).

The engine runs independently as a scheduled task and keeps enforcing even if the UI crashes. If the engine task isn't running, the UI works for hosts-file blocking only (via HostsManager), but loses keyboard hook, window monitoring, and all punishments.

The engine runs as a scheduled task rather than a Windows Service because EnumWindows and SetWindowsHookEx(WH_KEYBOARD_LL) require an interactive user session — Service Session 0 cannot access the user's desktop. See docs/ARCHITECTURE.md for the full architecture diagram and security model.


Quick Start

Prerequisites

  • Windows 10/11 (64-bit)
  • .NET 9.0 SDK
  • Visual Studio 2022+ (C++ and .NET workloads)
  • Inno Setup 6 (winget install JRSoftware.InnoSetup)
  • WiX Toolset v7 (dotnet tool install --global wix)

Build

.\cook.ps1            # Build everything (engine + UI + both installers)
.\cook.ps1 engine     # Build C++ engine only
.\cook.ps1 ui         # Build C# UI only
.\cook.ps1 installer  # Build both MSI + EXE installers
.\cook.ps1 verify     # Build + run self-checks
.\cook.ps1 clean      # Clean build artifacts

Install

  1. Run Oathkeeper_Setup_v1.0.0-beta.exe or Oathkeeper_v1.0.0-beta.msi as Administrator
  2. The installer creates the OathkeeperEngine scheduled task (runs at logon)
  3. Launch Oathkeeper.exe from the Start Menu or desktop shortcut

Features

  • Website blocking — via hosts file + 250ms window title scanning + DNS flush
  • Application blocking — process name substring matching via EnumWindows
  • Keyword blocking — window title substring matching (case-insensitive, any position)
  • Adult Shield — 10,000+ adult domain blocklist via hosts file (toggle)
  • 6 punishments — Black Screen, Hardware Beep, Cut Internet, Flash, Keyboard Freeze, Drunk Mouse
  • Punishment escalation — duration scales linearly per offense, capped per type
  • Accountability Email — screenshot + violation report sent to trusted recipient
  • Auto-Apology Protocol — 30s countdown window, confirm to cancel + send apology email
  • Accountability Partner — anti-format notification system
  • Commitment / Pledge system — time-locked with SHA-256 password, time cheat detection
  • Smart Intent Blocker — block specific window titles inside allowed processes
  • Bypass mechanism — secret 10-char code (SHA-256 hashed, never stored in plaintext)

Detection & Response Pipeline

The engine operates a fixed-rate polling loop in ServiceWorkerThread (main.cpp:579):

Cycle Period What Happens
Window poll 250 ms EnumWindows scans visible window titles against blocklist
Violation cooldown 5 s Post-violation sleep prevents duplicate triggers
Config hot-reload ~250 ms GetFileModTime checked every iteration
Time tamper check 30 s Counter >= 120 iterations × 250 ms triggers state-file expiry check
State save 60 s Counter >= 240 iterations × 250 ms persists offense counts to disk
Network integrity 30 s NetworkFilter verifies hosts file still contains Oathkeeper sentinel

Detection Chain

Key press → KeyboardHook (WH_KEYBOARD_LL, 0 latency, callback on same thread)
   ↓ keyword match → erase typed buffer + fire m_onBlocked
   ↓ bypass match (SHA-256 of last 10 chars) → EraseLastChars(10) + fire m_onBypass

Window title → EnumWindows (every 250 ms via WindowMonitor thread)
   ↓ Tier 1: Title keyword match (substring, case-insensitive)
   ↓ Tier 2: Browser process domain match (strip protocol/path, substring on proc name)
   ↓ Tier 3: Smart blocked app (process + title allow/block lists)
   ↓ Tier 4: Process name blocklist (substring, case-insensitive)
   → m_onViolation → PunishmentManager.Execute()

The keyboard hook runs synchronously on the input thread — key processing has zero polling delay. Window monitoring is poll-based with a worst-case detection latency of ~250 ms.

All six offense counters (screen_freeze, hardware_beep, cut_internet, flash, keyboard_freeze, drunk_mouse) increment on every violation regardless of which punishments are currently enabled (PunishmentManager.cpp:147-154).


Punishment Escalation

Duration scales linearly with the cumulative offense count for that punishment type:

duration = min(baseDurationMs × offenseCount, CAP_MS)

Escalation Table

Punishment Base Duration Formula Cap Default Enabled
Screen Freeze 10,000 ms min(10000 × count, 120000) 120 s Yes
Keyboard Freeze 10,000 ms min(10000 × count, 300000) 300 s No
Hardware Beep 2,000 ms (floor) max(2000, 1500 × count) uncapped — confirm this is intentional Yes
Cut Internet 60,000 ms (fixed) fixed 60 s No
Flash 5,000 ms (fixed) fixed 5 s No
Drunk Mouse 10,000 ms (fixed) fixed 10 s No

Open question: Screen Freeze and Keyboard Freeze both cap their escalation. Hardware Beep doesn't, and since offense counters accumulate across every violation regardless of which punishments are enabled/disabled at the time, a counter that's been climbing while Beep was off could produce an unexpectedly long beep sequence once it's re-enabled. Confirm in PunishmentManager.cpp whether this is deliberate (beep is low-severity, an uncapped duration is acceptable) or a missing cap that should be added to match the other two scaling punishments — then remove this note.

Example progression (Screen Freeze):

Offense # Duration Notes
1 10 s First violation
2 20 s
3 30 s
... ...
12 120 s Hits cap at 12th offense

Cooldown between punishment cycles prevents overlap:

cooldown = min(longestActiveDuration + 3000 ms, 600000 ms)

Max cooldown is 10 minutes. During cooldown, violations are counted but no new punishment starts.

Fixed Punishments Detail

  • Hardware Beep: 3 beeps at 1000 Hz / 500 ms, 200 ms gap between (PunishmentManager.cpp:445-448). Minimum duration floor: 2,000 ms.
  • Flash: Toggle overlay alpha between 5↔45 every 150 ms for the configured duration.
  • Drunk Mouse: Random offset ±80 px on both axes, applied every 50 ms (uniform_int_distribution<>(-80, 80)).
  • Pledge Breach: Fixed 2-hour black screen (7,200,000 ms), cannot be bypassed except via EMERGENCY_BYPASS.

See docs/ARCHITECTURE.md for exact per-punishment thread management and cleanup sequences.


Bypass Mechanism

A secret 10-character code provides emergency escape. The code is never stored in plaintext — only its SHA-256 hash exists on disk.

Keyboard Bypass (KeyboardHook.cpp:12)

On every key-down event (digits and letters only, lowercase-converted), the last 10 characters of m_bypassBuffer are SHA-256 hashed and compared against m_bypassHash:

// Pseudocode of the check (KeyboardHook.cpp:221-234)
if (bypassBuffer.length >= 10) {
    tail = bypassBuffer.last(10);
    tailHash = SHA256(tail);
    if (tailHash == m_bypassHash) {
        EraseLastChars(10);   // inject 10 Backspace key events
        fire m_onBypass;
    }
}
  • Buffer truncation: at 50 chars, keep last 30
  • Key filtering: only 0x30-0x39 (digits) and 0x41-0x5A (A-Z) pass through; modifiers, arrows, punctuation, and injected keys are all filtered
  • English keyboard only (primaryLang == 0x0009); Ctrl-held keys bypass the hook entirely

Console Bypass (main.cpp:464-478)

Sent over IPC as EMERGENCY_BYPASS:<sha256(value)>. The engine hashes the submitted value and compares against g_consoleBypassHash. On match:

  1. StopAllPunishments()
  2. SetKeyboardFrozen(false)
  3. Re-initialize PunishmentManager (reset all state)
  4. Delete oath.state
  5. Show "UNLOCKED" overlay for 3 seconds

Both hashes must be set in C:\ProgramData\Oathkeeper\bypass_config.json (keys "keyboard_bypass" and "console_bypass") before the bypass mechanism is usable. The repository contains no default hash values.

Open question, more important than it looks: confirm exactly how these values get into bypass_config.json on a real install. There's a real difference between: (a) the installer/first-run flow generates a fresh random value per install and forces the user to set their own code, versus (b) every build embeds the same default constant, and this file just isn't shown in the public repo. Only (a) actually fixes the original finding that the bypass secret was a fixed, extractable constant. If it's (b), the secret is still fixed and extractable — it's just no longer visible in the docs, which is a documentation fix, not a security fix. State whichever is actually true here once confirmed, then remove this note.


IPC Protocol

The UI communicates with the engine over a Windows Named Pipe at \\.\pipe\OathkeeperPipe. The pipe ACL grants GENERIC_ALL to SYSTEM and Administrators only (IPCServer.cpp). Elevated processes and SYSTEM can connect; non-elevated processes are denied access.

Commands are sent as JSON with "command" and "value" keys, newline-delimited, read 1 byte at a time by the engine. Responses are newline-terminated strings.

Command Reference

Command Value Format Action
BLOCK_WEBSITE domain.com Add to NetworkFilter hosts blocklist
UNBLOCK_WEBSITE domain.com Remove from NetworkFilter
BLOCK_PROCESS process.exe Add to WindowMonitor blocklist
UNBLOCK_PROCESS process.exe Remove from WindowMonitor
BLOCK_KEYWORD keyword Add to WindowMonitor + KeyboardHook
UNBLOCK_KEYWORD keyword Remove from WindowMonitor + KeyboardHook
BLOCK_WEBSITE_AS_KEYWORD domain.com Clean domain, add to WindowMonitor only
UNBLOCK_WEBSITE_AS_KEYWORD domain.com Remove from WindowMonitor
APPLY_SETTINGS JSON payload Bulk toggle punishments via string search
TOGGLE_ADULT_SHIELD true/false Enable/disable 10k adult domain list
TOGGLE_PUNISHMENT type:on/off Toggle individual punishment
SET_EMAIL `recipient sender
SET_ACCOUNTABILITY_PARTNER `email message`
SET_AUTO_APOLOGY `target message`
START_PLEDGE No-op (returns ok)
PLEDGE_BREACH Execute 2-hour black screen in new thread
EMERGENCY_STOP Sets g_running = false, signals stop
RESTART_INTERNET Re-enable all 6 network interfaces
EMERGENCY_BYPASS sha256(value) SHA-256 hash check against console bypass
CONFIRM_APOLOGY Sets m_apologyConfirmed = true
GET_PUNISHMENT_COUNTS Returns all offense counters as JSON
ADD_SMART_BLOCK `proc blockedTitles
REMOVE_SMART_BLOCK process.exe Remove smart block by process
NOTIFY_EMAIL `subject body`
GET_STATUS Returns all punishment enabled states

Network Filtering

Website blocking uses the Windows hosts file at C:\Windows\System32\drivers\etc\hosts:

# === Oathkeeper Blocklist ===
127.0.0.1 example.com
127.0.0.1 www.example.com
127.0.0.1 blocked2.com
127.0.0.1 www.blocked2.com
...
# === End Oathkeeper Blocklist ===

Each domain gets two entries (bare + www.), both resolving to 127.0.0.1.

Integrity Check

The NetworkFilter thread verifies the hosts file every 30 seconds (NetworkFilter.cpp:131). If the sentinel line # === Oathkeeper Blocklist === is missing (e.g., user or AV removed it), the blocklist is re-applied.

DNS Flush

Three-tier fallback strategy (NetworkFilter.cpp:184-197):

  1. Load dnsapi.dll dynamically → call DnsFlushResolverCache via GetProcAddress
  2. If that fails → ipconfig /flushdns via system()
  3. If dnsapi.dll won't load → same system() fallback

The hosts file is also backed up on first modification and restored when Stop() is called (NetworkFilter.cpp:155-164, 200-209).

Adult Shield

Optional 10,000+ domain list loaded from C:\ProgramData\Oathkeeper\data\adult_domains.txt. Added to the hosts file blocklist when toggled on via TOGGLE_ADULT_SHIELD.


Anti-Tamper

Time Cheat Detection

The engine reads expiry_timestamp from C:\ProgramData\Oathkeeper\oath.state and compares it against the current system time ( UTC — confirm GetSystemTime is used, not GetLocalTime; a prior audit found a local/UTC mismatch here that should have been fixed. Once confirmed, remove this note and state "UTC" plainly.) every 30 seconds (main.cpp:541-569). The tolerance window is 1 day past the expiry date:

// Violation condition (simplified)
current > expiry + 1 day

If detected: force-enables screenFreeze + hardwareBeep and fires a "TIME MANIPULATION" punishment.

State Persistence

oath.state stores: expiry timestamp, offense counters, active pledge status. Saved every 60 seconds. On startup, the engine checks for a prior state — if found, it resumes enforcement immediately.

SafeBoot Registration

The engine registers itself under both SafeBoot modes (main.cpp:617-646):

  • HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Minimal\OathkeeperEngineREG_SZ "Service"
  • HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot\Network\OathkeeperEngineREG_SZ "Service"

This ensures the enforcement engine starts even in Safe Mode.

Password Encryption

Email credentials are encrypted with Windows DPAPI (CryptProtectData, CRYPTPROTECT_UI_FORBIDDEN, desc: L"OathkeeperEmail") and stored locally. The encryption is machine-specific — the state file cannot be decrypted on another machine.


Build Matrix (CI)

Job Arch Output
Engine x64 / x86 / arm64 oathkeeper_srv.exe
UI x64 / x86 Oathkeeper.exe + DLLs
Installer x64 MSI + EXE

CI runs on every push via GitHub Actions — see build.yml.


Documentation


License

GNU General Public License v3.0 — see LICENSE.

About

A ruthless, open-source PC blocker that enforces your productivity goals through immediate consequences. If you break your pledge, it instantly locks your screen, triggers loud hardware alerts, or emails a screenshot of your violation to an accountability partner.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Contributors

Languages