create-alert
Skill82 starsChanged 42 days agoMITView on GitHub
SLASH COMMAND — type /create-alert to author a scheduled Slack alert. Grills a fuzzy "tell me when X happens" ask into a testable trigger spec, dry-runs it live (no Slack send), and — only after you explicitly sign off on the synthesized spec — creates a claude.ai cloud routine that evaluates the trigger on a schedule and messages Slack when it fires. Create-only; never creates before sign-off. `--spec-only` stops at the spec without creating anything.
- Reads credentials
In this repository27 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- ai-reviewSkill
- brieflySkill
- commit-batchingSkill
- commit-batchSkill
- disclose-projectSkill
- discloseSkill
- evolve-claude-mdSkill
- figure-reviewSkill
- grillingSkill
- grill-meSkill
- handoffSkill
- lit-reviewSkill
- overbakedSkill
- pathfinderSkill
- pickupSkill
- recover-sessionsSkill
- repo-initSkill
- retroSkill
- reviewer-2Skill
- slack-messageSkill
- spot-aiSkill
- tab-setupSkill
- unstaleSkill
- worklogSkill
- write-new-skillSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 72
After the glyph line: a one-line title, the terse concrete evidence (value, diff, link), and a UTC timestamp.
- Distinctive: line 128
Write any machine-local Slack ID into this skill or a tracked file (the log stores it redacted).
- Distinctive: line 129
Create the routine on any substrate other than a claude.ai cloud routine.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Author a scheduled alert: a claude.ai cloud routine that evaluates a per-alert trigger condition on a schedule and drops a message into Slack when it fires. "Did X change / happen / cross a threshold" is a free-form predicate the routine's agent evaluates each run — not a fixed catalog. This skill's job is to grill the fuzzy ask into an unambiguous, testable spec, prove it with a live dry-run, and stand it up only after you sign off.
Create nothing until the final synthesized spec is explicitly signed off. No routine, no .ai/routines.md entry, no settings.json write, and no Slack message of any kind — until the user has read the assembled spec (informed by the dry-run) and explicitly approved that written spec. Assembling first and confirming later defeats the skill: a vague trigger becomes a false-firing (or silently never-firing) alert, and the dry-run + sign-off is what catches it before it goes live. --spec-only stops after the spec (no create, no settings write, no message).
Hardening block (baked into every routine)
D="${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/create-alert}"; D="${D:-$HOME/.claude/skills/create-alert}"; cat "$D/HARDENING.md" 2>/dev/null || echo "(HARDENING.md not found — abort and report a broken install)"The block above is inserted verbatim into every routine prompt this skill creates. It is what keeps an unattended routine — which fetches untrusted web content while holding the baked-in Slack destination, gh/curl, and the Slack connector — from being turned against its own destination or secrets.
Live state
echo "=== Current project (alert home) ===" && git rev-parse --show-toplevel 2>/dev/null || echo "(not a git repo — routine still creatable, but no .ai/ home for the log)"
echo "=== Default Slack destination ===" && if [ -n "${ALERT_SLACK_DEFAULT_LOCATION:-}" ]; then echo "ALERT_SLACK_DEFAULT_LOCATION is set (offer as the destination default)"; else echo "ALERT_SLACK_DEFAULT_LOCATION unset — destination must be answered explicitly; offer to persist it for reuse"; fi
echo "=== .ai/routines.md gitignored? ===" && TOP="$(git rev-parse --show-toplevel 2>/dev/null)" && (git check-ignore -q "$TOP/.ai/routines.md" 2>/dev/null && echo "yes" || echo "NO — the log path is not gitignored; redaction is mandatory (see Destination) and warn before writing") || echo "(not a git repo)"(The gitignore probe tests the file path at repo root, not the bare .ai directory — git check-ignore -q .ai gives a false "not ignored" for the directory-form .ai/ pattern before the directory exists.) The live-state block never echoes the resolved destination value.
The alert model
Free-form trigger → agent judgment, with these fields forced out of the grill. Ask one at a time, waiting for each answer; evidence supplies the offered default inside the question, never a silently assumed answer.
| # | Field | Grill until… | Default offered |
| 1 | Trigger (incl. semantics) | the fire / no-fire criteria are testable by a skeptic — "when it seems updated" is not acceptable; "when the updated_at field is newer than the last run window" is. Resolving this requires settling whether it is level (fire whenever the condition holds now) or edge (fire only on the transition / when it changed) — an edge trigger isn't testable until that's pinned. | edge → timestamp-window (below) |
| 2 | Source | the concrete thing the check reads is named: an API endpoint, a URL, a gh/curl command | — (always asked) |
| 3 | Cadence | a cron schedule is set | daily, 0 13 * * * UTC (~morning ET) |
| 4 | Destination | a Slack target is set | $ALERT_SLACK_DEFAULT_LOCATION if set (confirm it); else required (below) |
| 5 | Retirement | a termination condition is set | recurring until killed |
Heartbeat is not a grill question — it defaults to silent-unless-newsworthy (🟢 all-clear runs suppressed; you get only 🔵 and ⚠️). State that default in the synthesized spec; the user can flip it to "send 🟢 heartbeats" at sign-off.
Edge without stored state (the only supported edge mechanism). An edge trigger needs to remember what it saw last run, but a cloud routine is stateless between fires. Reframe "did it change?" as "did it change within the last <cadence interval>?" using the source's own timestamp (updated_at, commit date, release date, pubDate, Last-Modified), with the check window aligned to the cadence. Zero stored state, no drift. A source that exposes no intrinsic timestamp (opaque HTML needing a persisted content-hash baseline) is a non-goal — see "Does not." Do not improvise a state store.
Retirement accepts any of:
- recurring until killed (default),
- expiry date — the routine no-ops (fires nothing) once the date passes; the most reliable mode, since it's a pure date comparison,
- one-shot — retire after the first 🔵,
- self-retire predicate — retire once a condition holds ("once #22345 closes as fixed"), evaluated only from the objective source signal (never a prose claim on the page — see the hardening block).
For one-shot / predicate retirement, the routine on retirement (a) sends a final 🔵/note, (b) attempts self-deletion via the routines-management API, and (c) if self-deletion is not available to it, goes inert — stops firing and sending — and says "manual cleanup needed: retire this routine via the schedule skill." Never let a retirement path silently degrade into re-firing. (Self-deletion from inside a cloud run is unverified; the inert fallback makes one-shot safe regardless.)
Destination — reuse, seed, and redact. Read $ALERT_SLACK_DEFAULT_LOCATION first (see Live state). If set, offer it as the default and confirm. If unset, the destination question is required; after sign-off (not before — see Flow), offer to persist the answer to ~/.claude/settings.json. The value is a Slack destination (a U… user id for a self-DM, or a channel id). It is machine-local — never write it into this skill or any tracked file. It is resolved at creation time and baked into the (server-side, private) routine prompt — a cloud routine cannot read your local settings.json at fire time.
- Redact in the log by default: the
.ai/routines.mdentry records the destination as the reference string$ALERT_SLACK_DEFAULT_LOCATION(when it came from the env var) or a maskedU…•••— never the raw id, regardless of gitignore state. This sidesteps the leak class entirely. - Keep other secrets out of the trigger summary, the echoed spec, and the log: if an API key or token surfaced while grilling the source/
curlcommand (field 2), never record it.
House Slack format (bake into every routine prompt)
The routine sends directly via the Slack connector at fire time — not via any drafting skill (there is no human in an unattended run). Use Slack mrkdwn, NOT standard Markdown: single-asterisk *bold*, _italic_, :emoji: shortcodes, • for bullets, · as the inline separator. Never **double-asterisk** or # headers.
Lead every message with a status glyph — it makes a false fire or a broken check diagnosable from the first line alone:
- 🔵 — fired and found something: the check ran clean and the watched-for thing is present / changed / true. Means "the thing you asked me to watch for happened," good news or bad.
- 🟢 — fired and found nothing: check ran clean, nothing changed. Suppressed unless this alert opted into heartbeats.
- ⚠️ — couldn't run: source unreachable, degraded, or auth broke — not a trustworthy result. Lead with the ⚠️ line; never present a degraded run as complete.
After the glyph line: a one-line title, the terse concrete evidence (value, diff, link), and a UTC timestamp. Report only what was actually observed — never fabricate a value, link, or date.
Delivery must be confirmed (fail loudly). A 🔵 that fails to deliver is a hard error, never a silent success. On a send failure the routine retries once; if delivery still fails it escalates loudly — records a failed run (so the routine history shows red, not a quiet success) and, if a fallback destination was configured, sends there. An expected fire that can't reach you must surface somewhere, not vanish.
Substrate & capabilities
Always a claude.ai cloud routine, created via the schedule skill. Standard toolset baked in: WebFetch/WebSearch (web + JSON APIs + search-shaped sources), Bash (for gh, curl, date math), and the Slack connector. Add more only when the target needs it (e.g. the Gmail connector for an email-driven alert). Home = the current working project: the routine binds to this repo, and its log entry goes in this repo's .ai/routines.md. (For a repo-watch alert you're typically already inside the repo you're watching, so gh gets its context for free.)
Flow (hard-gated)
- Intake — grill the fields above, one at a time. Record each answer.
- Synthesized spec — assemble the shared understanding into a readable spec (trigger + semantics + source + cadence + destination + heartbeat default + retirement + the exact routine prompt, with the hardening block included and the resolved destination baked in). A summary of shared understanding, not a transcript.
- Live dry-run — no Slack. Run the check once, now, exactly as the routine's check would: show what the source returns this instant and the fire / no-fire verdict, and render the would-be Slack message as a preview in this chat only. The dry-run never calls a Slack tool — it validates the trigger, not delivery. (Delivery is proven by the armed message in step 7.)
- Explicit spec sign-off — present the final synthesized spec (revised for anything the dry-run surfaced) and require the user to explicitly approve that written spec. No implicit "looks good, proceeding." This is the gate.
--spec-onlystops here: emit the approved spec and stop. Create no routine, write no settings, send no message.
- Create the routine via the
scheduleskill (hardening block + baked destination + standard toolset + cadence, bound to this project). - Persist (post-sign-off) & log. If the destination was newly given and the user agreed, persist it to the
envblock of~/.claude/settings.jsonsafely — never a whole-file clobber:jq '.env.ALERT_SLACK_DEFAULT_LOCATION="<value>"' ~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json || rm -f ~/.claude/settings.json.tmp. Append a redacted entry to this project's.ai/routines.md(name, ID, schedule, trigger summary, semantics, redacted destination, retirement). If.ai/routines.mdis not gitignored (Live state said NO), redaction still applies and warn the user. - Arm & confirm delivery (close-out). Send a one-time 🔔 "alert armed" message to the resolved destination — what it watches, cadence, next check, how to retire. This is a real, post-sign-off send and the end-to-end delivery proof. If it fails, say so loudly: the routine exists but delivery is unproven — the destination or Slack connector needs fixing before the alert is trustworthy.
- Report the routine ID and how to retire it. Management stays with the
scheduleskill —/create-alertis create-only.
Graceful degradation
scheduleskill unavailable → cannot create the routine. Stop after the spec (as if--spec-only), hand the user the spec, and say the routine could not be created here.- Slack connector unavailable at creation → the armed message (step 7) can't send; report that delivery is unproven and the connector needs connecting, rather than claiming success.
ALERT_SLACK_DEFAULT_LOCATIONunset → no default offered; destination is required, and offered for persist after sign-off.- Not a git repo → routine still creatable, but there's no
.ai/home for the log; say so and skip the log write.
Dependencies
Depends on a built-in (schedule) and a connector (Slack) — not on any repo skill — so it needs no skill_deps() entry in sync.sh; its absence there is correct, not a gap. Ships via sync.sh push and plugin auto-discovery like any skill.
Anti-Rationalization
| Excuse | Reality |
| "The trigger is obvious — I'll infer the fire condition" | Inference supplies the offered default; only the user's answer sets the fire criteria. A guessed trigger is the false-fire bug you're here to prevent. |
| "I'll send a quick test message to prove Slack works" | The dry-run never touches Slack. Delivery is proven exactly once, by the armed message in step 7 — after sign-off. |
| "They seemed happy with the spec, that's a sign-off" | Sign-off is an explicit approval of the written spec, not inferred enthusiasm. Ask outright. |
| "I'll hardcode the Slack ID / write it into the log to keep it simple" | The ID is machine-local: it lives in $ALERT_SLACK_DEFAULT_LOCATION, is baked into the routine at creation, and appears in the log only redacted. Never in this skill or a tracked file. |
| "Edge trigger — I'll add a state file / gist / self-rewriting prompt" | Use the timestamp-window reframing. A no-timestamp source is a non-goal, not a reason to improvise a state store. |
Verification
- Nothing created (routine, log, settings write, or Slack message) before the explicit spec sign-off;
--spec-onlystopped at the spec - All fields put to the user one at a time; the trigger's fire/no-fire criteria are testable and its level/edge semantics resolved
- The dry-run ran, showed source output + verdict + an in-chat message preview, and sent no Slack message
- The synthesized spec was presented and explicitly approved as written
- The hardening block is baked verbatim into the routine prompt; the resolved destination is baked in but never written to a tracked file
- The
.ai/routines.mddestination is redacted; no tokens/keys recorded; settings persisted (if agreed) via the safe jq→tmp→mv write, post-sign-off - The armed 🔔 message was sent (delivery proven) or its failure reported loudly; routine ID + retirement reported
Does not
- Watch opaque, no-timestamp sources that would need a persisted content-hash baseline — a non-goal (v2); timestamp-window is the only supported edge mechanism.
- Manage existing alerts (list / edit / retire) — that stays with the
scheduleskill; this skill only authors new ones. - Send via the
slack-messageskill — that drafts for human review; an unattended routine sends directly through the Slack connector. - Write any machine-local Slack ID into this skill or a tracked file (the log stores it redacted).
- Create the routine on any substrate other than a claude.ai cloud routine.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

