Never write a standup again. A Claude Code plugin that composes and posts your daily standup from what you actually did — Jira tickets, GitHub PRs, Claude Code sessions, calendar — every working day, unattended.
⚠️ macOS-only in v1 (the scheduler is launchd). Linux/Windows: the pipeline works interactively; contribute a scheduler card!
## 🗓️ Thu 2026-07-16 — you
**Yesterday**
- Payments: shipped PROJ-42 retry logic (repo#123, repo#124), CI green
- Platform: root-caused the flaky deploy (PROJ-57); filed PROJ-61
- (Calls: incident review, API design sync)
**Today**
- Payments: land PROJ-43; start PROJ-61
- Reviews: 6 PRs
- (Calls: quarterly planning)
Terse by design: two blocks, one line per initiative, meetings without a ticket in brackets, everything linked, no Blockers section. All knobs in config.
What the plugin actually does for you:
| Capability | Detail |
|---|---|
| Pulls from 4 sources | Jira (your JQLs), GitHub (gh CLI or GitHub MCP), Claude Code sessions, Google Calendar — each source is optional and independently toggled |
| Merges by ticket key | Branch PROJ-42-* + its PR + the Jira ticket collapse into one line, so a single piece of work reads as a single initiative |
| Reviews as activity, not a queue | Reports reviews you gave and feedback you addressed — never "N PRs awaiting your review" |
| Working-day aware | Weekends, OOO, sick leave, vacation, and company holidays (read from your calendar) skip the run cleanly; the next run's "Yesterday" stretches back to your last real working day |
| Posts to 3 sinks | Slack canvas (multi-writer safe, replace-not-duplicate), Slack channel (edits via ts, backlog collapsed to one message), local file archive — enable any combination |
| Never loses a standup | A failed delivery becomes typed pending and is flushed on the next success; a composed standup is never dropped |
| Runs unattended | launchd fires it on your schedule; an independent watchdog catches version drift, no-post days, and stuck pending |
| Fails loudly | Per-sink same-day failure alarms via macOS notification, plus optional free phone push (ntfy.sh). Silent failure is a designed-out state |
| Review mode | Optionally drafts to your own Slack DM first; you approve with post before anything goes public |
| Recap your history | On-demand month/quarter/half/date-range TLDRs and keyword search across everything you've shipped — appraisal fuel from your local archive |
| Self-diagnosing | doctor gives one table with exact remediations: card availability, connector flavors, scheduler liveness, receipt age, pending backlog, pause/resume |
| Extensible | A new source or sink is one markdown file against cards/CONTRACT.md — zero core changes |
| Privacy-safe | Alarms carry counts and dates only, never ticket text; nothing recap-related ever auto-posts |
flowchart LR
subgraph sources [source cards]
J[Jira] ; G[GitHub gh] ; S[Claude sessions] ; C[Calendar]
end
subgraph core [deterministic core]
W[working-day gate] --> M[merge by ticket key]
end
subgraph llm [LLM stage]
B[compose and bucketize]
end
subgraph sinks [sink cards]
K[Slack canvas] ; H[Slack channel] ; F[file archive]
end
sources --> W
M --> B --> sinks
sinks --> R[receipts and pending]
L[launchd daily] --> W
D[watchdog] -.-> R
| Piece | What it does |
|---|---|
Source cards (cards/sources/) |
One markdown file per source: probe + fetch recipe + output contract. Dark lane = fewer bullets + a flag, never a dead run, never invented content. |
| Working-day gate | Weekends, OOO, sick leave, vacation, company holidays (from your calendar, configurable patterns) ⇒ the run skips cleanly, and the NEXT run's "Yesterday" stretches back to your last actual working day. |
| Compose (LLM) | Merges everything by ticket key, bucketizes per initiative, formats. The date always comes from date, never model memory. |
Sink cards (cards/sinks/) |
Slack canvas (multi-writer safe, replace-not-duplicate), Slack channel (edit-via-ts, backlog collapsed to one message), local file archive. Delivery failure ⇒ typed pending, flushed on next success — a composed standup is never lost. |
| Scheduler | launchd daily job + an independent watchdog job. Version drift, no-post days, stuck pending — all surface as macOS notifications. Silent failure is a designed-out state. |
Prerequisites (the doctor checks all of this and tells you exactly what's missing):
- Claude Code with the connectors you want: Slack (canvas/channel), Atlassian (Jira), Google Calendar — each is its own OAuth
- GitHub lane: your choice of
ghCLI or the GitHub MCP server (setup probes both and asks) node≥ 20
claude plugin marketplace add jerry7991/standup-ghost
claude plugin install standup-ghostThen, inside Claude Code:
/standup-ghost:setup
The wizard collects your config, probes which connector flavors your machine has, dry-runs with your real data (showing exactly where it will post, by name), makes the first real post, and hands you ONE paste to install the schedule. Setup only declares itself done after a real calendar-fired run has posted.
| Command | What |
|---|---|
/standup-ghost:standup |
Full run (the scheduler calls this) |
/standup-ghost:standup dry-run |
Compose only, write to the local file sink |
/standup-ghost:standup post |
Approve today's held draft (review mode) |
/standup-ghost:doctor |
Full diagnosis — one table, exact remediations |
/standup-ghost:doctor pause / resume |
Pause cleanly when going on leave without calendar events |
/standup-ghost:recap month (or quarter, half, 2026-01-01..2026-03-31) |
TLDR of what you shipped, from your local archive |
/standup-ghost:recap find <query> |
Search your work history: when/where a topic appeared, dated + linked |
/standup-ghost:recap team / recap sprint |
On-demand team digest / current-sprint slice. Never runs automatically. |
Copy config.example.json to ~/.config/standup-ghost/config.json (setup does this for you). Key sections:
| Key | Meaning |
|---|---|
schedule |
Days + time (local) — changing it regenerates the launchd jobs |
sources.* / sinks.* |
Enable flags + per-card settings (JQLs, canvas/channel IDs) |
absence.* |
OOO event types, leave/sick/holiday title patterns, company-holiday calendar IDs, auto_skip |
format.* |
Heading, bucket hints, max lines, lookback floors |
behavior.* |
review_mode, notify_on_post, min-content gate, pending expiry, alarm threshold |
notifications.ntfy_topic |
Optional free phone push for failure alarms (via ntfy.sh) — no account/token. Empty ⇒ local macOS notification only. |
A new source or sink = one markdown file — read cards/CONTRACT.md. The cards/sinks/file.md demo sink was written strictly against that contract with zero core changes; yours can be too. Security: declared tools must sit inside the per-kind enum in runtime/lib/cards.js (widening it is a reviewed change), and only reviewed: true cards merged via PR run by default.
node --test test/*.test.js # 31 unit tests
zsh test/run-smoke.sh # runner/watchdog smoke with mocked claude
./scripts/scan-identifiers.sh # release gate: no private identifiers shipThe scan gate ships only generic patterns. Put YOUR company/personal strings (org names, channel/canvas IDs, emails) in ~/.config/standup-ghost/private-patterns.txt — one regex per line, part of your local profile, never committed. .scan-patterns.local works repo-locally (gitignored) too.
Operational notes worth reading before filing an issue
- launchd + TCC (2026-07-16): background jobs cannot read
~/Desktop/~/Documents. The runtime lives in~/.local/share/standup-ghost— never move it to Desktop. - Powered-off days: launchd fires a missed run when the Mac wakes, but not if it was off at trigger time. Off all day = skipped day (the watchdog will tell you).
- Slack canvas API drift (2026-07-16): parameterless prepend was rejected; replace-without-section-id wipes canvases. The canvas card encodes the current verified semantics and read-back-verifies every write. If Slack drifts again, file an issue with the error.
- Notifications: macOS may silently suppress
osascriptnotifications until you allow them (System Settings → Notifications). Setup fires a test one and asks if you saw it. Setnotifications.ntfy_topicfor a phone push that doesn't depend on being at the Mac. - Cards path / empty allowlist (2026-07-21): the runner generates
--allowedToolsfrom the enabled cards. If the generator can't find the cards dir it emits core tools only — every Slack/Jira/Calendar MCP call is then denied headless, silently. Fixed:allowlist.jsresolves both install (<runtime>/cards) and repo (<repo>/cards) layouts, andrun.shexportsSTANDUP_GHOST_CARDS_DIR. Sanity check:node runtime/lib/allowlist.jsmust list your MCP tools. - Headless connectors: the claude.ai connectors (Slack/Jira/Calendar) DO work under
claude -pwhen authenticated — but they lapse to "Needs authentication" and can't self-re-auth headless. The runner preflightsclaude mcp listand alarms which connector needs/mcp; a failed sink is held to pending and alarms same-day (per-sink). gh searchquoting: the positionalorg:… updated:…form breaks; the github card uses flag form only.
MIT © jerry7991