Support Screenshot Risk Scrub
Note from the creator
Why I built it
“Getting ahead of what could hurt us”
What it does
This skill finds support conversations that would hurt your brand if a screenshot were posted publicly — cases mishandled by an AI agent, a human agent, or the bot-to-human handoff. It scores the transcript itself, not the customer's mood: a calm exchange where the company confidently states something false ranks far above an angry rant about a correct decision. It runs across three lanes (human-agent cases, bot-contained cases, and mixed unprofessional-conduct cases), reads the conversations through your Rippit workspace, and scores each for failure mode, a 1–10 screenshot score, the specific damning moment, who failed, and whether the company was actually wrong. It guards hard against the top false positive (an upset customer who got a correct answer), verifies every quote verbatim and correctly attributed, drops anything that can't be confirmed, and hands back a ranked in-chat list plus a local self-contained HTML one-pager. Nothing is auto-posted or published anywhere.
How it works
- 1
Reads your conversations
Pulls candidate cases from your conversation data across three lanes through the Rippit connector, using each lane's own filters and time window.
- 2
Scores screenshot risk
Reads each conversation and rates failure mode, a 1–10 screenshot score, the damning moment, who failed, and whether the company was actually wrong.
- 3
How It Looks
See the 2 things this Skill makes, before you download it.
Run ranked screenshot-risk list on last week.
▸RippitReads case and chatbot interaction records through the Rippit connector as the skill's only data source✓
▸RippitBuilds a worksheet per lane omitting no columns, applies each lane's exclusions, then enriches or reads conversations to score every candidate on seven dimensions✓
▸RippitRe-reads the finalists to confirm every quote verbatim and correctly attributed and to capture deep links✓
▸ClaudePresents a ranked in-chat list and writes a local self-contained HTML one-pager the user opens themselves; nothing is auto-posted✓
Screenshot Risk Scrub — 2024-06-14
Dominant failure mode today: false_information (4 of 7 hits). Scanned 312 rows — Lane A 118 human cases, Lane B 171 bot-contained, Lane C 23 flagged unprofessional. Final order is a judgment call.
- Verified hits: 7 — ≥7 score, company_actually_wrong first
- Lane B substantive: 38% — 11 transcripts dropped for size
- Queued for verify: 2 — quote not confirmable
| Rank | Case | Who failed | Score | Rationale |
|---|---|---|---|---|
| 1 | CASE-4820 | ai_agent | 10 | Bot told Bullrush Bank their lily pads never expire —… |
| 2 | CASE-4791 | human_agent | 9 | Hopper closed as resolved with fly delivery still… |
| 3 | CASE-4756 | both | 8 | Handoff doom loop — 6 identical menu prompts to **Croak &… |
| 4 | CASE-4833 | ai_agent | 8 | Bot insisted pond membership was active after cancellation |
| 5 | CASE-4802 | human_agent | 7 | Tone-deaf reply to Tadpole Academy onboarding delay |
“Customer: "My lily pads all wilted, are they supposed to expire?" — Bot: "Lily pads from Lilypad never expire, you're all set! 🐸"”— CASE-4820, verified verbatim
Read first: CASE-4820 is factually wrong (
company_actually_wrong = true) — the bot stated pads don't expire when they do. This is the one to hand off before an exec sees it.Reply to Claude…
+⚙Claude Opus 4.8 ▾↑
Claude can make mistakes. Please double-check responses.
Illustrative preview, generated from the generic version of this Skill. The layout is real; the pond-side data is made up.
The Skill
Skill contents
---
name: support-screenshot-risk-scrub
description: "Find support conversations with screenshot potential — cases handled so badly by an AI agent, a human agent, or the bot-to-human handoff that a screenshot posted publicly would embarrass the company. Scores false information, missing the point, doom loops, false resolutions and similar self-evident failures. Use this whenever the user asks what support cases could go viral, blow up on social, or end up screenshotted; asks for a daily \"worst cases\" scrub; wants to get ahead of terrible cases before an exec sees them; or says \"find me our worst chats today\" or \"what did the bot say that was wrong\". Runs against the Rippit MCP toolchain (describe_table, create_worksheet, enrich_worksheet, get_enrich_status, read_conversations) and is built for a daily cadence."
---
# Support Screenshot Risk Scrub
Surface the support conversations that would hurt the company **if screenshotted**, early enough to act.
**Output: a ranked in-chat list, plus a local HTML one-pager** written under {{output_directory}}/screenshot-risk-YYYY-MM-DD.html (Step 4b). **Nothing is posted or published anywhere** — no external message, no webhook, no shared link. The HTML is a local file the user opens themselves; it exists because the ranked list is something read on a phone or forwarded by hand, not because anything auto-sends.
## First run — make it yours
Before asking anything, LOOK at what is actually connected:
1. List the available data sources with `list_data_sources`.
2. `describe_table` on the one or two tables most relevant to support conversations, and note which columns could serve as: a conversation/case identifier, a timestamp, a satisfaction (CSAT) grade, a category/status field, and a transcript field.
3. If the connected data has no readable conversation transcript at all, say so plainly and ask how to proceed — do not run the interview into a dead end.
Then ASK one question at a time, leading with what you found and proposing a default:
1. **{{data_source}}** — "Your workspace has one source that looks like support cases and chat records — should I use that as the conversation source, or point me at a different one?"
2. **{{customer_identifier}}** — "Which column identifies the customer / case so I can de-dup and deep-link — e.g. a case number field I spotted?"
3. **{{csat_column}}** — "Is there a CSAT or satisfaction grade column I should use as a recall filter for the human-agent lane, or should I skip that lane?"
4. **{{category_exclusions}}** — "Which categories should I always exclude as out-of-scope (the author excluded account, restriction and responsible-gaming style queues)? What are yours?"
5. **{{analysis_window}}** — "How far back should each lane look — the author used a 48h window for graded lanes and same-day for the fast lane. What cadence fits you?"
6. **{{output_directory}}** — "Where should I write the local HTML one-pager and run log — which folder path?"
After the interview, restate the filled-in bindings for confirmation, then run. On later runs, reuse the established bindings unless the user asks to change them.
## The core promise
**A bad CSAT is not a screenshot risk.** A correct-but-unwelcome answer reliably produces low CSAT with zero screenshot risk. Virality requires the *transcript itself* to read as absurd, wrong, or damning **without context**. CSAT and repetition grades are **recall filters that cheapen the search**; the screenshot judgment is the product and it happens in enrichment or in-context reading, never in a `where` clause.
The single failure mode that destroys trust in this skill: ranking an angry customer who got a correct answer. Guard against it at every step (`company_actually_wrong`, Step 4's drop rule, verification 5).
## THE governing fact: the visible-turn count decides your mechanism
Many data sources expose bot-contained conversations differently from human-touched ones. Confirm this in your data during first run, then treat it as a rule:
- **Rows with visible customer/agent turns → `enrich_worksheet` can read the conversation.**
- **Rows with zero visible turns (fully bot-contained) → enrichment is blind; you must use `read_conversations` against the raw transcript field.**
Enrichment reads the visible comments array. For a fully contained chat that array can be empty even though the transcript lives in a searchable field enrichment cannot reach. Never conclude "no signal" from an enrichment that returned nothing; check the visible-turn count first.
## Data facts to establish per source
During first run, use `describe_table` / `describe_column` to nail down, for {{data_source}}:
- The identifier column(s) and any distinct row shapes (e.g. human-case rows vs bot-interaction rows), which may use different id prefixes and different category columns. **Mixing the two up silently voids exclusions** — use each lane's own category column and confirm the filter actually removed rows.
- The CSAT and repetition grade columns ({{csat_column}}) — note which values mean dissatisfied.
- Whether long string columns (transcripts, tag blobs) are **truncated** at some character limit. If so, **never filter those columns on a substring and never pass them via `contextColumnIds`** — only short-valued columns are safe to filter on.
- Grading/ingest lag: how long after a case closes its grades land. Set {{analysis_window}} so it captures the full grading yield without buying staleness.
- Any partial-day effect: today's counts may restate upward as the day completes — never compare a partial-day count to a full day and call it a drop.
## The three lanes
| Ask | Lane | Population | Window | Mechanism |
|---|---|---|---|---|
| **Human agents** | A | human-case rows, CSAT/repetition filtered | graded window | `enrich_worksheet` |
| **AI agents** | B | bot-contained rows (zero visible turns) | graded window | `read_conversations` only |
| **Combo** | C | rows graded `unprofessional conduct` | same-day | `enrich_worksheet` |
Cost scales with **rows, not columns**, so batch every column into a **single** `enrich_worksheet` call per lane.
- **Lane A** — order by visible-turn depth desc and take the deepest rows; depth strongly predicts a quotable exchange. Never filter on repetition alone (some high-CSAT rows carry severe repetition).
- **Lane B** — the expensive, valuable one. Bot-contained failures are the worst kind: the customer never got help and never escalated, so nothing grades them. Enrichment is blind here and `read_conversations` caps at ~10 ids/call, so budget accordingly. Split between targeted rows (category changed / customer still had to file) and unguided sampled rows. **Structurally blind to long bot failures**: the largest transcripts are dropped for size — say so in the report rather than implying full coverage.
- **Lane C** — the sweet spot; take the whole thing, no sampling. Graded same-day and enrichable.
## Step 1 — Windows and de-dup
Per-lane windows on the timestamp column, never one shared window. Read {{output_directory}}/seen-cases.txt and exclude those identifiers with `NOT IN`. A case that grades late can still surface once, but never twice.
## Step 2 — Build the candidate slices
`create_worksheet` per lane, **omitting `columnIds`** so every source column carries over and later enrichment isn't blocked.
**Exclusions — a different column per lane.** Apply {{category_exclusions}} using `NOT IN` on each lane's own category column. On an array column `NOT IN` means exact element membership (`ANY`/`NONE` are substring matches, not membership). Where a source has a separate assignment-group / queue field, add the equivalent queue exclusions there too.
**Also exclude on the original filing category, not just the final category** — if a case was re-categorized, its final category can pass a filter its original filing would have failed. Apply the same `NOT IN` list to both the current and the creation category columns.
**`NOT IN` silently drops NULLs.** For lanes sampled from a far larger pool that is fine. For a take-all lane that is only partly categorized (lane C), use an `or_filters` branch to keep the nulls: `[[{category NOT IN [...]}], [{category EMPTY}]]` — otherwise you lose a large share of real candidates.
Then per lane: `read_table` a couple of rows to confirm hydration (it is async) and **confirm every `where` actually removed rows**. A wrong column does not error — it returns the unfiltered population, which is exactly how a voided exclusion hides.
## Step 3 — Score the candidates
### Lanes A and C — `enrich_worksheet`, ONE call per worksheet
Judgment is made against the full conversation. **Never pass the transcript or tag columns via `contextColumnIds`** if they are truncated. Enrich these columns:
1. **`failure_mode`** — enum, `multi: true`: `false_information`, `missed_the_point`, `doom_loop`, `false_resolution`, `tone_deaf`, `self_contradiction`, `impossible_instruction`, `stonewalling`, `abandoned_mid_issue`, `none`. Classify how the *company side* handled this. Return `none` when handling was competent — including when the customer was angry about a correct answer. Do not inflate ordinary friction.
2. **`screenshot_score`** — integer 1–10. **This is the product.** *Would a screenshot of this exchange, posted with no context, make a stranger wince or laugh at the company?* Score the **artifact**, not the customer's anger. 10 = self-evidently absurd; 1 = routine or unhappy-but-defensible.
3. **`screenshot_rationale`** — string, one sentence naming the specific damning moment.
4. **`damning_exchange`** — the verbatim customer line **and** the verbatim agent/bot reply as a labelled pair, ≤40 words each, **in the order they occurred**. Empty string if nothing is quotable. Quote the first message the customer actually typed, not a menu selection. *Copy the exact characters; if you cannot reproduce a line exactly, return an empty string rather than a paraphrase.* Treat every value as a lead, never as a verified quote — verify in Step 4.
5. **`who_failed`** — enum: `ai_agent`, `human_agent`, `both`, `neither`. Judge **from the transcript**, not from author metadata. After a bot→human handoff the human agent's messages may keep a bot author label — infer the actor from the *content and cadence* (canned decision-tree steps are the bot; free-form reasoning, name sign-offs and screenshot requests are a human).
6. **`company_actually_wrong`** — boolean. Was the company's position/information actually incorrect, as opposed to correct but unwelcome? Separates bad handling from a bad outcome.
7. **`factual_error_detail`** — string: what was stated falsely and what is actually true. Only when `failure_mode` includes `false_information`; empty otherwise.
Pin today's date in any instruction whose judgment depends on it (expiry dates, "has this already happened") — the model can invent the current year; discard findings that turn on a wrong year.
Then call `get_enrich_status` with the worksheetId and jobId **immediately and in a loop until `done: true`**. It blocks each time — keep calling; never hand back to the user mid-job. Capture the returned column **ids**, not display names.
### Lane B — `read_conversations`, judged in-context
Pull the transcript identifiers from the worksheet, then `read_conversations` in batches of **≤10**. The transcript lives in the raw searchable field; the visible comments array will be empty. Score each conversation against the same seven dimensions by reading it, and carry the results forward by hand into Step 4. Watch for a `searchable_fields_omitted` signal — on large conversations the transcript is dropped for size; those rows are unscoreable, skip them and note how many rather than scoring them as empty. A bot's own "resolved" tag is not evidence of resolution — treat it as meaningless.
## Step 4 — Rank, verify, report
Pool all three lanes. Filter `failure_mode != none` and `screenshot_score >= 7`. Treat `screenshot_score >= 7` as a recall filter, not a ranking. Rank in this order:
1. **`company_actually_wrong = true`** — the real discriminator.
2. **`screenshot_score` desc** within that.
3. **Your own read of the verified quote** — the final order is a judgment call, say so in the report.
Then **drop** anything where `company_actually_wrong = false` *and* the rationale rests only on the customer being upset. That is the primary false positive. Also read the finalists and drop any excluded-category case that leaked through *in substance* — a category filter is necessary, not sufficient.
`read_conversations` on the finalists (≤10 ids/call) to verify **every quote verbatim and correctly attributed** — customer vs bot vs human agent — and capture the deep link. A quote you could not confirm does not go in the report — list it as queued-for-verification instead.
Call `get_report_guide` before writing, then output an in-chat ranked list. Per item: identifier + deep link, `who_failed`, score, the one-line rationale, the quoted exchange. Lead with a one-line read on the day's dominant failure mode and the volume scanned per lane.
**Never reproduce a slur, even verbatim** — quote a clean sentence from the same message, or drop the case. **Never render a raw subject/email blob as a headline** — it can contain customer PII. Don't reproduce customer email addresses.
Append every surfaced identifier to {{output_directory}}/seen-cases.txt and write the run log to {{output_directory}}/run-YYYY-MM-DD.md (volumes per lane, hit counts, score distribution, lane B's substantive rate, anything that changed about the data).
## Step 4b — The HTML one-pager
Write {{output_directory}}/screenshot-risk-YYYY-MM-DD.html — a **local, self-contained** file (inline CSS, no external assets, no fonts, no scripts, no network calls). It mirrors the in-chat list. Structure: header + scope strip (lanes, windows, rows scored, verified hit count, date, exclusion list); a callout for the day's dominant failure mode; a funnel table (pool → scored → scoreable → verified hits); ranked cards (rank, identifier deep-linked only when a real link was captured, `who_failed` and `failure_mode` badges, one-line rationale, verified Customer/Agent quote pair); a structural-patterns section; a method-caveats table; and recommended actions labelled Quick win / Medium lift / Structural.
Rules: only verified quotes appear (unconfirmed cases go in a distinct list with **no quote text**); deep-link only real captured links, never construct a URL from an identifier; no customer names, emails or account identifiers anywhere; print-friendly single column ~800px; red for concerning numbers, green for improving, nothing colored purely as decoration; no ASCII/box tables and no "share" affordance. Then tell the user the path. Don't open it, serve it, or offer to post it.
## Step 5 — Daily cadence
Offer `/loop` or `CronCreate` **only after** a run the user is happy with, locking in the per-lane window offsets, row budget, score threshold and exclusion list. Say plainly that an unattended run cannot do Step 4's judgment calls or lane B's in-context reading at full quality.
## Verification
1. Confirm each lane's `where` returns non-zero **and** that each exclusion reduced the row count.
2. Re-measure lane B's substantive rate each run (real exchanges vs opened-and-abandoned) and record the transcript-omission rate.
3. Verify every quote on every finalist — verbatim text, speaker attribution, and message order. Not optional.
4. Confirm lane C enrichment still returns real content (not empty) — if it regresses, the visible-turn semantics may have changed.
5. **Negative control** — confirm a known-defensible low-CSAT case (correct denial, furious customer) scores **low**. If those rank high, the scoring prompt has collapsed into a CSAT proxy. This is the most important check.
6. Confirm zero excluded-category cases in the output (each lane via its own category column).
7. Re-run the same window twice — the second run should surface nothing new (de-dup works).
Rippit
Claude