Webhook Notifications

The Problem¶
Your agent runs autonomously (loops, implements, releases) while you are away from the terminal. You have no way to know when it finishes, hits a limit, or when a hook fires a nudge.
How do you get notified about agent activity without watching the terminal?
TL;DR¶
ctx notify setup # configure webhook URL (encrypted)
ctx notify test # verify delivery
# Hooks auto-notify on: session-end, loop-iteration, resource-danger
Commands and Skills Used¶
| Tool | Type | Purpose |
|---|---|---|
ctx notify setup |
CLI command | Configure and encrypt webhook URL |
ctx notify test |
CLI command | Send a test notification |
ctx notify --event <name> "msg" |
CLI command | Send a notification from scripts/skills |
.ctxrc notify.events |
Configuration | Filter which events reach your webhook |
The Workflow¶
Step 1: Get a Webhook URL¶
Any service that accepts HTTP POST with JSON works. Common options:
| Service | How to get a URL |
|---|---|
| IFTTT | Create an applet with the "Webhooks" trigger |
| Slack | Create an Incoming Webhook |
| Discord | Channel Settings > Integrations > Webhooks |
| ntfy.sh | Use https://ntfy.sh/your-topic (no signup) |
| Pushover | Use API endpoint with your user key |
The URL contains auth tokens. ctx encrypts it; it never appears in plaintext
in your repo.
Step 2: Configure the Webhook¶
ctx notify setup
# Enter webhook URL: https://maker.ifttt.com/trigger/ctx/json/with/key/YOUR_KEY
# Webhook configured: https://maker.ifttt.com/***
# Encrypted at: .context/.notify.enc
This encrypts the URL with AES-256-GCM using the same key as the scratchpad
(~/.ctx/.ctx.key). The encrypted file (.context/.notify.enc)
is safe to commit. The key lives outside the project and is never committed.
Step 3: Test It¶
If you see No webhook configured, run ctx notify setup first.
Step 4: Configure Events¶
Notifications are opt-in: no events are sent unless you configure an event
list in .ctxrc:
# .ctxrc
notify:
events:
- loop # loop completion or max-iteration hit
- nudge # VERBATIM relay hooks (context checkpoint, persistence, etc.)
- relay # all hook output (verbose, for debugging)
- heartbeat # every-prompt session-alive signal with metadata
Only listed events fire. Omitting an event silently drops it.
Step 5: Use in Your Own Skills¶
Add ctx notify calls to any skill or script:
# In a release skill
ctx notify --event release "v1.2.0 released successfully" 2>/dev/null || true
# In a backup script
ctx notify --event backup "Nightly backup completed" 2>/dev/null || true
The 2>/dev/null || true suffix ensures the notification never breaks your
script: If there's no webhook or the HTTP call fails, it's a silent noop.
Event Types¶
ctx fires these events automatically:
| Event | Source | When |
|---|---|---|
loop |
Loop script | Loop completes or hits max iterations |
nudge |
System hooks | VERBATIM relay nudge is emitted (context checkpoint, persistence, ceremonies, journal, resources, knowledge, version) |
relay |
System hooks | Any hook output (VERBATIM relays, agent directives, block responses) |
heartbeat |
System hook | Every prompt: session-alive signal with prompt count and context modification status |
test |
ctx notify test |
Manual test notification |
| (custom) | Your skills | You wire ctx notify --event <name> in your own scripts |
nudge vs relay: The nudge event fires only for VERBATIM relay hooks
(the ones the agent is instructed to show verbatim). The relay event fires
for all hook output: VERBATIM relays, agent directives, and hard gates.
Subscribe to relay for debugging ("did the agent get the post-commit nudge?"),
nudge for user-facing assurance ("was the checkpoint emitted?").
Webhooks as a Hook Audit Trail
Subscribe to relay events and you get an external record of every
hook that fires, independent of the agent.
This lets you verify hooks are running and catch cases where the agent absorbs a nudge instead of surfacing it.
See Auditing System Hooks for the full workflow.
Payload Format¶
Every notification sends a JSON POST:
{
"event": "nudge",
"message": "check-context-size: Context window at 82%",
"detail": {
"hook": "check-context-size",
"variant": "window",
"variables": {"Percentage": 82, "TokenCount": "164k"}
},
"session_id": "abc123-...",
"timestamp": "2026-02-22T14:30:00Z",
"project": "ctx"
}
The detail field is a structured template reference containing the hook
name, variant, and any template variables. This lets receivers filter by
hook or variant without parsing rendered text. The field is omitted when
no template reference applies (e.g. custom ctx notify calls).
Heartbeat Payload¶
The heartbeat event fires on every prompt with session metadata and token
usage telemetry:
{
"event": "heartbeat",
"message": "heartbeat: prompt #7 (context_modified=false tokens=158k pct=79%)",
"detail": {
"hook": "heartbeat",
"variant": "pulse",
"variables": {
"prompt_count": 7,
"session_id": "abc123-...",
"context_modified": false,
"tokens": 158000,
"context_window": 200000,
"usage_pct": 79
}
},
"session_id": "abc123-...",
"timestamp": "2026-02-28T10:15:00Z",
"project": "ctx"
}
The tokens, context_window, and usage_pct fields are included when
token data is available from the session JSONL file. They are omitted when
no usage data has been recorded yet (e.g. first prompt).
Unlike other events, heartbeat fires every prompt (not throttled). Use it
for observability dashboards or liveness monitoring of long-running sessions.
Security Model¶
| Component | Location | Committed? | Permissions |
|---|---|---|---|
| Encryption key | ~/.ctx/.ctx.key |
No (user-level) | 0600 |
| Encrypted URL | .context/.notify.enc |
Yes (safe) | 0600 |
| Webhook URL | Never on disk in plaintext | N/A | N/A |
The key is shared with the scratchpad. If you rotate the encryption key,
re-run ctx notify setup to re-encrypt the webhook URL with the new key.
Key Rotation¶
ctx checks the age of the encryption key once per day. If it's older
than 90 days (configurable via key_rotation_days), a VERBATIM nudge
is emitted suggesting rotation.
Worktrees¶
The webhook URL is encrypted with the same encryption key
(~/.ctx/.ctx.key). Because the key lives at the user level, it is
shared across all worktrees on the same machine —
notifications work in worktrees automatically.
This means agents running in worktrees cannot send webhook alerts. For autonomous runs where worktree agents are opaque, monitor them from the terminal rather than relying on webhooks. Enrich journals and review results on the main branch after merging.
Event Log: The Local Complement¶
Don't need a webhook but want diagnostic visibility? Enable event_log: true
in .ctxrc. The event log writes the same payload as webhooks to a local
JSONL file (.context/state/events.jsonl) that you can query without any
external service:
ctx system events --last 20 # recent hook activity
ctx system events --hook qa-reminder # filter by hook
Webhooks and event logging are independent: you can use either, both, or
neither. Webhooks give you push notifications and an external audit trail.
The event log gives you local queryability and ctx doctor integration.
See Troubleshooting for how they work together.
Tips¶
- Fire-and-forget: Notifications never block. HTTP errors are silently ignored. No retry, no response parsing.
- No webhook = no cost: When no webhook is configured,
ctx notifyexits immediately. System hooks that callnotify.Send()add zero overhead. - Multiple projects: Each project has its own
.notify.enc. You can point different projects at different webhooks. - Event filter is per-project: Configure
notify.eventsin each project's.ctxrcindependently.
Next Up¶
Auditing System Hooks →: Verify your hooks are running, audit what they do, and get alerted when they go silent.
See Also¶
- CLI Reference: ctx notify: full command reference
- Configuration:
.ctxrcsettings includingnotifyoptions - Running an Unattended AI Agent: how loops work and how notifications fit in
- Hook Output Patterns: understanding VERBATIM relays, agent directives, and hard gates
- Auditing System Hooks: using webhooks as an external audit trail for hook execution