Email Deliverability skill for Claude Code
From p=none to p=reject without losing real mail. Claude treats deliverability as an audit, not a guess. It inventories every service sending as your domain, reads DMARC aggregate reports with a bundled parser to find unaligned senders, fixes them with custom-domain DKIM, then moves the policy to quarantine and reject over full monthly cycles. It also checks Gmail, Yahoo and Outlook bulk rules, RFC 8058 unsubscribe, stream separation and warm-up.
- name
email-deliverability - size262 lines · ≈3,550 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Gets transactional and marketing email into the inbox and keeps it there - inventory of every service sending as the domain, SPF/DKIM/DMARC alignment, DMARC rollout from p=none to quarantine to reject driven by aggregate reports (RFC 9989), Gmail, Yahoo and Microsoft bulk sender rules, RFC 8058 one-click unsubscribe, subdomain and IP stream separation, complaint and bounce hygiene, domain and IP warm-up, BIMI, and diagnosis from headers and SMTP errors. Use when mail lands in spam, Gmail returns 5.7.26 or 4.7.x errors, setting up DMARC enforcement, preparing for bulk sender requirements, or warming a new domain. Try asking
- Our password reset emails go to spam at Gmail; figure out why and fix it
- Parse these DMARC reports and tell me what still fails before we move to p=reject
- Get our newsletter compliant with the Gmail and Yahoo bulk sender requirements
Use it when
- Mail lands in spam or Gmail returns 5.7.26, 5.7.32 or 4.7.28 errors
- Moving DMARC from p=none to quarantine or reject
- Preparing a domain for Gmail, Yahoo or Outlook bulk sender rules
- Launching a new sending domain, subdomain or dedicated IP
Skip it when
- You are wiring an email API, templates or bounce webhooks into the app - use transactional-email instead
- You are planning sequences, segments and copy - use email-campaigns instead
How to check the result
The DMARC report parser shows every known source aligned and passing, seed sends to Gmail, Outlook and Yahoo show aligned SPF, DKIM and DMARC in the headers, and a one-click unsubscribe POST returns 200.
The common mistake it prevents
Switching DMARC to p=reject after a week of reports, before monthly billing or quarterly newsletters from a forgotten tool have appeared, so legitimate mail starts bouncing.
Done well means: every legitimate source sending as your domain passes DMARC through an
aligned DKIM signature, the domain sits at p=reject (or quarantine with a reason),
marketing and transactional mail have separate subdomains and reputations, user-reported
spam stays under 0.1% at Gmail, unsubscribes and complaints are honored automatically,
and someone looks at DMARC reports and Postmaster data on a schedule. Authentication is
the entry ticket; reputation decides the folder.
Ground rules
- Find every sender before tightening anything: the app, the marketing platform, the CRM, support desk, billing, HR tools, Google Workspace or Microsoft 365. DMARC reports are how you find the ones nobody remembers.
- Align with DKIM, not SPF. SPF breaks on forwarding and ESPs often use their own
return-path domain; a DKIM signature with
d=on your domain survives both. - Never jump to
p=rejectwithout at least one full monthly cycle of clean reports. Monthly invoices and quarterly newsletters only show up once per cycle. - DNS changes on the production domain (SPF, DMARC policy, deleting DKIM selectors) can stop mail company-wide. Show the exact record diff and get the user’s confirmation before publishing.
- This skill covers infrastructure and reputation. Wiring a provider’s send API and
webhooks belongs to
transactional-email; campaign content and segmentation toemail-campaigns.
Mailbox provider requirements (verify yearly; they tighten)
| Requirement | Gmail (personal accounts) | Yahoo | Outlook.com consumer |
|---|---|---|---|
| Who is “bulk” | About 5,000+ messages/day to Gmail, counted per primary domain; status is permanent once reached | Bulk senders (no published number) | More than 5,000/day, enforced since May 5, 2025 |
| SPF and DKIM | All senders need SPF or DKIM; bulk needs both | Both | Both |
| DMARC | Bulk: published, p=none allowed, From aligned with SPF or DKIM | At least p=none, must pass | At least p=none, aligned |
| Spam rate | Keep below 0.1%, never reach 0.3% (Postmaster Tools) | Below 0.3% | Not published as a number |
| One-click unsubscribe | Marketing/subscribed mail, RFC 8058, honor within 48 hours | Required for marketing, honor within 2 days | Recommended |
| Other | TLS, valid forward and reverse DNS, RFC 5322 format, DKIM key >= 1024 bits (2048 recommended) | Non-compliant mail junked, then rejected with 550 5.7.515 |
Gmail says it ramped up enforcement from November 2025, moving non-compliant traffic from temporary (4.7.x) to permanent (5.7.x) failures. Above 0.3% spam rate a bulk sender loses eligibility for mitigation until it stays below 0.3% for 7 consecutive days. Gmail’s spam rate is measured against inboxed mail, so a low number while most mail goes to spam is not good news.
Workflow
- [ ] 1. Inventory senders and current DNS (SPF, DKIM selectors, DMARC, BIMI)
- [ ] 2. Publish DMARC p=none with rua reporting; collect 2 to 4 weeks
- [ ] 3. Analyze reports; fix alignment for each legitimate source
- [ ] 4. Separate streams: subdomains, ESP streams/IP pools, Feedback-ID
- [ ] 5. Bulk sender compliance: one-click unsubscribe, complaint and bounce suppression
- [ ] 6. Monitoring: Postmaster Tools, SNDS, Yahoo CFL, DMARC report review
- [ ] 7. Enforce: quarantine, then reject; sp and np for subdomains
- [ ] 8. Warm up any new domain or IP; optional BIMI
- [ ] 9. Verify with real headers at Gmail, Outlook and Yahoo
1. Inventory
dig +short TXT example.com | grep -i spf
dig +short TXT _dmarc.example.com
dig +short TXT news.example.com; dig +short TXT _dmarc.news.example.com
dig +short TXT s1._domainkey.example.com # repeat for each known selector
dig +short TXT default._bimi.example.com
List each service, the From domain it uses, its return-path (envelope) domain, and its
DKIM d= domain. The transactional-email skill ships a DNS checker script for SPF lookup
counts and record syntax.
2. DMARC with reporting
_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:[email protected]; adkim=r; aspf=r"
- Reports to a mailbox on another domain (for example a DMARC vendor) require that domain
to publish an authorization record at
example.com._report._dmarc.<vendor-domain>; vendors do this for you. - Volume can be large; a dedicated mailbox or a report-processing service beats a person’s inbox.
- RFC 9989 (2026) replaces RFC 7489:
pctis removed,t=ysignals test mode (receivers apply one level less strict),npsets policy for non-existent subdomains, and the organizational domain is found by a DNS tree walk instead of the Public Suffix List. Existingv=DMARC1records keep working.
3. Analyze and fix alignment
python3 scripts/dmarc_report.py reports/ --top 30
python3 scripts/dmarc_report.py reports/ --domain example.com --min-pass 98 --resolve
It reads .xml, .gz and .zip reports, prints the overall DMARC pass rate, and lists
sources by failing volume with DKIM/SPF alignment and raw results. It flags “DKIM passes
for a different domain” (the service signs with its own domain: enable custom-domain DKIM)
and “no authentication at all” (unknown sender or spoofing). Exit 1 when below --min-pass.
| Report shows | Meaning | Fix |
|---|---|---|
SPF pass for amazonses.com / sendgrid.net, DKIM pass for the ESP domain only | Service not configured for your domain | Add the ESP’s DKIM CNAMEs for your domain; optionally a custom MAIL FROM / return-path subdomain |
DKIM pass, d= your domain, DMARC pass | Correct | None |
| Your own IPs failing SPF | Missing include: or IP in SPF | Add it, staying within 10 DNS lookups |
| Unknown IPs, no auth, small volume | Spoofing or forwarding | Nothing to fix; enforcement will block spoofing |
| Known forwarder/mailing list failing | Forwarding breaks SPF, list footers break DKIM | Expected; ARC-sealing receivers handle much of it |
4. Stream separation
| Stream | From domain | Why |
|---|---|---|
| Transactional (receipts, resets, alerts) | example.com or mail.example.com | Highest reputation; users expect it |
| Marketing / newsletters | news.example.com | Complaints here must not sink password resets |
| Cold or partner outreach | A separate domain entirely, or not at all | Highest complaint risk |
- Each subdomain gets its own DKIM keys and return-path; DMARC at the organizational
domain covers subdomains via
sp=unless the subdomain publishes its own record. - Use separate ESP streams or pools (Postmark message streams, SES configuration sets or dedicated IP pools, SendGrid IP pools) so reputation does not mix.
- Dedicated IPs only with steady volume high enough to keep a reputation; low or bursty volume on a dedicated IP performs worse than a good shared pool. Ask the ESP for its minimum.
- Add a
Feedback-ID: a:b:c:SenderIdheader (optional campaign, customer and mail-type slots, then a fixed 5 to 15 character sender ID; never a per-message value) so Gmail’s Postmaster feedback loop can flag the stream with unusual spam rates. It must be added before DKIM signing.
5. Unsubscribe, complaints, bounces
One-click unsubscribe (RFC 8058) on every marketing or subscribed message:
List-Unsubscribe: <https://news.example.com/u/3f9c2a...signed-token>, <mailto:[email protected]>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
- The HTTPS endpoint accepts
POSTwith bodyList-Unsubscribe=One-Click(form-encoded or multipart), needs no cookie or login, returns 200 without redirecting, and is idempotent. - The token identifies recipient and list and is signed or random and stored; never a raw email address.
- The DKIM signature must cover both headers (
h=includes them); ESPs that add the headers normally sign them, but check a real message. - Keep a visible unsubscribe link in the body too. A
GETon that link shows a confirmation page; do not unsubscribe onGETalone, since security scanners prefetch links. - Transactional mail does not need one-click unsubscribe, but recipients decide what feels promotional; keep upsells out of receipts.
Complaints and bounces:
- Suppress complainers from all non-essential mail immediately. Sources: ESP webhooks, Yahoo Complaint Feedback Loop, Microsoft JMRP. Gmail has no per-message feedback loop; watch Postmaster Tools.
- Hard bounces: suppress permanently. Repeated soft bounces: suppress after a few consecutive attempts.
- Sunset unengaged recipients on marketing streams (no clicks, logins or purchases for a period you define, such as 6 to 12 months); opens are unreliable since Apple Mail Privacy Protection.
- Double opt-in on public forms; never import purchased or scraped lists (spam traps).
6. Monitoring
| Tool | Shows | Cadence |
|---|---|---|
| Google Postmaster Tools | Compliance status, spam rate, authentication for Gmail traffic | Weekly; daily during warm-up |
| Microsoft SNDS | IP-level data for Outlook.com (own IPs only) | Weekly with dedicated IPs |
| Yahoo Sender Hub / CFL | Complaints per message | Wire into suppression |
| DMARC aggregate reports | All sources sending as the domain | Weekly until reject, monthly after |
| Blocklists (Spamhaus and others) | Listings of your IPs/domains | On delivery drops |
7. Enforcement
p=quarantineonce all legitimate sources pass aligned DKIM and the pass rate for known sources is near 100% across a full monthly cycle. On receivers that follow RFC 9989,t=ylets you publish the stricter policy in test mode first; older receivers may still honorpct.p=rejectafter another clean cycle. Addsp=reject(or keep subdomain records) andnp=rejectto block made-up subdomains.- Keep monitoring: a new SaaS tool sending as the domain will now fail loudly. Document “how to add a new sender” (DKIM first) for the team.
8. Warm-up and BIMI
Warm-up for a new domain, subdomain or dedicated IP:
- Start with the most engaged recipients (recent clickers, active users) and transactional traffic; increase volume gradually day over day rather than sending the full list at once.
- Watch deferrals (
421 4.7.x), Postmaster spam rate and bounces daily. On deferrals, hold or reduce volume until they clear; never push through. - Use the ESP’s automated warm-up for dedicated IPs where available instead of inventing a schedule.
BIMI (logo in the inbox, Gmail requirements): DMARC p=quarantine or p=reject applied to
100% of mail, an SVG Tiny PS logo (baseProfile="tiny-ps", version="1.2", square, at least
96 px), and a VMC or CMC certificate:
default._bimi.example.com TXT "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/vmc.pem"
BIMI does not improve placement on its own; do it after enforcement, not instead of it.
Diagnosing from headers and errors
In Gmail use “Show original”; elsewhere read Authentication-Results:
Authentication-Results: mx.google.com;
dkim=pass [email protected] header.s=s1;
spf=pass smtp.mailfrom=bounces.example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
Check that header.from’s organizational domain matches header.i/d= (DKIM alignment)
or smtp.mailfrom (SPF alignment).
| Error | Meaning | Action |
|---|---|---|
421 4.7.28 (Gmail) | Unusual rate of unsolicited mail from your IP, DKIM or SPF domain | Slow down, check spam rate and recent list sources |
421 4.7.x other deferrals | Temporary reputation or rate limiting | Back off and retry; review recent changes |
550 5.7.26 (Gmail) | Unauthenticated, SPF hard fail, or rejected by your DMARC policy | Fix authentication and alignment for that source |
5.7.27 / 5.7.30 (Gmail) | Failed SPF / failed DKIM (both required for bulk senders) | Fix the failing mechanism for that source |
421 4.7.32 / 5.7.32 (Gmail) | From header not aligned with SPF or DKIM domain: rate limited / blocked | Sign with d= on the From domain |
550 5.7.515 (Outlook.com) | Domain does not meet required authentication level | SPF, DKIM and DMARC for the sending domain |
| Inbox at Outlook, spam at Gmail (or reverse) | Provider-specific reputation | Look at that provider’s tool; segment by provider |
Gotchas
- Second SPF record for a new tool: both become
permerror. Merge into one record. - SPF over 10 DNS lookups from stacked
include:s; remove unused vendors before flattening. - DMARC report mailbox on the same domain filtered as spam by your own gateway.
- DKIM key rotation that deletes the old selector before mail signed with it is delivered; publish the new selector, switch signing, remove the old one days later.
- Marketing platform sending from the root domain because nobody configured the subdomain.
- Unsubscribe endpoint behind login or bot protection: one-click POSTs fail silently.
- Re-mailing an old list after a long gap: bounce and trap hits spike on the first send.
Verification
dmarc_report.pyon the latest week of reports shows every known source aligned and passing; remaining failures are identified as forwarding or spoofing.- Seed sends to Gmail, Outlook.com and Yahoo inboxes show SPF, DKIM and DMARC pass with aligned domains in the headers, and land in the inbox.
- A one-click POST to the unsubscribe URL (
curl -X POST -d 'List-Unsubscribe=One-Click' <url>) returns 200 and suppresses the address within the 48-hour window. - Postmaster Tools compliance status shows passing checks and spam rate under 0.1%.
Output contract
## Deliverability audit: example.com (<date>)
- Senders found: <service -> From domain, DKIM d=, aligned yes/no>
- DNS: SPF <record, lookups n/10>; DKIM selectors <list>; DMARC <record>; BIMI <yes/no>
- DMARC pass rate (last 14 days): <x>% of <n> messages; failing sources: <list + cause>
- Streams: transactional <domain/stream>, marketing <domain/stream>
- Bulk sender compliance: one-click unsub <ok>, spam rate <x>%, complaints/bounces suppressed <ok>
- Plan: <dates for p=quarantine, p=reject, sp/np>; DNS changes awaiting approval: <diff> Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/dmarc_report.py
#!/usr/bin/env python3
"""Summarize DMARC aggregate (rua) reports: who sends as your domain, and what fails.
Usage:
python3 scripts/dmarc_report.py REPORT [REPORT ...] [--top 25] [--min-pass 98]
[--domain example.com] [--resolve]
REPORT may be an .xml, .xml.gz/.gz, or .zip file as mailbox providers send them, or a
directory containing such files. Handles RFC 7489 and RFC 9990 (namespaced) reports.
Output: per-report header, overall DMARC pass rate, and a table of sending sources
(source IP + header From domain) ordered by failing volume, with DKIM/SPF alignment
and the raw authentication results, so unaligned-but-passing services stand out.
Exit codes: 0 = parsed (and pass rate >= --min-pass if given), 1 = pass rate below
--min-pass, 2 = no readable reports.
"""
import argparse
import gzip
import io
import os
import socket
import sys
import xml.etree.ElementTree as ET
import zipfile
from collections import defaultdict
from datetime import datetime, timezone
MAX_BYTES = 50 * 1024 * 1024
def strip_ns(root):
for el in root.iter():
if isinstance(el.tag, str) and "}" in el.tag:
el.tag = el.tag.split("}", 1)[1]
return root
def text(el, path, default=""):
found = el.find(path) if el is not None else None
return (found.text or "").strip() if found is not None and found.text else default
def day(ts):
try:
return datetime.fromtimestamp(int(ts), timezone.utc).strftime("%Y-%m-%d")
except (TypeError, ValueError, OverflowError, OSError):
return ts or "?"
def read_payloads(path):
"""Yield (name, xml_bytes) for one file."""
lower = path.lower()
with open(path, "rb") as fh:
raw = fh.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
raise ValueError("file larger than 50 MB")
if lower.endswith(".zip") or raw[:4] == b"PK\x03\x04":
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
for info in zf.infolist():
if info.is_dir():
continue
if info.file_size > MAX_BYTES:
raise ValueError(f"{info.filename}: uncompressed size over 50 MB")
yield f"{path}:{info.filename}", zf.read(info)
elif lower.endswith(".gz") or raw[:2] == b"\x1f\x8b":
with gzip.GzipFile(fileobj=io.BytesIO(raw)) as gz:
data = gz.read(MAX_BYTES + 1)
if len(data) > MAX_BYTES:
raise ValueError("decompressed size over 50 MB")
yield path, data
else:
yield path, raw
def parse_report(name, data):
head = data[:4096].upper()
if b"<!DOCTYPE" in head or b"<!ENTITY" in data.upper():
raise ValueError("DTD/entity declarations are not allowed in DMARC reports")
root = strip_ns(ET.fromstring(data))
if root.tag != "feedback":
raise ValueError(f"root element is <{root.tag}>, expected <feedback>")
meta = root.find("report_metadata")
pol = root.find("policy_published")
report = {
"name": name,
"org": text(meta, "org_name", "?"),
"begin": text(meta, "date_range/begin"),
"end": text(meta, "date_range/end"),
"domain": text(pol, "domain"),
"policy": "p={} sp={} adkim={} aspf={}".format(
text(pol, "p", "?"), text(pol, "sp", "-"), text(pol, "adkim", "r"), text(pol, "aspf", "r")),
"records": [],
}
for rec in root.findall("record"):
row = rec.find("row")
try:
count = int(text(row, "count", "0"))
except ValueError:
count = 0
dkim_eval = text(row, "policy_evaluated/dkim", "fail")
spf_eval = text(row, "policy_evaluated/spf", "fail")
dkim_raw = ["{}({})={}".format(text(d, "domain", "?"), text(d, "selector", "-"), text(d, "result", "?"))
for d in rec.findall("auth_results/dkim")]
spf_raw = ["{}={}".format(text(s, "domain", "?"), text(s, "result", "?"))
for s in rec.findall("auth_results/spf")]
report["records"].append({
"ip": text(row, "source_ip", "?"),
"count": count,
"disposition": text(row, "policy_evaluated/disposition", "?"),
"dkim_aligned": dkim_eval == "pass",
"spf_aligned": spf_eval == "pass",
"header_from": text(rec, "identifiers/header_from", "?").lower(),
"dkim_raw": dkim_raw,
"spf_raw": spf_raw,
"dkim_any_pass": any(r.endswith("=pass") for r in dkim_raw),
"spf_any_pass": any(r.endswith("=pass") for r in spf_raw),
})
return report
def collect(paths):
files = []
for p in paths:
if os.path.isdir(p):
for dirpath, _, names in os.walk(p):
files += [os.path.join(dirpath, n) for n in sorted(names)
if n.lower().endswith((".xml", ".gz", ".zip"))]
else:
files.append(p)
return files
def main():
ap = argparse.ArgumentParser(description="Summarize DMARC aggregate reports")
ap.add_argument("reports", nargs="+")
ap.add_argument("--top", type=int, default=25, help="rows to show in the source table (default 25)")
ap.add_argument("--min-pass", type=float, default=None, help="exit 1 if overall DMARC pass %% is below this")
ap.add_argument("--domain", default=None, help="only count records whose header From is this domain")
ap.add_argument("--resolve", action="store_true", help="reverse-DNS each source IP (slow)")
args = ap.parse_args()
reports, errors = [], 0
for f in collect(args.reports):
try:
for name, data in read_payloads(f):
reports.append(parse_report(name, data))
except (OSError, ValueError, ET.ParseError, zipfile.BadZipFile, EOFError) as exc:
errors += 1
print(f"skip {f}: {exc}", file=sys.stderr)
if not reports:
print("error: no readable DMARC reports", file=sys.stderr)
sys.exit(2)
print(f"{len(reports)} report(s) parsed, {errors} skipped\n")
for r in reports:
print(f"- {r['org']:<28} {r['domain']:<24} {r['policy']:<36} {day(r['begin'])}..{day(r['end'])}")
agg = defaultdict(lambda: {"count": 0, "fail": 0, "dkim_al": 0, "spf_al": 0,
"disp": defaultdict(int), "dkim_raw": set(), "spf_raw": set(),
"dkim_unaligned_pass": 0, "spf_unaligned_pass": 0})
total = passed = 0
want = args.domain.lower() if args.domain else None
for r in reports:
for rec in r["records"]:
if want and rec["header_from"] != want:
continue
key = (rec["ip"], rec["header_from"])
a = agg[key]
n = rec["count"]
ok = rec["dkim_aligned"] or rec["spf_aligned"]
total += n
passed += n if ok else 0
a["count"] += n
a["fail"] += 0 if ok else n
a["dkim_al"] += n if rec["dkim_aligned"] else 0
a["spf_al"] += n if rec["spf_aligned"] else 0
a["disp"][rec["disposition"]] += n
a["dkim_raw"].update(rec["dkim_raw"])
a["spf_raw"].update(rec["spf_raw"])
if rec["dkim_any_pass"] and not rec["dkim_aligned"]:
a["dkim_unaligned_pass"] += n
if rec["spf_any_pass"] and not rec["spf_aligned"]:
a["spf_unaligned_pass"] += n
if total == 0:
print("\nno messages matched")
sys.exit(2)
rate = 100.0 * passed / total
print(f"\nmessages: {total} DMARC pass (aligned DKIM or SPF): {passed} ({rate:.2f}%) fail: {total - passed}")
if args.resolve:
socket.setdefaulttimeout(2)
print(f"\n{'source ip':<40} {'header from':<24} {'msgs':>7} {'fail':>7} {'dkim al':>8} {'spf al':>7} disposition / auth results")
rows = sorted(agg.items(), key=lambda kv: (-kv[1]["fail"], -kv[1]["count"]))
for (ip, hfrom), a in rows[: args.top]:
label = ip
if args.resolve:
try:
label = f"{ip} ({socket.gethostbyaddr(ip)[0]})"
except (OSError, socket.herror):
pass
disp = ",".join(f"{k}:{v}" for k, v in sorted(a["disp"].items()))
print(f"{label[:40]:<40} {hfrom[:24]:<24} {a['count']:>7} {a['fail']:>7} {a['dkim_al']:>8} {a['spf_al']:>7} {disp}")
print(f"{'':<40} dkim: {', '.join(sorted(a['dkim_raw'])) or 'none'} spf: {', '.join(sorted(a['spf_raw'])) or 'none'}")
hints = []
if a["dkim_unaligned_pass"]:
hints.append("DKIM passes for a different domain: set up custom-domain DKIM for this service")
if a["spf_unaligned_pass"] and not a["dkim_al"]:
hints.append("SPF passes for the provider's bounce domain only: needs aligned DKIM or a custom return-path")
if a["fail"] and not a["dkim_raw"] and not a["spf_raw"]:
hints.append("no authentication at all: unknown sender or spoofing")
for h in hints:
print(f"{'':<40} hint: {h}")
if len(rows) > args.top:
print(f"... {len(rows) - args.top} more source(s); raise --top to see them")
if args.min_pass is not None and rate < args.min_pass:
print(f"\nFAIL: pass rate {rate:.2f}% is below --min-pass {args.min_pass}%")
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.