https://github.com/escoffier-labs/agent-notify
Privacy-first push notifications for AI coding agents to Discord, Telegram, and Signal. Zero telemetry, single Go binary.
https://github.com/escoffier-labs/agent-notify
ai-agents claude-code cli discord go notifications signal telegram
Last synced: 13 days ago
JSON representation
Privacy-first push notifications for AI coding agents to Discord, Telegram, and Signal. Zero telemetry, single Go binary.
- Host: GitHub
- URL: https://github.com/escoffier-labs/agent-notify
- Owner: escoffier-labs
- License: mit
- Created: 2026-05-23T19:52:44.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-10T06:52:29.000Z (14 days ago)
- Last Synced: 2026-06-10T08:21:44.815Z (14 days ago)
- Topics: ai-agents, claude-code, cli, discord, go, notifications, signal, telegram
- Language: Go
- Size: 55.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# agent-notify
A privacy-first notification dispatcher for AI coding agents and any other host process. Sends to Discord, Telegram, and Signal - no telemetry, no third-party push infrastructure, no Anthropic / OpenAI involvement.
Built for engineers running an always-on agent stack who want push notifications when the agent finishes or needs input, without routing real-time session activity through a vendor's notification service.
## Why this exists
If you set `DISABLE_TELEMETRY=1` to keep your agent harness from phoning home, you've also disabled the harness's built-in mobile push feature (which routes through the same telemetry plumbing). `agent-notify` gives you the same UX with zero data flow you didn't ask for: messages go from your machine directly to Discord's API, the Telegram Bot API, or your self-hosted Signal CLI - and nowhere else.
## Privacy posture
- No telemetry endpoints. Ever.
- No update checks at startup or runtime.
- No persistent state. No state file. No log file. No cache.
- Outbound HTTP only to channel URLs you configured.
- Test `cmd/agent-notify/privacy_test.go` asserts the above.
## Install
Install the latest tagged release with `go install`:
```bash
go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latest
```
Or build from source:
```bash
git clone https://github.com/escoffier-labs/agent-notify.git
cd agent-notify
make install # builds and copies to ~/bin/agent-notify
```
Prebuilt binaries (linux, macOS, windows for amd64 and arm64) plus a `checksums.txt` are attached to each [release](https://github.com/escoffier-labs/agent-notify/releases). Download the archive for your platform, verify the checksum, extract, and drop the binary in `~/bin/` or `/usr/local/bin/`:
```bash
tar -xzf agent-notify_*_linux_amd64.tar.gz
install -m 0755 agent-notify_*_linux_amd64/agent-notify ~/bin/agent-notify
```
Confirm the installed binary:
```bash
agent-notify version
```
## Quickstart (no config file)
Set env vars for the channel(s) you want and run:
```bash
export DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/...'
agent-notify "hello from agent-notify"
```
The explicit subcommand form is equivalent:
```bash
agent-notify send "hello from agent-notify"
```
Multiple channels at once:
```bash
export DISCORD_WEBHOOK_URL='...'
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='...'
agent-notify "build finished" # fans out to both
```
## Config file (when you outgrow env-only)
Generate a starter config:
```bash
agent-notify init
```
Or create `~/.config/agent-notify/config.toml` manually:
```toml
[channels.tg-personal]
type = "telegram"
bot_token_env = "TELEGRAM_BOT_TOKEN"
chat_id_env = "TELEGRAM_CHAT_ID"
[channels.discord-main]
type = "discord"
webhook_url_env = "DISCORD_WEBHOOK_URL"
[channels.signal-personal]
type = "signal"
url_env = "SIGNAL_CLI_URL"
from_env = "SIGNAL_FROM"
to_env = "SIGNAL_TO"
[profiles.agent-stop]
channels = ["tg-personal", "discord-main"]
default = true
[profiles.error]
channels = ["tg-personal", "discord-main", "signal-personal"]
prefix = "đ¨ "
```
Secrets stay in env vars (the config references env-var names, not literal tokens).
Validate the wiring without sending a live notification:
```bash
agent-notify status --json
agent-notify doctor
agent-notify doctor --json
```
## Routing precedence
1. `--to ` (explicit, comma-separated) - overrides everything else.
2. `--profile ` - channels from the named profile in config.
3. Profile in config with `default = true`.
4. All configured channels.
`--skip ` filters from any of the above.
```bash
agent-notify "build done" # default profile or all channels
agent-notify --profile error "5 critical alerts" # error profile
agent-notify --to tg-personal "ack" # only Telegram
agent-notify --profile error --skip signal "minor" # error profile minus Signal
```
## Hook integrations
### Claude Code (`~/.claude/settings.json`)
Generate the snippet:
```bash
agent-notify hooks print claude-code --profile agent-stop
```
```json
{
"hooks": {
"Stop": [{
"hooks": [
{ "type": "command", "command": "agent-notify --hook claude-code-stop --profile agent-stop" }
]
}],
"Notification": [{
"hooks": [
{ "type": "command", "command": "agent-notify --hook claude-code-notification --profile agent-stop" }
]
}]
}
}
```
### Claude Desktop
Not applicable - Claude Desktop does not expose a hook surface that runs local commands. Use the Claude Code integration instead, or call `agent-notify` from a shortcut/script bound to whatever event you care about.
### OpenClaw
OpenClaw has its own multi-channel delivery built in, so you typically would not wire `agent-notify` for OpenClaw's own events. If you do want to use it (e.g., uniformity across all your agents), call it from a plugin's `agent_end` hook:
```typescript
api.on("agent_end", async (event, ctx) => {
const proc = spawn("agent-notify", ["--hook", "custom", "--profile", "agent-stop"]);
proc.stdin.write(JSON.stringify({
title: "OpenClaw session ended",
body: `Session ${event.sessionId} done`,
source: "openclaw",
}));
proc.stdin.end();
});
```
### Hermes Agent
Same pattern as OpenClaw - wire `agent-notify` to whichever scheduled-task or session-end hook Hermes exposes in your version. Pass canonical JSON via stdin and use `--hook custom` (the default).
### Codex CLI (`~/.codex/config.toml`)
Generate the snippet:
```bash
agent-notify hooks print codex --profile agent-stop
```
```toml
notify = ["agent-notify", "--hook", "codex-notify", "--profile", "agent-stop"]
```
## Adding a custom hook source (escape hatch)
If a built-in adapter ever breaks because an upstream tool changes its event schema, write a small shell wrapper that extracts the fields you want and pipes canonical JSON to `agent-notify`:
```bash
#!/usr/bin/env bash
# my-tool-notify.sh - wrapper for some-future-agent
event=$(cat)
body=$(echo "$event" | jq -r '.message_field // "(no message)"')
title=$(echo "$event" | jq -r '.title_field // "MyTool"')
jq -n --arg t "$title" --arg b "$body" \
'{title: $t, body: $b, source: "my-tool"}' \
| agent-notify --profile agent-stop
```
Then point the upstream tool's hook config at `my-tool-notify.sh` instead.
## Exit codes
- `0` - all sends succeeded
- `1+` - number of channel sends that failed (other channels still received the message)
- `2` - config or input error before any send was attempted
## Channel formatting
| Channel | Format |
|---------|--------|
| Discord | Embed with title + body. Color by level (info=blue, warn=yellow, error=red, success=green). Tags as inline fields. Source as footer. |
| Telegram | Markdown V2. Level emoji prefix (âšī¸ / â ī¸ / đ¨ / â
). Title bolded. Tags as italicized footer. |
| Signal | Plain text. Level emoji prefix. Title on its own line. Tags as `[tag1, tag2]` footer. |
## Limitations (v1)
- No retry queue. A rate-limited or down channel means dropped notification.
- No templating. The canonical message goes through as-is.
- Three channels only. Adding more is straightforward (one file in `internal/channels/`).
## License
MIT - see [LICENSE](LICENSE).