From engineering
Install a self-scheduling daily Slack reminder for pending PR reviews using launchd (macOS). On first run, configures the repo, notification time, and Slack webhook, then installs and loads a launchd agent that fires automatically every day. Also cancels the schedule or shows its status. Use when asked to "track my PR reviews", "set up daily PR review reminders", "notify me about pending review requests", "cancel PR review tracking", or "check PR tracker status".
How this skill is triggered — by the user, by Claude, or both
Slash command
/engineering:track-pr-review-request [owner/repo] [cancel|status] [label=<label>][owner/repo] [cancel|status] [label=<label>]This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Install a launchd agent that runs daily, checks open PRs where your GitHub review is still requested, and sends a Slack notification. One invocation of this skill installs everything. Subsequent daily runs happen automatically without any manual trigger.
Install a launchd agent that runs daily, checks open PRs where your GitHub review is still requested, and sends a Slack notification. One invocation of this skill installs everything. Subsequent daily runs happen automatically without any manual trigger.
When invoked in setup mode (the default), configure all required inputs, store the Slack webhook securely in the macOS Keychain, generate a self-contained shell script, and install a launchd agent that fires it at the configured time every day.
Once installed, the agent runs headlessly — no model session, no MCP, no manual invocation needed. The generated shell script handles deduplication, GitHub API calls, and Slack notification via curl.
To stop the daily notifications, invoke the skill with cancel. To inspect the current schedule state, use status.
Stop notifying for a PR when any of these conditions is true:
reviewRequests automatically).reviewDecision == "APPROVED").| Argument | Behavior |
|---|---|
| (none) | Setup: configure, generate script, install agent |
cancel | Unload agent, remove plist and script |
status | Show whether the agent is loaded and last run |
Before running, collect any missing data using AskUserQuestion.
Required:
acme/my-service).09:00).Optional:
$ARGUMENTS as label=<value>).notify_when_empty; default: false).ignore_drafts; default: true).To create a Slack Incoming Webhook: go to https://api.slack.com/apps → create or select an app → "Incoming Webhooks" → "Add New Webhook to Workspace" → choose a channel → copy the URL. The URL looks like
https://hooks.slack.com/services/T.../B.../.... Keep it private.
Parse $ARGUMENTS:
cancel → jump to Step 3 (Cancel) after completing Step 2.status → jump to Step 4 (Status) after completing Step 2.Also parse:
$ARGUMENTS contains label=<value>, extract it as the label filter (overrides config).$ARGUMENTS starts with owner/repo format, use that as the repo.Check that required tools are available:
gh auth status # GitHub CLI, authenticated
jq --version # JSON processor
security list-keychains # macOS Keychain
launchctl version # macOS launchd
curl --version # HTTP client
If gh auth fails, stop and tell the user to run gh auth login.
If jq is missing, stop and tell the user to install it:
brew install jq
security, launchctl, and curl are built-in on macOS; if any are missing, stop and report.
Resolve the repository (for setup and cancel/status to find the plist label):
owner/repo from $ARGUMENTS if present.gh repo view --json nameWithOwner --jq .nameWithOwnerDerive the plist label: replace / with - in the repo slug and prefix with com.pr-review-tracker.:
com.pr-review-tracker.owner-repo
Derive file paths (all absolute):
PLIST_PATH=~/Library/LaunchAgents/com.pr-review-tracker.<owner-repo>.plist
SCRIPT_PATH=<abs-project-dir>/.claude/pr-review-tracker-run.sh
CONFIG_PATH=<abs-project-dir>/.claude/pr-review-tracker.json
LOG_PATH=<abs-project-dir>/.claude/pr-review-tracker.log
Unload the launchd agent if it is loaded:
launchctl bootout gui/$(id -u) "$PLIST_PATH" 2>/dev/null || true
Remove the plist and the run script:
rm -f "$PLIST_PATH"
rm -f "$SCRIPT_PATH"
Ask the user whether to also delete the Slack webhook from the Keychain:
The Slack webhook is stored in Keychain under service "com.pr-review-tracker", account "<owner-repo>-slack-webhook".
Delete it too? (yes/no)
If yes:
security delete-generic-password \
-s "com.pr-review-tracker" \
-a "<owner-repo>-slack-webhook" 2>/dev/null || true
Keep .claude/pr-review-tracker.json so configuration survives for a future re-setup.
Print:
PR Review Tracker cancelled.
Removed:
plist: ~/Library/LaunchAgents/com.pr-review-tracker.<owner-repo>.plist
script: .claude/pr-review-tracker-run.sh
keychain entry: yes / no
Config file kept at .claude/pr-review-tracker.json
Stop.
Check whether the launchd agent is installed and loaded:
# Is the plist file present?
ls "$PLIST_PATH" 2>/dev/null
# Is the agent loaded?
launchctl list com.pr-review-tracker.<owner-repo> 2>/dev/null
Read last_notified_date from .claude/pr-review-tracker.json if it exists.
Extract the scheduled time from the plist file if it exists.
Print a summary:
PR Review Tracker Status
Repository: owner/repo
Plist installed: yes / no
Agent loaded: yes / no
Schedule: 09:00, weekdays
Last notified: 2025-06-10 (or "never")
Log file: .claude/pr-review-tracker.log
If the agent is loaded, also print the last exit code from launchctl:
launchctl list com.pr-review-tracker.<owner-repo> | grep LastExitStatus
Stop.
Load existing configuration from $CONFIG_PATH. If the file does not exist or any required field is missing, ask for only the missing values.
Example question:
I need a few values to set up the PR review tracker:
1. GitHub username:
2. Notification time (24h, e.g. 09:00):
3. Schedule: weekdays or every day?
4. Label filter (leave blank to track all PRs):
5. Notify when no PRs are pending? (yes/no, default no):
6. Ignore draft PRs? (yes/no, default yes):
The Slack Incoming Webhook URL is collected separately in Step 6 and never stored in this file.
Config structure:
{
"repo": "owner/repo",
"github_username": "username",
"notification_hour": 9,
"notification_minute": 0,
"schedule_days": "weekdays",
"notify_when_empty": false,
"ignore_drafts": true,
"label_filter": "",
"plist_label": "com.pr-review-tracker.owner-repo",
"plist_path": "/abs/path/to/LaunchAgents/com.pr-review-tracker.owner-repo.plist",
"script_path": "/abs/path/to/.claude/pr-review-tracker-run.sh",
"last_notified_date": ""
}
Never store the Slack webhook URL, GitHub tokens, or any other secrets in this file.
Ask for the Slack Incoming Webhook URL if not already stored:
Paste your Slack Incoming Webhook URL (starts with https://hooks.slack.com/services/...):
Store it in the macOS Keychain:
security add-generic-password \
-s "com.pr-review-tracker" \
-a "<owner-repo>-slack-webhook" \
-w "$SLACK_WEBHOOK_URL" \
-U # -U updates if already exists
Verify the store succeeded by reading it back:
security find-generic-password \
-s "com.pr-review-tracker" \
-a "<owner-repo>-slack-webhook" \
-w
If verification fails, stop and report the error. Do not continue without a retrievable webhook.
Write the following self-contained script to $SCRIPT_PATH. Fill in all literal values from the config (do not use shell variables that depend on the environment at run time — bake the values in).
#!/bin/bash
# Auto-generated by track-pr-review-request skill — do not edit manually.
# Re-run the skill to update configuration.
set -euo pipefail
REPO="<owner/repo>"
GITHUB_USERNAME="<username>"
IGNORE_DRAFTS=<true|false>
NOTIFY_WHEN_EMPTY=<true|false>
LABEL_FILTER="<label or empty>"
KEYCHAIN_SERVICE="com.pr-review-tracker"
KEYCHAIN_ACCOUNT="<owner-repo>-slack-webhook"
CONFIG_FILE="<abs-path>/.claude/pr-review-tracker.json"
LOG_FILE="<abs-path>/.claude/pr-review-tracker.log"
log() {
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >> "$LOG_FILE"
}
# Deduplication
TODAY=$(date -u +%Y-%m-%d)
LAST_NOTIFIED=$(jq -r '.last_notified_date // ""' "$CONFIG_FILE" 2>/dev/null || echo "")
if [ "$LAST_NOTIFIED" = "$TODAY" ]; then
log "Already notified today. Skipping."
exit 0
fi
log "Fetching PRs for $REPO..."
# Build gh arguments
GH_ARGS=(pr list
--repo "$REPO"
--state open
--search "review-requested:$GITHUB_USERNAME"
--limit 100
--json number,title,url,author,reviewRequests,reviewDecision,isDraft
)
if [ -n "$LABEL_FILTER" ]; then
GH_ARGS+=(--label "$LABEL_FILTER")
fi
PR_JSON=$(gh "${GH_ARGS[@]}" 2>&1) || {
log "ERROR: gh command failed: $PR_JSON"
exit 1
}
# Filter for PRs that still need my review
PENDING=$(printf '%s' "$PR_JSON" | jq \
--arg user "$GITHUB_USERNAME" \
--argjson ignore_drafts "$IGNORE_DRAFTS" \
'[.[] | select(
(if $ignore_drafts then .isDraft == false else true end) and
(.reviewDecision != "APPROVED") and
(.reviewRequests | map(.login) | any(. == $user))
)]')
PENDING_COUNT=$(printf '%s' "$PENDING" | jq 'length')
log "Pending reviews: $PENDING_COUNT"
if [ "$PENDING_COUNT" -eq 0 ]; then
if [ "$NOTIFY_WHEN_EMPTY" = "true" ]; then
MESSAGE="Daily PR Review Reminder
No PRs are currently waiting for your review."
else
log "No pending reviews and notify_when_empty=false. Skipping Slack."
jq --arg d "$TODAY" '.last_notified_date = $d' "$CONFIG_FILE" \
> "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
exit 0
fi
else
MESSAGE=$(printf '%s' "$PENDING" | jq -r \
'"Daily PR Review Reminder\n\nYou have \(length) PR(s) waiting for your review:\n\n" +
(to_entries | map(
"\(.key + 1). \(.value.title)\n Author: \(.value.author.login)\n URL: \(.value.url)"
) | join("\n\n"))')
fi
# Retrieve Slack webhook from Keychain
SLACK_WEBHOOK=$(security find-generic-password \
-s "$KEYCHAIN_SERVICE" \
-a "$KEYCHAIN_ACCOUNT" \
-w 2>/dev/null) || {
log "ERROR: Slack webhook not found in Keychain (service=$KEYCHAIN_SERVICE, account=$KEYCHAIN_ACCOUNT)."
exit 1
}
# Send to Slack
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H 'Content-type: application/json' \
--data "$(jq -n --arg text "$MESSAGE" '{"text": $text}')" \
"$SLACK_WEBHOOK")
if [ "$HTTP_STATUS" != "200" ]; then
log "ERROR: Slack returned HTTP $HTTP_STATUS. Message not delivered."
exit 1
fi
log "Slack notified. $PENDING_COUNT pending PR(s)."
# Update deduplication state
jq --arg d "$TODAY" '.last_notified_date = $d' "$CONFIG_FILE" \
> "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
After writing the file, make it executable:
chmod +x "$SCRIPT_PATH"
Build the StartCalendarInterval block based on schedule_days:
Every day (schedule_days == "daily"):
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer><notification_hour></integer>
<key>Minute</key>
<integer><notification_minute></integer>
</dict>
Weekdays only (schedule_days == "weekdays"):
<key>StartCalendarInterval</key>
<array>
<dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer><h></integer><key>Minute</key><integer><m></integer></dict>
<dict><key>Weekday</key><integer>2</integer><key>Hour</key><integer><h></integer><key>Minute</key><integer><m></integer></dict>
<dict><key>Weekday</key><integer>3</integer><key>Hour</key><integer><h></integer><key>Minute</key><integer><m></integer></dict>
<dict><key>Weekday</key><integer>4</integer><key>Hour</key><integer><h></integer><key>Minute</key><integer><m></integer></dict>
<dict><key>Weekday</key><integer>5</integer><key>Hour</key><integer><h></integer><key>Minute</key><integer><m></integer></dict>
</array>
Write the complete plist to $PLIST_PATH:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.pr-review-tracker.<owner-repo></string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string><abs-script-path></string>
</array>
<key>StartCalendarInterval</key>
<!-- insert block from above -->
<key>RunAtLoad</key>
<false/>
<key>StandardOutPath</key>
<string><abs-log-path></string>
<key>StandardErrorPath</key>
<string><abs-log-path></string>
</dict>
</plist>
Unload any existing version of this agent before loading the new one (handles updates):
launchctl bootout gui/$(id -u) "$PLIST_PATH" 2>/dev/null || true
Load the new agent:
launchctl bootstrap gui/$(id -u) "$PLIST_PATH"
Verify it loaded:
launchctl list com.pr-review-tracker.<owner-repo>
If the list command returns a non-empty result, the agent is active. If it fails, report the error and stop.
Ask the user:
Setup complete. Run a test notification now to confirm everything works? (yes/no)
If yes, run the script directly (bypassing the deduplication date check by temporarily clearing last_notified_date):
# Temporarily clear last_notified_date
jq '.last_notified_date = ""' "$CONFIG_PATH" > "${CONFIG_PATH}.tmp" \
&& mv "${CONFIG_PATH}.tmp" "$CONFIG_PATH"
bash "$SCRIPT_PATH"
Report whether the script exited 0 and whether a Slack message was delivered. If it failed, show the last lines of the log:
tail -20 "$LOG_PATH"
After successful setup:
PR Review Tracker installed
Repository: owner/repo
Schedule: 09:00, weekdays
Agent label: com.pr-review-tracker.owner-repo
Plist: ~/Library/LaunchAgents/com.pr-review-tracker.owner-repo.plist
Run script: .claude/pr-review-tracker-run.sh
Log file: .claude/pr-review-tracker.log
Slack webhook: stored in Keychain (service=com.pr-review-tracker)
The agent is loaded and will fire automatically.
To cancel: track PR review requests cancel
To check status: track PR review requests status
If any step fails:
If the Keychain store fails:
If launchctl bootstrap fails:
plutil -lint "$PLIST_PATH"), permissions issue, label collision with an already-loaded agent from a different path.launchctl bootstrap / bootout, not the deprecated launchctl load / unload.--search "review-requested:USERNAME" in gh pr list to filter at the API level.--limit 100 to avoid silently truncating large repositories.reviewRequests[].login (not the whole object) against the GitHub username.last_notified_date if the Slack send failed — allow the next run to retry..claude/pr-review-tracker.json on cancel so re-running setup requires minimal re-entry.last_notified_date).npx claudepluginhub blacksam07/agent-skills --plugin engineeringCreates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.