A lightweight, cross-platform device security monitor that turns your phone into a remote alarm system — no cloud, no accounts, just a QR code scan.
LeaveSafe runs on your laptop as a terminal dashboard and lets you arm a security monitor from your phone by scanning a QR code. Once armed, it watches the laptop's sensors (charger, lid, USB, screen lock, network, input) and sends instant alerts to your phone if anything changes while you're away.
No internet connection required. Communication is local-only over WebSocket or Bluetooth Low Energy (BLE), secured with a 16-digit Luhn-validated pairing key.
|
|
graph LR
subgraph LAPTOP ["Laptop"]
TUI["TUI Dashboard"]
SM["Sensor Monitor"]
WS["WebSocket / BLE Server"]
TUI --> SM
SM --> WS
end
subgraph PHONE ["Phone / Tablet"]
BR["Browser"]
CTRL["Arm / Disarm"]
ALERT["Alert Cards"]
end
TUI -- "QR Code" --> BR
BR -- "Authenticate" --> WS
CTRL -- "Control" --> WS
WS -- "Real-time Alerts" --> ALERT
style LAPTOP fill:#1a1a2e,stroke:#16213e,color:#e6e6e6
style PHONE fill:#0f3460,stroke:#16213e,color:#e6e6e6
style TUI fill:#533483,stroke:#2c2c54,color:#fff
style SM fill:#533483,stroke:#2c2c54,color:#fff
style WS fill:#533483,stroke:#2c2c54,color:#fff
style BR fill:#e94560,stroke:#c81d4e,color:#fff
style CTRL fill:#e94560,stroke:#c81d4e,color:#fff
style ALERT fill:#e94560,stroke:#c81d4e,color:#fff
graph LR
P["Power / Charger"] ~~~ L["Lid Open / Close"] ~~~ U["USB Devices"]
S["Screen Lock"] ~~~ N["Network / IP"] ~~~ I["Input Events"]
style P fill:#00b4d8,stroke:#0096c7,color:#fff
style L fill:#00b4d8,stroke:#0096c7,color:#fff
style U fill:#00b4d8,stroke:#0096c7,color:#fff
style S fill:#0077b6,stroke:#005f8a,color:#fff
style N fill:#0077b6,stroke:#005f8a,color:#fff
style I fill:#0077b6,stroke:#005f8a,color:#fff
sequenceDiagram
participant L as Laptop
participant P as Phone
L->>L: Generate 16-digit pairing key
L->>L: Display QR code in terminal
P->>L: Scan QR code / open URL
P->>L: Authenticate with pairing key
L->>P: Session token (256-bit)
P->>L: Arm
L->>L: Start monitoring sensors
loop While Armed
L-->>P: Real-time sensor alerts
end
P->>L: Disarm
L->>L: Stop monitoring
Grab the latest pre-built binary from the Releases page.
| Platform | File |
|---|---|
| Windows 64-bit | leavesafe-windows-amd64.exe |
| Linux 64-bit | leavesafe-linux-amd64 |
| macOS Intel | leavesafe-darwin-amd64 |
| macOS Apple Silicon | leavesafe-darwin-arm64 |
macOS blocks unsigned binaries by default. After downloading, run these commands in Terminal to make it executable:
# 1. Grant execute permission
chmod +x leavesafe-darwin-arm64
# 2. Remove the quarantine attribute added by macOS Gatekeeper
xattr -d com.apple.quarantine leavesafe-darwin-arm64
# 3. Run it
./leavesafe-darwin-arm64Note: Replace
leavesafe-darwin-arm64withleavesafe-darwin-amd64if you are on an Intel Mac.
If you still see a "cannot be opened" warning, go to System Settings > Privacy & Security and click Open Anyway next to the LeaveSafe entry.
# Requires Go 1.25+
git clone https://github.com/atakankizilyuce/LeaveSafe.git
cd LeaveSafe
# Build for your current platform
go build -o leavesafe ./cmd/leavesafe
# Or use the Makefile to build all platforms
make allEvery pull request has to pass the same gate, and all of it runs locally:
make fmt # gofmt
make vet # go vet
make lint # golangci-lint (staticcheck, gosec, revive, errcheck, ...)
make web-lint # biome, for web/*.js
make vuln # govulncheck
make test # unit tests
make check # all of the aboveThe CI workflow adds a few things a laptop cannot cover on its own:
| Job | What it does |
|---|---|
format |
gofmt plus a check that go.mod/go.sum are tidy |
typos |
spell check across the repo (typos, configured in _typos.toml) |
lint |
golangci-lint once per target OS — half this codebase sits behind build tags, so a single platform never sees all of it |
test |
unit tests on Linux, Windows and macOS, with coverage reported in the run summary |
e2e |
starts the real binary on each OS and drives the whole user flow over a real WebSocket |
realtrigger |
fires the hardware changes each runner genuinely permits, and records every one it cannot |
sandbox-linux |
boots a real Linux VM under QEMU/KVM and creates real kernel-backed hardware |
frontend |
Biome lint and format check on the embedded web client |
build |
the full five-target release matrix |
vulncheck |
govulncheck against the Go toolchain and dependencies |
ci-success aggregates all of them, so branch protection only needs that one required check. Dependency and action updates arrive weekly through Dependabot.
Every run publishes a coverage matrix naming each sensor that was genuinely triggered and each one that could not be, with the reason. No test fakes hardware and reports success: where a real trigger is impossible, it is skipped and the gap is stated.
In the Linux VM the charger is genuinely unplugged through the test_power kernel module and the real binary reads the change from a real /sys. On Windows real pointer activity is synthesised and the input sensor fires; real IP changes are detected on all three. Everything else skips with a measured reason rather than a fake pass — what no CI environment can reach is listed in docs/manual-verification.md.
Run the layers locally with make test-e2e, make test-realtrigger and make test-sandbox; plain make test stays fast and touches no hardware.
./leavesafe # normal mode
./leavesafe -dev # development mode (serves web assets from filesystem)The terminal dashboard opens with:
- A QR code on the left — scan it with your phone
- A status panel on the right showing armed state, connected clients, active sensors, and the pairing key
- A live log area at the bottom
| Command | Description |
|---|---|
test |
Send a test alert to all connected clients |
trigger <sensor> |
Manually trigger a specific sensor (power, lid, usb, screen, network, input) |
stop / silence |
Stop an active alarm |
history |
Show the last 20 security events |
rotate-key |
Generate a new pairing key and invalidate all sessions |
help |
Show available commands |
Ctrl+C |
Graceful shutdown |
On your phone: Open the URL shown in the terminal (or scan the QR code), authenticate, and use the Arm / Disarm buttons. You can also enable/disable individual sensors and configure alarm settings from the phone UI.
| Feature | Windows | Linux | macOS |
|---|---|---|---|
| Power / Charger | ✅ | ✅ | ✅ |
| Lid open / close | ✅ | ✅ | ✅ |
| USB connect / disconnect | ✅ | ✅ | ✅ |
| Screen lock / unlock | ✅ | ✅ | ✅ |
| Network / IP change | ✅ | ✅ | ✅ |
| Input (keyboard/mouse) | ✅ | ✅ | ✅ |
| Bluetooth Low Energy | ✅ | ✅ | ✅ |
| Local alarm siren | ✅ | ✅ | ✅ |
Settings are stored in a JSON file and persist across sessions:
| OS | Path |
|---|---|
| Windows | %APPDATA%\LeaveSafe\config.json |
| Linux / macOS | ~/.leavesafe/config.json |
You can change settings from the phone UI or by editing the file directly.
All configuration options
| Setting | Default | Description |
|---|---|---|
port |
0 (auto) |
HTTP server port |
max_sessions |
3 |
Maximum concurrent clients |
max_auth_attempts |
5 |
Failed attempts before lockout |
lockout_seconds |
60 |
Lockout duration |
heartbeat_seconds |
15 |
Status broadcast interval |
disconnect_grace_seconds |
30 |
Delay before alarm on full disconnect |
auto_arm_on_lock |
false |
Arm automatically when screen locks |
connection_mode |
wifi |
Transport mode (wifi or bluetooth) |
pin_protection.enabled |
false |
Require PIN to disarm |
alarm.escalation_enabled |
false |
Enable volume escalation levels |
enabled_sensors.* |
varies | Toggle individual sensors on/off |
| Layer | Detail |
|---|---|
| Pairing Key | 16 digits with Luhn check digit, generated fresh each run |
| Session Tokens | 256-bit random hex strings, never reused |
| Rate Limiting | 60-second lockout after 5 failed auth attempts |
| Session Limit | Maximum 3 concurrent connections |
| Disconnect Alarm | Triggers after 30-second grace period if all clients drop while armed |
| Local-Only | No data ever leaves your LAN |
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes
- Open a Pull Request
Run tests before submitting:
go test ./... -v -race