Event-driven background task listeners for Claude Code. Replace polling with real event notifications.
Claude Code uses a request-response model. When waiting for CI, log output, webhooks, or file changes, the typical approach is polling: sleep, check, repeat. This burns turns, wastes tokens, and feels janky.
Claude Code's background task mechanism (run_in_background: true) provides genuine event-driven behavior:
- A background task runs a blocking command that waits for an event
- While waiting, Claude does nothing — no turns burned, no tokens spent
- When the event occurs, the command exits and Claude gets a
<task-notification> - Claude reads the output, reacts, and optionally re-subscribes
This isn't polling dressed up. The OS does the blocking. Claude only wakes up when something actually happens.
┌──────────────────────────┐
│ Background Task │
│ (event source script) │
│ │
Event source ───► │ Blocks until event ───► │ ──► Task completes
(log, webhook, │ Outputs event data │ ↓
CI, file, ...) │ Exits cleanly │ Claude gets notified
└──────────────────────────┘ reads output, reacts,
starts new listener
# From the marketplace
claude plugin marketplace add mividtim/claude-code-event-listeners
claude plugin install el
# Or load directly for a single session
claude --plugin-dir /path/to/claude-code-event-listeners| Command | What it does |
|---|---|
/el:log-tail <file> [timeout] [max_lines] |
Tail a log file, return chunks of output |
/el:webhook [port] |
One-shot HTTP server on localhost |
/el:webhook-public [port] [name] [subdomain] |
One-shot HTTP server with ngrok tunnel (stable vanity URL with subdomain) |
/el:ci-watch <run-id | branch> |
Watch a GitHub Actions run until completion |
/el:pr-checks <pr-number> |
Watch all PR checks until they resolve |
/el:file-change [--root dir] <path-or-glob>... |
Watch file(s) for modifications (supports globs) |
/el:context-sync [project-root] |
Watch CLAUDE.md and .claude/ docs for cross-session sync |
/el:poll <interval> <command> |
Poll a command, fire when output changes |
/el:listen <command...> |
Run any blocking command as an event source |
| Command | What it does |
|---|---|
/el:list |
Show all available sources (built-in + user) |
/el:register <script> |
Register a custom event source |
/el:unregister <name> |
Remove a user-installed source |
You: /el:log-tail api.log
... time passes, log lines arrive ...
<task-notification> → Claude reads the chunk, summarizes errors/warnings
Claude: starts another listener for the next chunk
You: /el:ci-watch my-branch
... minutes pass, Claude does nothing ...
<task-notification> → CI passed! (or failed → Claude investigates)
You: /el:poll 60 "curl -s https://api.example.com/status | jq .count"
... polls every 60s, Claude does nothing ...
<task-notification> → count changed! Claude reads old/new value and reacts
You: /el:webhook-public 9999 gh-review
Claude: immediately reads URL: WEBHOOK_URL=https://xxxx.ngrok.app
registers URL with GitHub
... waits ...
<task-notification> → GitHub POSTed a review event → Claude reads and reacts
The plugin is designed as a platform, not a monolith. Every event source —
including the built-in ones — is a standalone script in sources.d/.
event-listen.sh (dispatcher)
│
├── looks up source type in:
│ 1. ~/.config/claude-event-listeners/sources.d/ (user, wins)
│ 2. <plugin>/sources.d/ (built-in)
│ 3. ~/.claude/plugins/cache/*/*/sources.d/ (community plugins)
│
└── exec's the matching script with remaining args
Built-in sources are not special. They can be overridden, replaced, or used as templates for new ones.
An event source is any executable script that:
- Receives args as
$@ - Blocks until an event occurs
- Outputs event data to stdout
- Exits cleanly
That's the entire contract. Here's a minimal example:
#!/bin/bash
# sources.d/port-ready.sh — Wait for a TCP port to open.
# Args: <host> <port>
set -euo pipefail
HOST="${1:?}" PORT="${2:?}"
while ! nc -z "$HOST" "$PORT" 2>/dev/null; do sleep 1; done
echo "PORT_OPEN=$HOST:$PORT"/el:list # List all sources
/el:register ./my-custom-source.sh # Register a new source
/el:unregister my-custom-source # Remove a user sourceUser sources override built-ins with the same name — so you can replace
log-tail with your own implementation by registering a script named
log-tail.sh.
Write your script following the protocol. Publish it as a Claude Code plugin:
- Create a repo named
claude-code-el-<source-name> - Put your source script(s) in
sources.d/ - Add
.claude-plugin/plugin.jsondeclaringelas a dependency - Users install via the marketplace — el auto-discovers the source
# Users install your source as a plugin:
claude plugin marketplace add yourname/claude-code-el-my-source
claude plugin install el-my-source
# That's it — el discovers sources.d/ automaticallySources can also be registered manually without the marketplace:
git clone https://github.com/you/claude-code-el-my-source.git
/el:register ./claude-code-el-my-source/sources.d/my-source.sh| Source | What it does | Repo |
|---|---|---|
http-poll |
Poll a URL until status/body matches | claude-code-el-http-poll |
coderabbit |
Poll for new CodeRabbit reviews/comments on a PR | claude-code-el-coderabbit |
slack |
Listen for Slack messages via webhook | claude-code-el-slack |
sendgrid |
Receive inbound emails via SendGrid Inbound Parse | claude-code-el-sendgrid |
Want to build one? We'd love to see:
postgres-changes— LISTEN/NOTIFY on a Postgres channeldocker-health— Wait for a container health check to pass/failredis-subscribe— Subscribe to a Redis pub/sub channelmqtt-subscribe— Subscribe to an MQTT topics3-object— Wait for an S3 object to appear
Name your repo claude-code-el-<source-name> and open a PR to add it to
the table above.
- bash (3.2+ for macOS, 4.0+ for Linux)
- python3 (for webhook sources)
- gh CLI (for ci-watch and pr-checks) — install
- ngrok (for webhook-public only) — install
The best way to contribute is to write new event sources. See the
Event Source Protocol above and the scripts in
sources.d/ for examples.
MIT
