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.
Metadata
Release files for signal-notify 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| signal_notify-0.1.0.tar.gz | 125.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| signal_notify-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 235.0 kB
Release files / signal_notify-0.1.0.tar.gz
| Download URL | signal_notify-0.1.0.tar.gz |
|---|---|
| Size | 125.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2216b7d7455dde628ae5625306dc9e9395699d94ac58b947bcd296cbb7b969fe
|
|
BLAKE2b-256 checksum How to use checksums |
9017348ffaeba68daef1c82e9cd68f2c230649ff6020955a31a451ab0acb1458
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.
Transparency logRelease files / signal_notify-0.1.0-py3-none-any.whl
| Download URL | signal_notify-0.1.0-py3-none-any.whl |
|---|---|
| Size | 109.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
95b7cf43b2fc5bd53de160a3072d597e48d5b294d7ab173df343fba636d17c37
|
|
BLAKE2b-256 checksum How to use checksums |
7110c1396f69f0fb321ca0cab03e0bddca5407d97c7b56e700685ca4be962934
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 3, 2026.
Transparency log