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.
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.
- 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.goasserts the above.
Install the latest tagged release with go install:
go install github.com/escoffier-labs/agent-notify/cmd/agent-notify@latestOr build from source:
git clone https://github.com/escoffier-labs/agent-notify.git
cd agent-notify
make install # builds and copies to ~/bin/agent-notifyPrebuilt binaries (linux, macOS, windows for amd64 and arm64) plus a checksums.txt are attached to each release. Download the archive for your platform, verify the checksum, extract, and drop the binary in ~/bin/ or /usr/local/bin/:
tar -xzf agent-notify_*_linux_amd64.tar.gz
install -m 0755 agent-notify_*_linux_amd64/agent-notify ~/bin/agent-notifyConfirm the installed binary:
agent-notify versionSet env vars for the channel(s) you want and run:
export DISCORD_WEBHOOK_URL='https://discord.com/api/webhooks/...'
agent-notify "hello from agent-notify"The explicit subcommand form is equivalent:
agent-notify send "hello from agent-notify"Multiple channels at once:
export DISCORD_WEBHOOK_URL='...'
export TELEGRAM_BOT_TOKEN='...'
export TELEGRAM_CHAT_ID='...'
agent-notify "build finished" # fans out to bothGenerate a starter config:
agent-notify initOr create ~/.config/agent-notify/config.toml manually:
[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:
agent-notify status --json
agent-notify doctor
agent-notify doctor --json--to <names>(explicit, comma-separated) - overrides everything else.--profile <name>- channels from the named profile in config.- Profile in config with
default = true. - All configured channels.
--skip <names> filters from any of the above.
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 SignalGenerate the snippet:
agent-notify hooks print claude-code --profile agent-stop{
"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" }
]
}]
}
}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 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:
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();
});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).
Generate the snippet:
agent-notify hooks print codex --profile agent-stopnotify = ["agent-notify", "--hook", "codex-notify", "--profile", "agent-stop"]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:
#!/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-stopThen point the upstream tool's hook config at my-tool-notify.sh instead.
0- all sends succeeded2- config or input error before any send was attempted3- one or more channel sends failed (other channels still received the message; the per-channel failure count is logged to stderr)
| 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 (ℹ️ / |
| Signal | Plain text. Level emoji prefix. Title on its own line. Tags as [tag1, tag2] footer. |
- 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/).
MIT - see LICENSE.