When an agent is blocked and needs your approval, an answer, or a decision, it pings you through your normal notification settings and waits for your reply. No more coming back to a chat that has been idle for an hour because the agent was too polite to interrupt.
One file, no dependencies (Python 3.8+ standard library only), works from any agent that can run a shell command: Cursor, Claude Code, Codex, OpenClaw, Hermes, or your own scripts.
# the agent runs this and blocks until you tap a button on your Mac/Linux/Windows:
agentping confirm "Run the destructive migration on prod?" && ./migrate.sh
Why
Agents stall silently or, worse, guess. Both are bad. Agent Ping gives them a reliable out-of-band channel to you:
- Approval gates. Pause before anything risky or irreversible.
- Real questions. Get the one answer the agent genuinely cannot infer.
- Sign-off. "The long job finished; what next?"
- Away from the keyboard? Route the ping to your phone and reply from there.
The agent always calls the same four verbs. You decide where the ping lands: desktop, phone, Slack, or a command of your own. That indirection is the whole idea: the notification linkup lives in your config, not in the agent.
Install
npm (one command; gives you the agentping command):
npm install -g agentping
Agent Ping is one Python file, and the npm package is a thin launcher around
it, so you need Python 3.8+ on your PATH (point AGENTPING_PYTHON at a
specific interpreter if python3/python isn't it). No Python packages to install.
From source:
git clone https://github.com/Kevin-Liu-01/Agent-Ping.git
cd Agent-Ping
./install.sh # symlinks `agentping` into ~/.local/bin
# or:
./install.sh --skills # also expose it as a skill to installed agents
Or just drop the single agentping file anywhere on your PATH and
chmod +x it. Verify:
agentping doctor
The four verbs
# 1. notify: fire-and-forget banner
agentping notify "Deploy finished, all checks green" --title "CI"
# 2. ask: free-text answer (printed to stdout)
region="$(agentping ask 'Which region should I deploy to?' --default 'us-east-1')"
# 3. confirm: approve/deny (the exit code IS the answer)
if agentping confirm 'Delete 1,204 orphaned rows?'; then ./cleanup.sh; fi
# 4. choose: pick one of several
plan="$(agentping choose 'How should I fix this?' \
--option 'Patch the call site' \
--option 'Fix the root cause' \
--option 'Leave it for now')"
Reading the result
The exit code is the headline:
| code | meaning |
|---|---|
0 |
delivered / answered / approved / chosen |
20 |
denied, declined, or dismissed |
124 |
timed out (no answer before --timeout) |
2 |
usage error |
3 |
could not deliver |
Add --json for the full picture (recommended for ask/choose so you can
also detect timeouts):
agentping ask 'Pick a name' --json
# {"status":"answered","verb":"ask","channel":"system","response":"orion","error":null}
status is one of delivered | answered | approved | denied | chosen | timeout | cancelled | error.
Common flags
| flag | applies to | meaning |
|---|---|---|
--title |
all | notification title (default Agent) |
--timeout SECONDS |
ask/confirm/choose | wait time; 0 = forever (default 120) |
--channel NAME |
all | send through a specific configured channel |
--urgency low|normal|high |
all | sound / priority hint |
--sound NAME |
all (macOS) | notification sound: a name (Glass, Ping, Sosumi...), auto (a tone per verb), or none |
--default TEXT |
ask | prefill the answer box |
--yes / --no LABEL |
confirm | relabel buttons (labels are the approve/deny words) |
--option VALUE |
choose | a choice; repeat for each (need >= 2) |
Channels (the linkup layer)
By default everything goes to the system channel: a native desktop
notification/dialog (macOS osascript, Linux notify-send/zenity/kdialog,
Windows PowerShell). Zero config required.
To add more destinations, create ~/.config/agentping/config.json
(see config.example.json):
{
"default_channel": "system",
"channels": {
"system": { "type": "system" },
"phone": { "type": "ntfy", "topic": "my-unguessable-topic-x9f2" },
"iphone": { "type": "imessage", "to": "+15551234567" },
"sms": { "type": "sms", "from": "+15550000000", "to": "+15551234567" },
"slack": { "type": "webhook", "url": "https://hooks.slack.com/services/..." },
"discord":{ "type": "webhook", "url": "https://discord.com/api/webhooks/...", "body": "{{\"content\": \"{title}: {message}\"}}" },
"say": { "type": "command", "notify": ["say", "{title}. {message}"] }
}
}
Then target one explicitly: agentping ask "..." --channel phone, or set it as
default_channel (handy on a headless box where the desktop channel can't run).
Logo banners (optional, macOS)
Out of the box, notify uses osascript (no extra setup). If you want the
repo logo in the banner, opt in once:
agentping setup-logo
That installs terminal-notifier if needed, writes config so notify uses the
banner channel while ask/confirm/choose stay on system dialogs, and sends
a test ping. macOS may ask once to allow your terminal app to post notifications.
Notification sounds (macOS)
Give Agent Ping a distinct, recognizable sound so you know a ping is from your
agent. Set sound on the system (or banner) channel:
"system": { "type": "system", "sound": "auto" }
"auto"plays a different tone per verb so you can tell by ear what the agent needs:notify-> Glass,ask-> Ping,choose-> Pop,confirm-> Sosumi.- Set one name for everything (
"sound": "Hero"), a per-verb map ("sound": { "confirm": "Sosumi", "default": "Glass" }), or"none"to silence. - Override per call with
--sound NAME(or--sound none). - Names are macOS sounds in
/System/Library/Sounds(Glass, Hero, Ping, Pop, Sosumi, Submarine, Tink, ...). For a custom sound, drop a.aiffin~/Library/Soundsand use its filename. Sounds require notifications to be allowed for your terminal in System Settings -> Notifications.
This covers the desktop system and banner channels. imessage/sms
use the phone's text tone, and ntfy uses the ntfy app's sound (set there).
Channel types
| type | notify | ask / confirm / choose | notes |
|---|---|---|---|
system |
yes | yes | native desktop; the default |
banner |
yes | no | macOS logo banners via setup-logo (terminal-notifier) |
ntfy |
yes | yes (round-trip) | phone push; you reply from the ntfy app and the agent reads it back |
imessage |
yes | yes (round-trip) | macOS; texts your own number/Apple ID via Messages, reads your reply back from the Messages database. No API key, no fee |
sms |
yes | no | outbound SMS via Twilio (external provider); reaches non-Apple phones and works from a non-Mac host |
webhook |
yes | no | one-way POST; default body {"text": ...} suits Slack. Discord needs a content body template (see config.example.json) |
command |
yes | yes | run any program; placeholders {message} {title} {default} {options} |
The ntfy channel is what makes "the user is away" work: the agent publishes
your question to a topic, you get a push notification, you reply to the topic from
the ntfy app, and the agent reads your reply back off the same topic. Self-host
ntfy or use the public ntfy.sh; pick an unguessable topic name.
The command channel is the escape hatch for anything else: a Telegram CLI,
text-to-speech, a webhook with a custom shape. For ask/choose
the agent reads your program's stdout; for confirm, exit 0 = approve,
nonzero = deny.
Text me: iMessage and SMS
Two ways to land the ping as a normal text message on a phone.
imessage (macOS, free, your own number). Sends through the Messages app
under your own account to any phone number or Apple ID, and it is two-way:
the agent texts you the question and reads your reply back from the local
Messages database, so you can answer from your phone like ntfy.
"iphone": { "type": "imessage", "to": "+15551234567" }
agentping confirm "Deploy v2 to prod?" --channel iphone # reply yes/no from your phone
One-time macOS setup (it fails loud and tells you which of these is missing):
- Sign in to Messages with your Apple ID, and text the recipient once by hand.
- Allow your terminal/agent to control Messages (System Settings -> Privacy & Security -> Automation) so it can send.
- Grant your terminal/agent Full Disk Access so it can read your replies
(for the blocking verbs) and confirm delivery (it reports a Messages "Not
Delivered" as an error instead of a false success). Without it,
notifycan only confirm the message was handed to Messages, not that Apple delivered it.
Set "service": "SMS" to relay green-bubble texts through a connected iPhone
(Text Message Forwarding). SSH sessions can't send.
sms (Twilio, any OS, reaches non-Apple phones). Outbound only -- a real SMS
through Twilio's API, useful from a Linux/Windows or
headless host, or to text someone not on iMessage. Keep the token in the
environment, not the file:
"sms": { "type": "sms", "from": "+15550000000", "to": "+15551234567" }
export TWILIO_ACCOUNT_SID=ACxxx TWILIO_AUTH_TOKEN=xxxx
agentping notify "Nightly backup finished" --channel sms
For a reply over SMS use imessage (macOS) or ntfy (any OS) instead; two-way
SMS would need a public inbound webhook, which this tool deliberately does not run.
No silent fallback. If a channel can't do the verb you asked for (say, the
systemchannel on a machine with no desktop), it fails loudly with a typed error naming the problem. It never quietly routes elsewhere. Choose the channel deliberately.
Configuration: tune everything
Everything about a ping is configurable in ~/.config/agentping/config.json
(override the path with $AGENTPING_CONFIG, or $XDG_CONFIG_HOME). The agent
always calls the same verbs; this file decides where it lands, how it looks
and sounds, how often it fires, and for what it fires.
{
"default_channel": { "notify": "banner", "default": "system" },
"defaults": {
"title": "Agent",
"urgency": "normal",
"sound": "auto",
"timeout": 120
},
"policy": {
"min_urgency": "low",
"min_interval": 0
},
"channels": {
"system": { "type": "system", "sound": "auto" },
"quiet": { "type": "system", "sound": "none", "title": "Background agent" }
}
}
Resolution order for every field: a command-line flag wins, then the chosen
channel's config, then the defaults block, then the built-in fallback. So you
can set a global default and override it per channel or per call.
What you can configure
| where | key | what it controls |
|---|---|---|
| routing | default_channel |
a channel name, or a per-verb map { "notify": "banner", "default": "system" } |
| appearance | title |
the notification title (default Agent) |
| appearance | urgency |
low / normal / high (sound + priority hint) |
| appearance | sound |
macOS sound: a name, a per-verb map, auto, or none (see Notification sounds) |
| appearance | banner channel logo / sender |
the image and the app a logo banner appears to come from |
| timing | timeout |
seconds a blocking verb waits (0 = forever) |
| how often | policy.min_interval |
drop a notify that lands within N seconds of the last delivered one (returns throttled, exit 0). 0 = never throttle |
| for what | policy.min_urgency |
only deliver a notify at or above this urgency; lower ones return suppressed (exit 0) |
title, urgency, sound, and timeout may live in defaults (global) or on
any individual channel. policy applies to notify only -- a blocking
ask/confirm/choose is never suppressed or throttled, because the agent is
waiting on the answer. Run agentping doctor to print the resolved channels,
defaults, and policy.
Using it from an agent
Agent Ping ships a SKILL.md, so agents that support skills
(Claude Code, Codex, Cursor, OpenClaw, ...) can load it and learn when to reach for
it. ./install.sh --skills symlinks this repo into the skill directories it finds
(~/.claude/skills, ~/.codex/skills, ~/.cursor/skills, ~/.openclaw/skills,
~/.config/hermes/skills). For any other agent, point it at the four verbs above
and the exit-code contract; that is the entire interface.
How it works
agent --> shell --> agentping <verb> <message> [flags]
|
| load ~/.config/agentping/config.json (or built-in default)
| pick the channel (--channel, else default_channel)
| deliver; for ask/confirm/choose, block for the reply
v
system : osascript / notify-send / zenity / PowerShell (this machine)
ntfy : POST to a topic, then poll it for your reply (your phone)
imessage: Messages send, then poll chat.db for reply (your phone, macOS)
sms : POST to Twilio (any phone)
webhook: POST once (Slack/Discord)
command: run your program (anything)
|
v
result: stdout (answer) + exit code (+ optional --json)
Security notes
config.jsoncan hold an ntfy token or a webhook URL, so it is git-ignored here; keep itchmod 600and out of version control.- The
commandchannel runs whatever you put in your config. Only put trusted commands there; the message/title text an agent passes is substituted as argv, not run through a shell. - ntfy topics are effectively passwords. Use a long, random topic name (or a self-hosted server with auth) so others can't read your prompts or inject replies.
- The
sms(Twilio) channel needs an auth token. Prefer$TWILIO_ACCOUNT_SID/$TWILIO_AUTH_TOKENin your environment over writing them intoconfig.json. - The
imessagechannel reads~/Library/Messages/chat.dbto collect your reply, which is why the blocking verbs need Full Disk Access. It only ever reads, and only matches replies from the configuredtohandle.
Limitations
- The
systemchannel needs a desktop session; on headless/CI machines usentfy/webhook/commandinstead. - macOS
choosehas no native countdown, so--timeoutis enforced by killing the dialog rather than letting it self-dismiss. - Windows support (
notify/ask/confirm) is best-effort via PowerShell;chooseis not implemented there; useaskor anntfy/commandchannel.
License
MIT. See LICENSE.
No comments yet
Be the first to share your take.