Notify yourself via Signal (Note-to-Self), pure Python: QR device-linking, native send/receive, quiet hours, batching, and file-based alert dedupe.
Project description
signal-notify
Notify yourself via Signal with full push notifications (lock screen + Apple Watch). Messages land in your own Note-to-Self chat. No second phone number, no bot account, no third-party push service required.
A phone bridge for headless AI agents. signal-notify turns the Signal app you already have into a two-way remote interface for an AI agent (Claude Code, Codex, or any custom harness) running on a server or VM. The agent pushes you updates and questions; you reply in your own Note-to-Self chat; the agent reads your reply and acts. It's an alternative to needing a dedicated mobile app (like the Claude app) or a Tailscale-style tunnel just to reach your agent from your phone — the transport is Signal, end-to-end encrypted, reachable from anywhere your phone has signal. See 🤖 AI Agent Bridge below.
100% pure Python, no external Signal client, no Java, no Rust. Linking,
sending and receiving all talk directly to Signal's servers over
HTTPS/WebSockets. Every piece of protocol logic — X3DH/PQXDH, the Double
Ratchet, the post-quantum primitives Signal mandates (round-3 Kyber-1024
and the Sparse Post-Quantum Ratchet / SPQR), protobuf, transport, padding,
session management — is Python. The only compiled dependency is cryptography
(a standard pip wheel, used for X25519/AES/HKDF). No JVM, no subprocess, no
compiled extensions to build.
📖 Quick Links
- Self-Notifications Tutorial: Setting Up Note-to-Self Notifications Step-by-Step
- Home Assistant: Push HA notifications (and camera snapshots) to Signal
- Using, Extending & Customizing: How to drive the API and send richer content
- Caveats & Hard-Won Lessons: The non-obvious traps we hit building this
- Protocol Details & Architecture: Technical Reference & Protocol Design Docs
- Example Configuration: Configuration Schema Guide
🚀 Installation
From a local clone
git clone https://github.com/ricardodeazambuja/signal-notify.git
cd signal-notify
pip install -e .
Directly from GitHub (no manual clone)
pip install "git+https://github.com/ricardodeazambuja/signal-notify.git"
This installs the signal-notify package and CLI straight from the repo (pip
clones it internally). Python dependencies: cryptography>=38
(X25519/AES/HKDF), websockets, qrcode, PyYAML. No external binaries, no
Java, no Rust toolchain — a plain pip install is everything you need,
including the post-quantum crypto.
ARM boards (Raspberry Pi, Jetson, …)
signal-notify itself is a pure-Python (py3-none-any) package; the only
compiled dependency is cryptography, and PyPI ships prebuilt cryptography
wheels for both ARM flavours:
- 64-bit ARM (
aarch64): wheels go back tomanylinux2014(glibc ≥ 2.17), so a plainpip installjust works on 64-bit Raspberry Pi OS, Jetson (JetPack with Python ≥ 3.9), and essentially any aarch64 Linux — nothing is compiled locally. - 32-bit ARM (
armv7l, e.g. 32-bit Raspberry Pi OS): recentcryptographyreleases shipmanylinux_2_31_armv7lwheels, covering Raspberry Pi OS Bullseye (2021) and newer with CPython ≥ 3.9. On older 32-bit images (glibc < 2.31), pip would fall back to a source build (Rust toolchain + OpenSSL headers) — avoid that by using piwheels (preconfigured on Raspberry Pi OS) or the distro package (apt install python3-cryptography, version ≥ 38).
The other dependencies never need a compiler: qrcode and websockets ship
pure-Python wheels (websockets' C speedups are optional), and PyYAML is on
piwheels/apt for 32-bit ARM.
Post-quantum crypto is pure Python. Kyber-1024 and SPQR are implemented in
signalnotify/native/pure/and validated byte-for-byte against Signal's own Rust libraries (see caveat #19). The Rust bindings underrust/are kept only as differential-test oracles; you never need to build them to usesignal-notify. To run the cross-implementation tests against them, build withPYTHON=$(which python) rust/build.sh(needs a Rust toolchain +maturin) and setSIGNALNOTIFY_SPQR_BACKEND=rust/SIGNALNOTIFY_KEM_BACKEND=rust.
⚡ Quick Start
1. Link your device
Scan the terminal QR code using your phone's Signal app (Settings → Linked Devices → Link New Device):
signal-notify link -n "server-alerts"
2. Send a test message
signal-notify send -m "Deployment complete! ✅"
3. Send to another recipient
signal-notify send -m "Hello there" --to "+15551234567"
4. Receive replies (bidirectional, incl. Note-to-Self)
# Drain pending messages once (ideal for cron): prints each message.
signal-notify receive
# Only the replies you typed in your own Note-to-Self chat on your phone:
signal-notify receive --note-to-self
Receiving decrypts messages natively (responder X3DH/PQXDH + Double Ratchet). The Note-to-Self channel works because when you type in your own Note-to-Self chat, your phone broadcasts a sync transcript to this linked device — that sync transcript is the inbound half of the loop.
🤖 AI Agent Bridge
Give a headless agent (Claude Code, Codex, or your own harness) a two-way channel to your phone over Signal — no dedicated app, no tunnel.
Connect your Signal app (one time)
pip install -e . # pure Python; post-quantum crypto included
signal-notify link -n "my-agent" # prints a QR
On your phone: Signal → Settings → Linked Devices → Link New Device → scan the QR.
signal-notify doctor confirms the connection. Your account state lives (owner-only)
under ~/.local/share/signal-notify/data/ — back it up before re-linking.
Two building blocks
from signalnotify import send_message, receive
# 1. Push a message to your phone (fire-and-forget)
send_message("Build finished ✅ — deploy? (yes/no)")
# 1b. Push a file too — a plot, a screenshot, a log (encrypted client-side).
send_message("today's error rate", attachments=["plot.png"])
# 2. Wait for your reply, typed in Note-to-Self on the phone
# drain=False keeps ONE connection open and returns as soon as you reply.
reply = next((m.body for m in
receive(drain=False, max_messages=1, wait=300) if m.note_to_self), None)
if reply and reply.lower().startswith("y"):
deploy()
Ready-to-run examples
examples/agent_chat.py— send-then-wait. Exposesnotify(text)andask(prompt, timeout)(send a question, block until you reply on the phone). Run it for an interactive terminal ↔ phone chat.examples/agent_daemon.py— always-on. A persistent listener that dispatches every Note-to-Self command you send from your phone to a handler (wire your agent/LLM in). This is your phone "remote."
python examples/agent_chat.py # you type here → phone → you reply on phone → prints here
python examples/agent_daemon.py # then Note-to-Self "status" / "echo hi" from your phone
Why this works: your phone (the primary device) and this linked device are two devices on one Signal account, so anything you type in Note-to-Self syncs to the agent, and anything the agent sends to Note-to-Self shows up (and notifies) on your phone. It's end-to-end encrypted and reachable anywhere your phone has Signal. See Using, Extending & Customizing for the full API.
🏠 Home Assistant
Signal makes a great Home Assistant notification channel: end-to-end
encrypted, free, no bot number, and it lands on your lock screen. The
simplest hookup is a shell_command:
shell_command:
signal_notify: "signal-notify send -m '{{ message }}'"
…called from any automation with service: shell_command.signal_notify.
The Home Assistant guide covers a first-class
notify.signal service, sending camera snapshots as encrypted
attachments, alert batching/quiet hours via run, and a two-way daemon that
turns Note-to-Self replies into HA service calls.
🚨 Config-Driven Alert Monitoring (run)
signal-notify includes a powerful diff-based alerting engine designed for cron jobs. It compares an active alert list against a notified history file, pushing only new alerts and clearing resolved ones to ensure you get notified exactly once per alert instance.
Copy the template and edit your own copy:
cp notify.example.yaml notify.yaml # then put your number/recipients in notify.yaml
signal-notify run --config notify.yaml --active active.txt --notified notified.txt
Note:
notify.yaml(and*.local.yaml) is git-ignored on purpose — it holds your personal number / recipients, so it never lands in the repo. Only the placeholdernotify.example.yamlis tracked. Your Signal account keys live under~/.local/share/signal-notify/dataand are likewise never committed.
Example notify.yaml
channels:
signal:
enabled: true
note_to_self: true
🐍 Python API Usage
You can also import and use the library directly inside your Python applications:
from signalnotify import send_message, send
# Send a single message to Note-to-Self
send_message("Server backup successful! ✅")
# Send batched alerts with a header
send(["disk 92%", "load 14.2"], header="host01")
Receiving (bidirectional)
from signalnotify import receive, receive_note_to_self
# One-shot drain of whatever is queued:
for m in receive():
print(m.timestamp, m.source, m.body)
# Just the commands you typed into your Note-to-Self chat on your phone:
for m in receive_note_to_self():
handle_command(m.body)
Each item is a Message (timestamp, body, source, source_name,
note_to_self, group_id, raw, attachments). receive() is one-shot — call it once per
cron cycle. A thin listen(callback) loop is also provided for daemon-style use.
The server acks-and-deletes on receive; pass journal=True to have every
parsed message appended to <account>.inbox.jsonl before it is acked, so a
crash downstream can never lose one. Envelopes that cannot be decrypted (e.g.
sealed-sender messages from contacts) are always preserved raw in
<account>.undecryptable.jsonl instead of being destroyed by the ack.
⚠️ Gotchas & Tips
- Notification Body Previews: If you receive notifications but cannot see the message body, make sure you configure your phone's notifications preview setting. See the Tutorial for details.
- Session De-authorization: Signal will de-authorize linked secondary devices if they remain unused for a prolonged period. If notifications stop delivering, simply re-run the
linkcommand and scan the QR code again. - Security: Device key credentials and session files are saved securely under owner-only read/write permissions (
0o600) inside~/.local/share/signal-notify/data(override withSIGNALNOTIFY_DATA_DIR; an account store found at the legacy pre-1.0 location is migrated here automatically, once).
🗺️ Future Developments
The two post-quantum primitives Signal mandates — round-3 Kyber-1024 and
SPQR — were originally in-process Rust (pyo3) bindings. They are now
reimplemented in pure Python under signalnotify/native/pure/
(mlkem768.py, kyber1024.py, spqr.py and helpers), making signal-notify
100% pure Python with zero compiled extensions to build — no Rust
toolchain, no maturin, no platform-specific wheels. The pure implementation
is the default; the Rust bindings under rust/ are retained only as
differential-test oracles (keys, ciphertexts, wire messages and serialized
state are byte-compatible, so a live session can even move between the two
mid-conversation). See caveat #19 for the security
posture (notably: the pure code is not constant-time).
Remaining ideas: optional acceleration of the erasure-coding hot path (only material under heavy packet loss), and PyPI publishing.
💖 Credits & Acknowledgments
This project is inspired by and built upon the design of these outstanding open source repositories:
- AsamK/signal-cli — Historical reference for the account-store layout and provisioning patterns; signal-notify no longer uses or shares state with it.
- signalapp/libsignal — The cryptographic protocol specification defining the X3DH key agreement and Double Ratchet systems.
- signalapp/SparsePostQuantumRatchet — Signal's post-quantum triple-ratchet crate.
signalnotify/native/pure/spqr.pyis a faithful pure-Python port of v1.5.1 (byte-compatible state and wire format); therust/spqr_pyoracle binds the original for differential testing.
License
AGPL-3.0-or-later — see LICENSE. Same license as the Signal source
it interoperates with and derives from: libsignal, Signal-Android,
Signal-Server, and the SparsePostQuantumRatchet crate — of which
signalnotify/native/pure/spqr.py is a direct pure-Python port (the optional
rust/spqr_py test oracle still binds the crate itself) — are all AGPL-3.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file signal_notify-0.1.0.tar.gz.
File metadata
- Download URL: signal_notify-0.1.0.tar.gz
- Upload date:
- Size: 125.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2216b7d7455dde628ae5625306dc9e9395699d94ac58b947bcd296cbb7b969fe
|
|
| MD5 |
eb8ff718dc16db11484b75c9f63e882d
|
|
| BLAKE2b-256 |
9017348ffaeba68daef1c82e9cd68f2c230649ff6020955a31a451ab0acb1458
|
Provenance
The following attestation bundles were made for signal_notify-0.1.0.tar.gz:
Publisher:
wheels.yml on ricardodeazambuja/signal-notify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
signal_notify-0.1.0.tar.gz -
Subject digest:
2216b7d7455dde628ae5625306dc9e9395699d94ac58b947bcd296cbb7b969fe - Sigstore transparency entry: 2060738657
- Sigstore integration time:
-
Permalink:
ricardodeazambuja/signal-notify@fd976b6e660924a727aeaf0485494c1c0f975246 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ricardodeazambuja
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@fd976b6e660924a727aeaf0485494c1c0f975246 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file signal_notify-0.1.0-py3-none-any.whl.
File metadata
- Download URL: signal_notify-0.1.0-py3-none-any.whl
- Upload date:
- Size: 109.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95b7cf43b2fc5bd53de160a3072d597e48d5b294d7ab173df343fba636d17c37
|
|
| MD5 |
78c3682676cd426a6bdc9ded0dea1e14
|
|
| BLAKE2b-256 |
7110c1396f69f0fb321ca0cab03e0bddca5407d97c7b56e700685ca4be962934
|
Provenance
The following attestation bundles were made for signal_notify-0.1.0-py3-none-any.whl:
Publisher:
wheels.yml on ricardodeazambuja/signal-notify
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
signal_notify-0.1.0-py3-none-any.whl -
Subject digest:
95b7cf43b2fc5bd53de160a3072d597e48d5b294d7ab173df343fba636d17c37 - Sigstore transparency entry: 2060739052
- Sigstore integration time:
-
Permalink:
ricardodeazambuja/signal-notify@fd976b6e660924a727aeaf0485494c1c0f975246 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/ricardodeazambuja
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
wheels.yml@fd976b6e660924a727aeaf0485494c1c0f975246 -
Trigger Event:
workflow_dispatch
-
Statement type: