[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-posthog-signals-scout-csp-violations":3,"mdc--m40f5q-key":38,"related-org-posthog-signals-scout-csp-violations":2866,"related-repo-posthog-signals-scout-csp-violations":3037},{"slug":4,"name":4,"fn":5,"description":6,"org":7,"tags":11,"stars":23,"repoUrl":24,"updatedAt":25,"license":26,"forks":27,"topics":28,"repo":33,"sourceUrl":36,"mdContent":37},"signals-scout-csp-violations","monitor CSP violations in PostHog","Signals scout for Content Security Policy violation reports. Watches `$csp_violation` events for blocked-URL clusters, per-directive bursts, post-deploy regressions, and suspicious third-party domains, and files each validated cluster as a report in the inbox.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},"posthog","PostHog","https:\u002F\u002Fpexgzepcugksgbtrxkhf.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Forg-logos\u002Fposthog.png",[12,16,19,20],{"name":13,"slug":14,"type":15},"Security","security","tag",{"name":17,"slug":18,"type":15},"Observability","observability",{"name":9,"slug":8,"type":15},{"name":21,"slug":22,"type":15},"Monitoring","monitoring",59,"https:\u002F\u002Fgithub.com\u002FPostHog\u002Fai-plugin","2026-07-18T05:11:41.432787",null,11,[29,30,31,32],"claude-code-plugin","codex-plugin","cursor-plugin","gemini-cli-extension",{"repoUrl":24,"stars":23,"forks":27,"topics":34,"description":35},[29,30,31,32],"Official PostHog plugin for Claude Code, Cursor, Gemini, Codex and other AI coding tools","https:\u002F\u002Fgithub.com\u002FPostHog\u002Fai-plugin\u002Ftree\u002FHEAD\u002Fskills\u002Fsignals-scout-csp-violations","---\nname: signals-scout-csp-violations\ndescription: >\n  Signals scout for Content Security Policy violation reports. Watches `$csp_violation` events\n  for blocked-URL clusters, per-directive bursts, post-deploy regressions, and suspicious\n  third-party domains, and files each validated cluster as a report in the inbox.\ncompatibility: >\n  PostHog Signals agent (Claude sandbox). Read-only analytics + signal_scout_internal:write\n  (scratchpad) + signal_scout_report:write (report channel), plus the analytics tools in the\n  MCP tools section (execute-sql over `$csp_violation` events, read-data-schema,\n  advanced-activity-logs-list).\nallowed_tools:\n  - emit_report\n  - edit_report\nmetadata:\n  owner_team: signals\n  scope: csp_violations\n  credits: pauldambra (PR #58596 — push-based CSP signal emission, encoded here as pull)\n---\n\n# Signals scout: CSP violations\n\nYou are a focused CSP scout. Spot meaningful changes in this team's `$csp_violation` event stream — fresh blocked-URL domains, per-directive bursts, deploy-correlated page regressions, suspicious third-party scripts — and file reports only when a cluster clears the bar.\n\nCSP violations are unusual on the noise\u002Fsignal spectrum: a single user with a misbehaving browser extension can pollute thousands of reports, while a genuine script compromise might surface as five carefully crafted requests from a fresh domain. **Reach (distinct users + distinct documents) matters more than raw count**. Internalize that shape.\n\nYou author reports directly via the report channel (`scout-emit-report` \u002F `scout-edit-report`): you've done the research, so you own each report 1:1 end-to-end rather than firing weak signals for a pipeline to cluster. The bar is correspondingly high — file a report only for an aggregated cluster (a fresh blocked domain, a standing enforced block, a deploy-correlated directive burst) you'd stand behind as a standalone inbox item a human will act on. A cluster the inbox already covers that's still active (or recovered then relapsed) is an **edit**, not a new report. The harness prompt carries the full report-channel contract (fields, status mapping, reviewer routing, dedupe, the `priority` \u002F `repository` fields, and the edit rules), and `authoring-scouts` → `references\u002Freport-contract.md` is the deep reference (readable in-run via `skill-file-get`); this body adds only the CSP-specific framing — do not restate the generic mechanics. (Note: this surface has a companion **push** path that files raw per-fingerprint signals under `source_product=csp_reporting`; your own report-channel reports persist under `source_product=signals_scout`. Both live in the same inbox — see Decide for how they interact.)\n\n## Quick close-out: is CSP reporting even active?\n\nIf `$csp_violation` is absent from `top_events` or its `count` is at baseline (no fresh 24h activity, `recent_24h_count` ≪ `count \u002F 7`), CSP reporting probably isn't where the signal is today. Cheap scratchpad entry + close out:\n\n- key: `pattern:csp_violations:baseline-team{team_id}`\n- content: `\"$csp_violation baseline ~{count}\u002Fday, no fresh 24h burst at {timestamp}\"`\n\n**Before** taking the baseline close-out, run the [standing enforced \u002F first-party block](#standing-enforced--first-party-block-no-freshness-required) check below. \"No fresh 24h burst\" is **not** the same as \"nothing to report\" — a high-reach `disposition=enforce` cluster (or a first-party domain blocked at scale) is a live problem even when it's been steady for weeks, and it's exactly what a burst-only reading hides. Only close out as baseline once that check is also clean.\n\nIf `$csp_violation` is absent from `top_events` entirely (project doesn't ship a CSP reporting endpoint at all):\n\n- key: `not-in-use:csp_violations:team{team_id}`\n- content: brief note (`\"no $csp_violation events in 7d window at {timestamp}\"`)\n\nClose out empty in both cases. Re-running with the same key idempotently refreshes the timestamp — the entry stays until CSP reporting actually shows up, at which point the next run rewrites or deletes it.\n\n## How a run works\n\nCycle between these moves; skip what's not useful.\n\n### Get oriented\n\nFour cheap reads cold-start a run:\n\n- `scout-scratchpad-search` (`text=csp` or `text=blocked`) — durable team steering from past CSP runs. Entries with `pattern:`, `noise:`, `addressed:`, `dedupe:`, `allowlist:`, `report:`, or `reviewer:` key prefixes tell you the team's healthy domains, recurring browser-extension noise, clusters already surfaced, which report covers a cluster, who owns a surface, and what to skip.\n- `scout-runs-list` (last 7d) — what prior CSP scouts found and ruled out.\n- `scout-project-profile-get` — the `$csp_violation` row in `top_events` carries `count`, `distinct_users`, `recent_24h_count`, `recent_24h_users`, plus `existing_inbox_reports`. Pattern the count\u002Fusers ratio against the table below.\n- `inbox-reports-list` (`ordering=-updated_at`, `search`=the blocked domain \u002F directive) — the reports already in the inbox. **Two source_products matter here:** your own report-channel reports persist under `source_product=signals_scout` (search these for edit-vs-author — don't filter them out), while the companion push path files raw per-fingerprint signals under `source_product=csp_reporting` (check these to stay quiet when the push path already covers a cluster — see Decide). A cluster you've reported before is an **edit**, not a fresh report; pull the closest matches with `inbox-reports-retrieve` before authoring.\n\n### Profile shape — count vs distinct_users\n\n| Pattern                                                 | What it usually means                                               |\n| ------------------------------------------------------- | ------------------------------------------------------------------- |\n| Both `count` and `distinct_users` spike in 24h          | Fresh broad-impact CSP regression — deploy missed an allowlist      |\n| `recent_24h_count \u002F count` ≫ `1\u002F7`, users also spike    | Today's burst is unusually broad — investigate first                |\n| `count` very high, `distinct_users` very low (≤ 5)      | Single user \u002F bot \u002F browser extension — usually skip                |\n| `count` ~ `distinct_users` for one blocked URL          | Per-pageload violation hitting every visitor — broken policy        |\n| Steady high `count` across many users + many directives | Mature CSP policy in `report-only` mode — high baseline expected    |\n| Steady high reach on one `enforce` \u002F first-party domain | **Standing block** — live breakage; report even with no fresh burst |\n| `count` and `distinct_users` both quiet                 | Nothing fresh today — close out                                     |\n\n### Explore\n\nPatterns to watch — starting points, not a checklist. Group violations along four dimensions and look for clusters worth a finding. PostHog's push-based CSP emission already deduplicates _individual_ violations at `sha1(violated_directive | blocked_url | document_url | source_file)` granularity with a 24h Redis TTL; your job is to _aggregate_ across that grain into higher-confidence findings the inbox wouldn't surface on its own.\n\n#### Fresh blocked-URL domain\n\nThe single highest-value CSP pattern. Group by `domain(properties.$csp_blocked_url)` over the last 24–48h. A domain with `first_seen` inside the window, ≥ 10 distinct pageviews, and not in the team's `allowlist`-tagged memory is the strongest scout signal.\n\n```sql\nSELECT\n    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,\n    count() AS occurrences,\n    uniq(person_id) AS distinct_users,\n    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents,\n    min(timestamp) AS first_seen,\n    max(timestamp) AS last_seen,\n    groupArray(DISTINCT JSONExtractString(properties, '$csp_effective_directive'))[1:5] AS directives\nFROM events\nWHERE event = '$csp_violation'\n  AND timestamp > now() - INTERVAL 48 HOUR\n  AND JSONExtractString(properties, '$csp_blocked_url') != ''\nGROUP BY blocked_domain\nHAVING first_seen > now() - INTERVAL 24 HOUR\n   AND distinct_users >= 10\n   AND blocked_domain != ''   -- drop inline\u002Feval\u002Fextension reports ('unsafe-inline' etc.): non-empty URL but no domain\nORDER BY occurrences DESC\nLIMIT 20\n```\n\nThree lenses for triage — every blocked-URL finding should name which one fits:\n\n1. **Legitimate — CSP policy needs widening.** New CDN, new analytics provider, new marketing tag the team rolled out and forgot to add to the allowlist.\n2. **Compromised — injected or third-party script indicating a security incident.** Fresh domain nobody recognizes, especially script-src violations on a small number of high-traffic pages, especially with `disposition=enforce` and a `source_file` that points at the team's own JS bundle.\n3. **Third-party drift — vendor script the team should remove.** Old analytics SDK still loaded from a deprecated bundle, ad pixel from a churned vendor, etc.\n\nFile a report only when one of these lenses fits with high confidence. If you're genuinely unsure which of the three it is, write a `pattern:csp_violations:\u003Centity>` scratchpad entry for the next run and close out.\n\n#### Standing enforced \u002F first-party block (no freshness required)\n\nThe fresh-domain query above only fires for domains that **first appeared in the last 24h** (`first_seen > now() - INTERVAL 24 HOUR`). A policy that has been enforce-blocking a real endpoint for weeks never trips it, and its steady volume reads as \"baseline\" and closes out — so a high-reach, actively-enforced block can sit invisible indefinitely. This is the scout's biggest blind spot. Two **standing** patterns deserve a finding even with zero freshness, because they are breaking functionality for real users _right now_:\n\n1. **High-reach enforced block.** A `disposition=enforce` blocked domain with broad reach (many distinct users _and_ documents) is not baseline noise — it is a live, enforced block degrading those users. Surface it regardless of when it first appeared.\n2. **First-party \u002F own-infra block.** A blocked domain that is the team's own surface (the blocked host equals or is a subdomain of a `$csp_document_url` host, or a known first-party domain) with high reach is an allowlist gap in the team's _own_ policy — a near-certain \"widen the policy\" fix.\n\n```sql\nSELECT\n    JSONExtractString(properties, '$csp_disposition') AS disposition,\n    JSONExtractString(properties, '$csp_effective_directive') AS directive,\n    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,\n    count() AS occurrences_7d,\n    uniq(person_id) AS distinct_users,\n    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents\nFROM events\nWHERE event = '$csp_violation'\n  AND timestamp > now() - INTERVAL 7 DAY\n  AND JSONExtractString(properties, '$csp_blocked_url') != ''\nGROUP BY disposition, directive, blocked_domain\nHAVING distinct_users >= 100          -- broad reach, not a single user \u002F extension\n   AND blocked_domain != ''           -- exclude inline\u002Feval\u002Fextension noise so named domains fill the limit\nORDER BY (disposition = 'enforce') DESC, distinct_users DESC\nLIMIT 30\n```\n\nTriage:\n\n- **Enforce + high reach** → report; these users are actively blocked. Highest priority when the directive is `script-src` \u002F `connect-src` (breaks behaviour, not just styling).\n- **First-party blocked domain** (own CDN, status page, replay proxy, internal endpoint) → file a report as \"policy allowlist gap — add `{domain}` to `{directive}`\". One report per domain.\n- **Third-party, report-only, high reach but stable** → report-only refinement case; remember (`pattern:`\u002F`allowlist:`) rather than report, unless it's a fresh domain (that's the fresh-domain path above).\n\nThe `blocked_domain != ''` filter already drops the giant inline \u002F `eval` \u002F `unsafe-inline` and browser-extension clusters (non-empty `$csp_blocked_url`, empty `domain()`) — the baseline noise this surface always carries — so the limit is spent on the reach that matters: **named** domains. Dedupe standing reports with `addressed:csp_violations:{blocked_domain}-{directive}` so a confirmed-and-allowlisted (or accepted) block doesn't re-surface every run.\n\n#### Per-directive burst\n\nGroup by `properties.$csp_effective_directive`. A directive whose recent 24h count is materially above its 7d-prior baseline (≥ 3×) with reach across multiple documents is a strong \"policy regression after deploy\" signal. Pair with `advanced-activity-logs-list` filtered to the last 24–48h — a deploy or hog-flow change correlating to the burst timestamp is the clean cross-source convergence.\n\nTop directives to expect (rough share-of-violations on a typical SPA): `script-src`, `script-src-elem`, `img-src`, `style-src`, `connect-src`, `frame-src`. `script-src` violations are weighted highest for security relevance; `img-src` and `style-src` more often indicate vendor \u002F CDN drift.\n\n#### Document-scoped regression\n\nGroup by `properties.$csp_document_url`. A document with no violations in the 7d-prior window and a sudden burst in the recent 24h is almost always a deploy regression on that route — a new script tag or inline style that the existing policy doesn't allow. High-value finding when the document is a critical funnel page (`\u002Fcheckout`, `\u002Fsignup`, `\u002Flogin`).\n\n#### Stuck loop \u002F single-user noise\n\n`count` very high but `distinct_users` ≤ 5 over the recent window. Almost always a single user with a misbehaving browser extension, or a bot probing the page. Skip — write a `noise:csp_violations:\u003Cblocked_domain>` scratchpad entry so future runs short-circuit.\n\nCommon skippable patterns:\n\n- `chrome-extension:\u002F\u002F` \u002F `moz-extension:\u002F\u002F` \u002F `safari-extension:\u002F\u002F` blocked URLs\n- Brave \u002F DuckDuckGo \u002F privacy-browser injected scripts\n- `about:blank`, `data:` URIs from translation tooling or password managers\n\n#### Disposition shift\n\nGroup by `properties.$csp_disposition`. A team running `report-only` for a long time and then flipping to `enforce` will see violations turn into actual blocks. If the project profile shows `count` for `disposition='enforce'` rising sharply (`recent_24h_count` materially above baseline) while `report-only` shows a corresponding fall, the team has flipped enforcement — write a `pattern:csp_violations:disposition-flip` scratchpad entry and file a report only if a critical page is suddenly seeing enforced blocks.\n\n### Save memory as you go\n\nMemory is a continuous activity. Write a scratchpad entry whenever you observe something a future CSP run should know. Encode the \"category\" in the key prefix — `pattern:`, `noise:`, `addressed:`, `dedupe:`, `allowlist:` — so future runs find it with a single `text=` search:\n\n- key `pattern:csp_violations:baseline` — _\"Project's healthy `$csp_violation` baseline: ~800\u002Fday across ~120 distinct users, mostly `img-src` from `*.googletagmanager.com` and `*.googlesyndication.com`. Anything above 1.5× this baseline is fresh.\"_\n- key `allowlist:csp_violations:gtm` — _\"`*.googletagmanager.com`, `*.googlesyndication.com`, `*.doubleclick.net` are the team's expected analytics\u002Fads domains — known, vetted, do not re-surface.\"_\n- key `noise:csp_violations:chrome-extension-scheme` — _\"Blocked URL pattern `chrome-extension:\u002F\u002F*` is a recurring browser-extension noise source for this team — skip unless `disposition=enforce` and `effective_directive=script-src`.\"_\n- key `addressed:csp_violations:cdn.suspicious.example.com` — _\"Surfaced fresh `script-src` cluster from `cdn.suspicious.example.com` on 2026-05-12; team confirmed it was a legitimate new vendor, allowlisted in policy on 2026-05-13. Do not re-file unless the domain re-appears after policy was widened.\"_\n- key `dedupe:csp_violations:a1b2c3d4` — _\"Fingerprint `a1b2c3d4...` (`script-src` | `evil.example.com\u002Fx.js` | `\u002Fcheckout` | `bundle.js`) — surfaced 2026-05-08, report still open in inbox. If this exact fingerprint fires again, edit the existing report; don't author fresh.\"_\n- key `report:csp_violations:\u003Cblocked_domain>-\u003Cdirective>` — _the `report_id` of a report you filed for a cluster on this domain\u002Fdirective, so the next run edits it (append_note with the fresh reach) instead of duplicating._\n- key `reviewer:csp_violations:\u003Carea>` — _a resolved owner (bare lowercase GitHub login) for the security \u002F frontend \u002F policy surface, so reports route to a human faster._\n\nBy run #5 you'll have a per-team domain allowlist in the scratchpad, known browser-extension noise patterns, and the typical per-directive shape — and burn near-zero time on cold-start exploration.\n\n### Decide\n\nThe generic report mechanics — searching the inbox for your own prior reports (via the `report:csp_violations:*` pointer, else an `inbox-reports-list` search on the specific blocked domain \u002F directive, not a broad word like `script-src`), edit-vs-author, the status rules, reviewer routing, non-idempotent dedup, and the `priority` \u002F `repository` fields — live in the harness prompt and in `authoring-scouts` → `references\u002Freport-contract.md`. Do not re-derive them here. This section is only the CSP judgment layered on top:\n\n- **Edit** when a still-live report already tracks the domain\u002Fdirective cluster — a fresh domain still blocked, an enforced block still degrading users, a directive burst still elevated. A persistent cluster is one report across runs: a new window confirming it's ongoing is a re-escalation (`append_note` the fresh reach \u002F occurrences), not a fresh report per tick.\n- **Author** when nothing live covers the cluster. A report-worthy finding names the blocked domain, the effective directive(s), the document URL(s), the distinct-user count, and a time range in the `evidence`, with an explicit lens (policy widen \u002F compromise \u002F vendor drift). These are investigations, not code fixes → `actionability=requires_human_input` + `repository=NO_REPO`. Priority: a `disposition=enforce` block on a `script-src` \u002F `connect-src` directive with broad reach, or a suspected compromise, is **P1–P2** (functionality broken \u002F possible security incident); a policy-allowlist-gap or vendor-drift finding is **P2–P3** by reach. After authoring, write the `report:csp_violations:\u003Cdomain>-\u003Cdirective>` pointer so the next run edits it.\n- **Remember** if below the bar but worth carrying forward (a fresh domain with only 3 distinct users — let it ripen), or to record what you ruled out.\n- **Skip** with a one-line note if a `noise:` \u002F `allowlist:` \u002F `addressed:` \u002F `dedupe:` entry, or an existing inbox report, already covers it.\n\n**The push path is the key dedupe partner.** The companion push emission (`source_product=csp_reporting`) already drops one raw signal per violation fingerprint into the same inbox. Cross-check it (`inbox-reports-list` filtered to `source_product=csp_reporting`) before authoring: your aggregated report should **reference those raw signals as evidence** (by fingerprint) rather than re-state them, and stay quiet when a single raw fingerprint already covers the whole story — author only when the aggregation adds cross-fingerprint context the push path can't see.\n\n### Close out\n\n**Summarize the run** — one paragraph: looked at what, which reports you authored or edited, remembered what, ruled out what. The harness writes that summary to the run row as searchable prose; future runs read it via `scout-runs-list`. Do **not** write a separate \"run metadata\" scratchpad entry — the run summary already serves that role.\n\n## Disqualifiers (skip these)\n\n- **Single user, single document, single fingerprint** — almost always a personal browser extension or a niche client. Low `count` AND `distinct_users` ≤ 2.\n- **Blocked URL scheme is `chrome-extension:\u002F\u002F` \u002F `moz-extension:\u002F\u002F` \u002F `about:` \u002F `data:`** — browser-side, not server-side; team can't fix.\n- **Domain matches an `allowlist:` scratchpad entry** — the team has already vetted this vendor; skip without re-surfacing.\n- **`disposition=report-only` with no enforcement signal** — the team is deliberately collecting violations to refine policy. File a report only when reach \u002F freshness \u002F domain novelty is exceptional.\n- **Fingerprint matches a `dedupe:` scratchpad entry from an open inbox report** — the push-emission path already covered it; don't double-up.\n- **Team has no `signal_source_config` row for `csp_reporting`** — push emission is off for this team. Scout can still find clusters, but the user signal is \"team hasn't opted in to CSP signals yet\"; raise the bar accordingly — require exceptional reach before filing.\n\nWhen in doubt, write a memory entry instead of filing a report.\n\n## MCP tools\n\nDirect calls (read-only):\n\n- `execute-sql` against `events` (filtered to `event = '$csp_violation'`) — primary drill-down. Group by `domain($csp_blocked_url)`, `$csp_effective_directive`, `$csp_document_url`, `$csp_source_file`. The full property list is in `posthog\u002Fapi\u002Fcsp.py`.\n- `read-data-schema` (`kind: event_properties`, `event_name: '$csp_violation'`) — discover the team's actual `$csp_*` property surface and sample values.\n- `advanced-activity-logs-list` — pair burst timestamps with recent deploys or feature-flag changes for cross-source convergence. Inbox & reviewer routing (mechanics in `authoring-scouts` → `references\u002Freport-contract.md`):\n\n- `inbox-reports-list` \u002F `inbox-reports-retrieve` — the reports already in the inbox. Check your own prior reports (`source_product=signals_scout`) so you edit instead of duplicating, and the push path's raw signals (`source_product=csp_reporting`) so you don't re-state a fingerprint it already covers.\n- `inbox-report-artefacts-list` — a comparable report's artefact log; reviewer precedent.\n- `scout-members-list` — the in-run roster for routing `suggested_reviewers` to a security \u002F frontend \u002F policy owner.\n\nHarness-level:\n\n- `scout-project-profile-get` \u002F `scout-scratchpad-search` \u002F `scout-runs-list` \u002F `scout-runs-retrieve` — orientation + dedupe.\n- `scout-emit-report` \u002F `scout-edit-report` — author a report \u002F edit an existing one (the report-channel contract is in the harness prompt).\n- `scout-scratchpad-remember` — remember.\n\n## When to stop\n\n- `$csp_violation` row in profile is at baseline **and** the standing enforced \u002F first-party block check is clean → close out empty. A steady baseline alone is not enough — a standing high-reach enforced (or first-party) block is a live problem even with no fresh burst.\n- A candidate matches a scratchpad entry with `noise:` \u002F `allowlist:` \u002F `addressed:` \u002F `dedupe:` key prefix, or an existing inbox report → edit-or-skip with a one-line note.\n- You've validated some hypotheses and filed reports for what's solid → close out, even if there's more you could look at. Fewer, better reports.\n\n\"Looked but found nothing meaningful\" is a real outcome.\n\n## How this relates to the push-based CSP source\n\nThe companion push path (`posthog\u002Ftasks\u002Fcsp_signal.py`, behind per-team `SignalSourceConfig` opt-in) emits **one raw signal per unique violation fingerprint** with a 24h Redis dedup TTL. That gives the inbox raw coverage of every fresh `(directive, blocked_url, document_url, source_file)` tuple, but per-fingerprint and without cross-fingerprint context.\n\nThis scout is the **aggregation layer above it.** Its reports should:\n\n- Bundle multiple raw fingerprints into a single aggregated report with shared root cause (one new domain across many pages, one deploy regression across many directives, one compromise pattern across many users).\n- Use the push path's existing signals as evidence in the report's body (referenced by fingerprint \u002F source_id) rather than re-deriving them.\n- Stay quiet when the push path's coverage is sufficient — a single raw fingerprint already in the inbox does not need a parallel scout report unless the aggregation adds new context.\n",{"data":39,"body":48},{"name":4,"description":6,"compatibility":40,"allowed_tools":41,"metadata":44},"PostHog Signals agent (Claude sandbox). Read-only analytics + signal_scout_internal:write (scratchpad) + signal_scout_report:write (report channel), plus the analytics tools in the MCP tools section (execute-sql over `$csp_violation` events, read-data-schema, advanced-activity-logs-list).\n",[42,43],"emit_report","edit_report",{"owner_team":45,"scope":46,"credits":47},"signals","csp_violations","pauldambra (PR",{"type":49,"children":50},"root",[51,59,74,87,177,184,228,255,289,306,332,337,343,348,355,360,573,579,789,795,823,830,859,1033,1038,1087,1100,1106,1139,1191,1320,1325,1403,1462,1468,1489,1555,1561,1595,1601,1626,1631,1682,1688,1751,1757,1801,2047,2052,2058,2111,2253,2291,2297,2320,2326,2461,2466,2472,2477,2659,2664,2723,2729,2783,2788,2794,2830,2842,2860],{"type":52,"tag":53,"props":54,"children":55},"element","h1",{"id":4},[56],{"type":57,"value":58},"text","Signals scout: CSP violations",{"type":52,"tag":60,"props":61,"children":62},"p",{},[63,65,72],{"type":57,"value":64},"You are a focused CSP scout. Spot meaningful changes in this team's ",{"type":52,"tag":66,"props":67,"children":69},"code",{"className":68},[],[70],{"type":57,"value":71},"$csp_violation",{"type":57,"value":73}," event stream — fresh blocked-URL domains, per-directive bursts, deploy-correlated page regressions, suspicious third-party scripts — and file reports only when a cluster clears the bar.",{"type":52,"tag":60,"props":75,"children":76},{},[77,79,85],{"type":57,"value":78},"CSP violations are unusual on the noise\u002Fsignal spectrum: a single user with a misbehaving browser extension can pollute thousands of reports, while a genuine script compromise might surface as five carefully crafted requests from a fresh domain. ",{"type":52,"tag":80,"props":81,"children":82},"strong",{},[83],{"type":57,"value":84},"Reach (distinct users + distinct documents) matters more than raw count",{"type":57,"value":86},". Internalize that shape.",{"type":52,"tag":60,"props":88,"children":89},{},[90,92,98,100,106,108,113,115,121,122,128,130,136,138,144,146,152,154,159,161,167,169,175],{"type":57,"value":91},"You author reports directly via the report channel (",{"type":52,"tag":66,"props":93,"children":95},{"className":94},[],[96],{"type":57,"value":97},"scout-emit-report",{"type":57,"value":99}," \u002F ",{"type":52,"tag":66,"props":101,"children":103},{"className":102},[],[104],{"type":57,"value":105},"scout-edit-report",{"type":57,"value":107},"): you've done the research, so you own each report 1:1 end-to-end rather than firing weak signals for a pipeline to cluster. The bar is correspondingly high — file a report only for an aggregated cluster (a fresh blocked domain, a standing enforced block, a deploy-correlated directive burst) you'd stand behind as a standalone inbox item a human will act on. A cluster the inbox already covers that's still active (or recovered then relapsed) is an ",{"type":52,"tag":80,"props":109,"children":110},{},[111],{"type":57,"value":112},"edit",{"type":57,"value":114},", not a new report. The harness prompt carries the full report-channel contract (fields, status mapping, reviewer routing, dedupe, the ",{"type":52,"tag":66,"props":116,"children":118},{"className":117},[],[119],{"type":57,"value":120},"priority",{"type":57,"value":99},{"type":52,"tag":66,"props":123,"children":125},{"className":124},[],[126],{"type":57,"value":127},"repository",{"type":57,"value":129}," fields, and the edit rules), and ",{"type":52,"tag":66,"props":131,"children":133},{"className":132},[],[134],{"type":57,"value":135},"authoring-scouts",{"type":57,"value":137}," → ",{"type":52,"tag":66,"props":139,"children":141},{"className":140},[],[142],{"type":57,"value":143},"references\u002Freport-contract.md",{"type":57,"value":145}," is the deep reference (readable in-run via ",{"type":52,"tag":66,"props":147,"children":149},{"className":148},[],[150],{"type":57,"value":151},"skill-file-get",{"type":57,"value":153},"); this body adds only the CSP-specific framing — do not restate the generic mechanics. (Note: this surface has a companion ",{"type":52,"tag":80,"props":155,"children":156},{},[157],{"type":57,"value":158},"push",{"type":57,"value":160}," path that files raw per-fingerprint signals under ",{"type":52,"tag":66,"props":162,"children":164},{"className":163},[],[165],{"type":57,"value":166},"source_product=csp_reporting",{"type":57,"value":168},"; your own report-channel reports persist under ",{"type":52,"tag":66,"props":170,"children":172},{"className":171},[],[173],{"type":57,"value":174},"source_product=signals_scout",{"type":57,"value":176},". Both live in the same inbox — see Decide for how they interact.)",{"type":52,"tag":178,"props":179,"children":181},"h2",{"id":180},"quick-close-out-is-csp-reporting-even-active",[182],{"type":57,"value":183},"Quick close-out: is CSP reporting even active?",{"type":52,"tag":60,"props":185,"children":186},{},[187,189,194,196,202,204,210,212,218,220,226],{"type":57,"value":188},"If ",{"type":52,"tag":66,"props":190,"children":192},{"className":191},[],[193],{"type":57,"value":71},{"type":57,"value":195}," is absent from ",{"type":52,"tag":66,"props":197,"children":199},{"className":198},[],[200],{"type":57,"value":201},"top_events",{"type":57,"value":203}," or its ",{"type":52,"tag":66,"props":205,"children":207},{"className":206},[],[208],{"type":57,"value":209},"count",{"type":57,"value":211}," is at baseline (no fresh 24h activity, ",{"type":52,"tag":66,"props":213,"children":215},{"className":214},[],[216],{"type":57,"value":217},"recent_24h_count",{"type":57,"value":219}," ≪ ",{"type":52,"tag":66,"props":221,"children":223},{"className":222},[],[224],{"type":57,"value":225},"count \u002F 7",{"type":57,"value":227},"), CSP reporting probably isn't where the signal is today. Cheap scratchpad entry + close out:",{"type":52,"tag":229,"props":230,"children":231},"ul",{},[232,244],{"type":52,"tag":233,"props":234,"children":235},"li",{},[236,238],{"type":57,"value":237},"key: ",{"type":52,"tag":66,"props":239,"children":241},{"className":240},[],[242],{"type":57,"value":243},"pattern:csp_violations:baseline-team{team_id}",{"type":52,"tag":233,"props":245,"children":246},{},[247,249],{"type":57,"value":248},"content: ",{"type":52,"tag":66,"props":250,"children":252},{"className":251},[],[253],{"type":57,"value":254},"\"$csp_violation baseline ~{count}\u002Fday, no fresh 24h burst at {timestamp}\"",{"type":52,"tag":60,"props":256,"children":257},{},[258,263,265,272,274,279,281,287],{"type":52,"tag":80,"props":259,"children":260},{},[261],{"type":57,"value":262},"Before",{"type":57,"value":264}," taking the baseline close-out, run the ",{"type":52,"tag":266,"props":267,"children":269},"a",{"href":268},"#standing-enforced--first-party-block-no-freshness-required",[270],{"type":57,"value":271},"standing enforced \u002F first-party block",{"type":57,"value":273}," check below. \"No fresh 24h burst\" is ",{"type":52,"tag":80,"props":275,"children":276},{},[277],{"type":57,"value":278},"not",{"type":57,"value":280}," the same as \"nothing to report\" — a high-reach ",{"type":52,"tag":66,"props":282,"children":284},{"className":283},[],[285],{"type":57,"value":286},"disposition=enforce",{"type":57,"value":288}," cluster (or a first-party domain blocked at scale) is a live problem even when it's been steady for weeks, and it's exactly what a burst-only reading hides. Only close out as baseline once that check is also clean.",{"type":52,"tag":60,"props":290,"children":291},{},[292,293,298,299,304],{"type":57,"value":188},{"type":52,"tag":66,"props":294,"children":296},{"className":295},[],[297],{"type":57,"value":71},{"type":57,"value":195},{"type":52,"tag":66,"props":300,"children":302},{"className":301},[],[303],{"type":57,"value":201},{"type":57,"value":305}," entirely (project doesn't ship a CSP reporting endpoint at all):",{"type":52,"tag":229,"props":307,"children":308},{},[309,319],{"type":52,"tag":233,"props":310,"children":311},{},[312,313],{"type":57,"value":237},{"type":52,"tag":66,"props":314,"children":316},{"className":315},[],[317],{"type":57,"value":318},"not-in-use:csp_violations:team{team_id}",{"type":52,"tag":233,"props":320,"children":321},{},[322,324,330],{"type":57,"value":323},"content: brief note (",{"type":52,"tag":66,"props":325,"children":327},{"className":326},[],[328],{"type":57,"value":329},"\"no $csp_violation events in 7d window at {timestamp}\"",{"type":57,"value":331},")",{"type":52,"tag":60,"props":333,"children":334},{},[335],{"type":57,"value":336},"Close out empty in both cases. Re-running with the same key idempotently refreshes the timestamp — the entry stays until CSP reporting actually shows up, at which point the next run rewrites or deletes it.",{"type":52,"tag":178,"props":338,"children":340},{"id":339},"how-a-run-works",[341],{"type":57,"value":342},"How a run works",{"type":52,"tag":60,"props":344,"children":345},{},[346],{"type":57,"value":347},"Cycle between these moves; skip what's not useful.",{"type":52,"tag":349,"props":350,"children":352},"h3",{"id":351},"get-oriented",[353],{"type":57,"value":354},"Get oriented",{"type":52,"tag":60,"props":356,"children":357},{},[358],{"type":57,"value":359},"Four cheap reads cold-start a run:",{"type":52,"tag":229,"props":361,"children":362},{},[363,442,453,513],{"type":52,"tag":233,"props":364,"children":365},{},[366,372,374,380,382,388,390,396,398,404,405,411,412,418,419,425,426,432,434,440],{"type":52,"tag":66,"props":367,"children":369},{"className":368},[],[370],{"type":57,"value":371},"scout-scratchpad-search",{"type":57,"value":373}," (",{"type":52,"tag":66,"props":375,"children":377},{"className":376},[],[378],{"type":57,"value":379},"text=csp",{"type":57,"value":381}," or ",{"type":52,"tag":66,"props":383,"children":385},{"className":384},[],[386],{"type":57,"value":387},"text=blocked",{"type":57,"value":389},") — durable team steering from past CSP runs. Entries with ",{"type":52,"tag":66,"props":391,"children":393},{"className":392},[],[394],{"type":57,"value":395},"pattern:",{"type":57,"value":397},", ",{"type":52,"tag":66,"props":399,"children":401},{"className":400},[],[402],{"type":57,"value":403},"noise:",{"type":57,"value":397},{"type":52,"tag":66,"props":406,"children":408},{"className":407},[],[409],{"type":57,"value":410},"addressed:",{"type":57,"value":397},{"type":52,"tag":66,"props":413,"children":415},{"className":414},[],[416],{"type":57,"value":417},"dedupe:",{"type":57,"value":397},{"type":52,"tag":66,"props":420,"children":422},{"className":421},[],[423],{"type":57,"value":424},"allowlist:",{"type":57,"value":397},{"type":52,"tag":66,"props":427,"children":429},{"className":428},[],[430],{"type":57,"value":431},"report:",{"type":57,"value":433},", or ",{"type":52,"tag":66,"props":435,"children":437},{"className":436},[],[438],{"type":57,"value":439},"reviewer:",{"type":57,"value":441}," key prefixes tell you the team's healthy domains, recurring browser-extension noise, clusters already surfaced, which report covers a cluster, who owns a surface, and what to skip.",{"type":52,"tag":233,"props":443,"children":444},{},[445,451],{"type":52,"tag":66,"props":446,"children":448},{"className":447},[],[449],{"type":57,"value":450},"scout-runs-list",{"type":57,"value":452}," (last 7d) — what prior CSP scouts found and ruled out.",{"type":52,"tag":233,"props":454,"children":455},{},[456,462,464,469,471,476,478,483,484,490,491,496,497,503,505,511],{"type":52,"tag":66,"props":457,"children":459},{"className":458},[],[460],{"type":57,"value":461},"scout-project-profile-get",{"type":57,"value":463}," — the ",{"type":52,"tag":66,"props":465,"children":467},{"className":466},[],[468],{"type":57,"value":71},{"type":57,"value":470}," row in ",{"type":52,"tag":66,"props":472,"children":474},{"className":473},[],[475],{"type":57,"value":201},{"type":57,"value":477}," carries ",{"type":52,"tag":66,"props":479,"children":481},{"className":480},[],[482],{"type":57,"value":209},{"type":57,"value":397},{"type":52,"tag":66,"props":485,"children":487},{"className":486},[],[488],{"type":57,"value":489},"distinct_users",{"type":57,"value":397},{"type":52,"tag":66,"props":492,"children":494},{"className":493},[],[495],{"type":57,"value":217},{"type":57,"value":397},{"type":52,"tag":66,"props":498,"children":500},{"className":499},[],[501],{"type":57,"value":502},"recent_24h_users",{"type":57,"value":504},", plus ",{"type":52,"tag":66,"props":506,"children":508},{"className":507},[],[509],{"type":57,"value":510},"existing_inbox_reports",{"type":57,"value":512},". Pattern the count\u002Fusers ratio against the table below.",{"type":52,"tag":233,"props":514,"children":515},{},[516,522,523,529,530,536,538,543,545,550,552,557,559,563,565,571],{"type":52,"tag":66,"props":517,"children":519},{"className":518},[],[520],{"type":57,"value":521},"inbox-reports-list",{"type":57,"value":373},{"type":52,"tag":66,"props":524,"children":526},{"className":525},[],[527],{"type":57,"value":528},"ordering=-updated_at",{"type":57,"value":397},{"type":52,"tag":66,"props":531,"children":533},{"className":532},[],[534],{"type":57,"value":535},"search",{"type":57,"value":537},"=the blocked domain \u002F directive) — the reports already in the inbox. ",{"type":52,"tag":80,"props":539,"children":540},{},[541],{"type":57,"value":542},"Two source_products matter here:",{"type":57,"value":544}," your own report-channel reports persist under ",{"type":52,"tag":66,"props":546,"children":548},{"className":547},[],[549],{"type":57,"value":174},{"type":57,"value":551}," (search these for edit-vs-author — don't filter them out), while the companion push path files raw per-fingerprint signals under ",{"type":52,"tag":66,"props":553,"children":555},{"className":554},[],[556],{"type":57,"value":166},{"type":57,"value":558}," (check these to stay quiet when the push path already covers a cluster — see Decide). A cluster you've reported before is an ",{"type":52,"tag":80,"props":560,"children":561},{},[562],{"type":57,"value":112},{"type":57,"value":564},", not a fresh report; pull the closest matches with ",{"type":52,"tag":66,"props":566,"children":568},{"className":567},[],[569],{"type":57,"value":570},"inbox-reports-retrieve",{"type":57,"value":572}," before authoring.",{"type":52,"tag":349,"props":574,"children":576},{"id":575},"profile-shape-count-vs-distinct_users",[577],{"type":57,"value":578},"Profile shape — count vs distinct_users",{"type":52,"tag":580,"props":581,"children":582},"table",{},[583,602],{"type":52,"tag":584,"props":585,"children":586},"thead",{},[587],{"type":52,"tag":588,"props":589,"children":590},"tr",{},[591,597],{"type":52,"tag":592,"props":593,"children":594},"th",{},[595],{"type":57,"value":596},"Pattern",{"type":52,"tag":592,"props":598,"children":599},{},[600],{"type":57,"value":601},"What it usually means",{"type":52,"tag":603,"props":604,"children":605},"tbody",{},[606,634,661,686,711,739,765],{"type":52,"tag":588,"props":607,"children":608},{},[609,629],{"type":52,"tag":610,"props":611,"children":612},"td",{},[613,615,620,622,627],{"type":57,"value":614},"Both ",{"type":52,"tag":66,"props":616,"children":618},{"className":617},[],[619],{"type":57,"value":209},{"type":57,"value":621}," and ",{"type":52,"tag":66,"props":623,"children":625},{"className":624},[],[626],{"type":57,"value":489},{"type":57,"value":628}," spike in 24h",{"type":52,"tag":610,"props":630,"children":631},{},[632],{"type":57,"value":633},"Fresh broad-impact CSP regression — deploy missed an allowlist",{"type":52,"tag":588,"props":635,"children":636},{},[637,656],{"type":52,"tag":610,"props":638,"children":639},{},[640,646,648,654],{"type":52,"tag":66,"props":641,"children":643},{"className":642},[],[644],{"type":57,"value":645},"recent_24h_count \u002F count",{"type":57,"value":647}," ≫ ",{"type":52,"tag":66,"props":649,"children":651},{"className":650},[],[652],{"type":57,"value":653},"1\u002F7",{"type":57,"value":655},", users also spike",{"type":52,"tag":610,"props":657,"children":658},{},[659],{"type":57,"value":660},"Today's burst is unusually broad — investigate first",{"type":52,"tag":588,"props":662,"children":663},{},[664,681],{"type":52,"tag":610,"props":665,"children":666},{},[667,672,674,679],{"type":52,"tag":66,"props":668,"children":670},{"className":669},[],[671],{"type":57,"value":209},{"type":57,"value":673}," very high, ",{"type":52,"tag":66,"props":675,"children":677},{"className":676},[],[678],{"type":57,"value":489},{"type":57,"value":680}," very low (≤ 5)",{"type":52,"tag":610,"props":682,"children":683},{},[684],{"type":57,"value":685},"Single user \u002F bot \u002F browser extension — usually skip",{"type":52,"tag":588,"props":687,"children":688},{},[689,706],{"type":52,"tag":610,"props":690,"children":691},{},[692,697,699,704],{"type":52,"tag":66,"props":693,"children":695},{"className":694},[],[696],{"type":57,"value":209},{"type":57,"value":698}," ~ ",{"type":52,"tag":66,"props":700,"children":702},{"className":701},[],[703],{"type":57,"value":489},{"type":57,"value":705}," for one blocked URL",{"type":52,"tag":610,"props":707,"children":708},{},[709],{"type":57,"value":710},"Per-pageload violation hitting every visitor — broken policy",{"type":52,"tag":588,"props":712,"children":713},{},[714,726],{"type":52,"tag":610,"props":715,"children":716},{},[717,719,724],{"type":57,"value":718},"Steady high ",{"type":52,"tag":66,"props":720,"children":722},{"className":721},[],[723],{"type":57,"value":209},{"type":57,"value":725}," across many users + many directives",{"type":52,"tag":610,"props":727,"children":728},{},[729,731,737],{"type":57,"value":730},"Mature CSP policy in ",{"type":52,"tag":66,"props":732,"children":734},{"className":733},[],[735],{"type":57,"value":736},"report-only",{"type":57,"value":738}," mode — high baseline expected",{"type":52,"tag":588,"props":740,"children":741},{},[742,755],{"type":52,"tag":610,"props":743,"children":744},{},[745,747,753],{"type":57,"value":746},"Steady high reach on one ",{"type":52,"tag":66,"props":748,"children":750},{"className":749},[],[751],{"type":57,"value":752},"enforce",{"type":57,"value":754}," \u002F first-party domain",{"type":52,"tag":610,"props":756,"children":757},{},[758,763],{"type":52,"tag":80,"props":759,"children":760},{},[761],{"type":57,"value":762},"Standing block",{"type":57,"value":764}," — live breakage; report even with no fresh burst",{"type":52,"tag":588,"props":766,"children":767},{},[768,784],{"type":52,"tag":610,"props":769,"children":770},{},[771,776,777,782],{"type":52,"tag":66,"props":772,"children":774},{"className":773},[],[775],{"type":57,"value":209},{"type":57,"value":621},{"type":52,"tag":66,"props":778,"children":780},{"className":779},[],[781],{"type":57,"value":489},{"type":57,"value":783}," both quiet",{"type":52,"tag":610,"props":785,"children":786},{},[787],{"type":57,"value":788},"Nothing fresh today — close out",{"type":52,"tag":349,"props":790,"children":792},{"id":791},"explore",[793],{"type":57,"value":794},"Explore",{"type":52,"tag":60,"props":796,"children":797},{},[798,800,806,808,814,816,821],{"type":57,"value":799},"Patterns to watch — starting points, not a checklist. Group violations along four dimensions and look for clusters worth a finding. PostHog's push-based CSP emission already deduplicates ",{"type":52,"tag":801,"props":802,"children":803},"em",{},[804],{"type":57,"value":805},"individual",{"type":57,"value":807}," violations at ",{"type":52,"tag":66,"props":809,"children":811},{"className":810},[],[812],{"type":57,"value":813},"sha1(violated_directive | blocked_url | document_url | source_file)",{"type":57,"value":815}," granularity with a 24h Redis TTL; your job is to ",{"type":52,"tag":801,"props":817,"children":818},{},[819],{"type":57,"value":820},"aggregate",{"type":57,"value":822}," across that grain into higher-confidence findings the inbox wouldn't surface on its own.",{"type":52,"tag":824,"props":825,"children":827},"h4",{"id":826},"fresh-blocked-url-domain",[828],{"type":57,"value":829},"Fresh blocked-URL domain",{"type":52,"tag":60,"props":831,"children":832},{},[833,835,841,843,849,851,857],{"type":57,"value":834},"The single highest-value CSP pattern. Group by ",{"type":52,"tag":66,"props":836,"children":838},{"className":837},[],[839],{"type":57,"value":840},"domain(properties.$csp_blocked_url)",{"type":57,"value":842}," over the last 24–48h. A domain with ",{"type":52,"tag":66,"props":844,"children":846},{"className":845},[],[847],{"type":57,"value":848},"first_seen",{"type":57,"value":850}," inside the window, ≥ 10 distinct pageviews, and not in the team's ",{"type":52,"tag":66,"props":852,"children":854},{"className":853},[],[855],{"type":57,"value":856},"allowlist",{"type":57,"value":858},"-tagged memory is the strongest scout signal.",{"type":52,"tag":860,"props":861,"children":866},"pre",{"className":862,"code":863,"language":864,"meta":865,"style":865},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","SELECT\n    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,\n    count() AS occurrences,\n    uniq(person_id) AS distinct_users,\n    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents,\n    min(timestamp) AS first_seen,\n    max(timestamp) AS last_seen,\n    groupArray(DISTINCT JSONExtractString(properties, '$csp_effective_directive'))[1:5] AS directives\nFROM events\nWHERE event = '$csp_violation'\n  AND timestamp > now() - INTERVAL 48 HOUR\n  AND JSONExtractString(properties, '$csp_blocked_url') != ''\nGROUP BY blocked_domain\nHAVING first_seen > now() - INTERVAL 24 HOUR\n   AND distinct_users >= 10\n   AND blocked_domain != ''   -- drop inline\u002Feval\u002Fextension reports ('unsafe-inline' etc.): non-empty URL but no domain\nORDER BY occurrences DESC\nLIMIT 20\n","sql","",[867],{"type":52,"tag":66,"props":868,"children":869},{"__ignoreMap":865},[870,881,890,899,908,917,926,935,944,953,962,970,979,988,997,1006,1015,1024],{"type":52,"tag":871,"props":872,"children":875},"span",{"class":873,"line":874},"line",1,[876],{"type":52,"tag":871,"props":877,"children":878},{},[879],{"type":57,"value":880},"SELECT\n",{"type":52,"tag":871,"props":882,"children":884},{"class":873,"line":883},2,[885],{"type":52,"tag":871,"props":886,"children":887},{},[888],{"type":57,"value":889},"    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,\n",{"type":52,"tag":871,"props":891,"children":893},{"class":873,"line":892},3,[894],{"type":52,"tag":871,"props":895,"children":896},{},[897],{"type":57,"value":898},"    count() AS occurrences,\n",{"type":52,"tag":871,"props":900,"children":902},{"class":873,"line":901},4,[903],{"type":52,"tag":871,"props":904,"children":905},{},[906],{"type":57,"value":907},"    uniq(person_id) AS distinct_users,\n",{"type":52,"tag":871,"props":909,"children":911},{"class":873,"line":910},5,[912],{"type":52,"tag":871,"props":913,"children":914},{},[915],{"type":57,"value":916},"    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents,\n",{"type":52,"tag":871,"props":918,"children":920},{"class":873,"line":919},6,[921],{"type":52,"tag":871,"props":922,"children":923},{},[924],{"type":57,"value":925},"    min(timestamp) AS first_seen,\n",{"type":52,"tag":871,"props":927,"children":929},{"class":873,"line":928},7,[930],{"type":52,"tag":871,"props":931,"children":932},{},[933],{"type":57,"value":934},"    max(timestamp) AS last_seen,\n",{"type":52,"tag":871,"props":936,"children":938},{"class":873,"line":937},8,[939],{"type":52,"tag":871,"props":940,"children":941},{},[942],{"type":57,"value":943},"    groupArray(DISTINCT JSONExtractString(properties, '$csp_effective_directive'))[1:5] AS directives\n",{"type":52,"tag":871,"props":945,"children":947},{"class":873,"line":946},9,[948],{"type":52,"tag":871,"props":949,"children":950},{},[951],{"type":57,"value":952},"FROM events\n",{"type":52,"tag":871,"props":954,"children":956},{"class":873,"line":955},10,[957],{"type":52,"tag":871,"props":958,"children":959},{},[960],{"type":57,"value":961},"WHERE event = '$csp_violation'\n",{"type":52,"tag":871,"props":963,"children":964},{"class":873,"line":27},[965],{"type":52,"tag":871,"props":966,"children":967},{},[968],{"type":57,"value":969},"  AND timestamp > now() - INTERVAL 48 HOUR\n",{"type":52,"tag":871,"props":971,"children":973},{"class":873,"line":972},12,[974],{"type":52,"tag":871,"props":975,"children":976},{},[977],{"type":57,"value":978},"  AND JSONExtractString(properties, '$csp_blocked_url') != ''\n",{"type":52,"tag":871,"props":980,"children":982},{"class":873,"line":981},13,[983],{"type":52,"tag":871,"props":984,"children":985},{},[986],{"type":57,"value":987},"GROUP BY blocked_domain\n",{"type":52,"tag":871,"props":989,"children":991},{"class":873,"line":990},14,[992],{"type":52,"tag":871,"props":993,"children":994},{},[995],{"type":57,"value":996},"HAVING first_seen > now() - INTERVAL 24 HOUR\n",{"type":52,"tag":871,"props":998,"children":1000},{"class":873,"line":999},15,[1001],{"type":52,"tag":871,"props":1002,"children":1003},{},[1004],{"type":57,"value":1005},"   AND distinct_users >= 10\n",{"type":52,"tag":871,"props":1007,"children":1009},{"class":873,"line":1008},16,[1010],{"type":52,"tag":871,"props":1011,"children":1012},{},[1013],{"type":57,"value":1014},"   AND blocked_domain != ''   -- drop inline\u002Feval\u002Fextension reports ('unsafe-inline' etc.): non-empty URL but no domain\n",{"type":52,"tag":871,"props":1016,"children":1018},{"class":873,"line":1017},17,[1019],{"type":52,"tag":871,"props":1020,"children":1021},{},[1022],{"type":57,"value":1023},"ORDER BY occurrences DESC\n",{"type":52,"tag":871,"props":1025,"children":1027},{"class":873,"line":1026},18,[1028],{"type":52,"tag":871,"props":1029,"children":1030},{},[1031],{"type":57,"value":1032},"LIMIT 20\n",{"type":52,"tag":60,"props":1034,"children":1035},{},[1036],{"type":57,"value":1037},"Three lenses for triage — every blocked-URL finding should name which one fits:",{"type":52,"tag":1039,"props":1040,"children":1041},"ol",{},[1042,1052,1077],{"type":52,"tag":233,"props":1043,"children":1044},{},[1045,1050],{"type":52,"tag":80,"props":1046,"children":1047},{},[1048],{"type":57,"value":1049},"Legitimate — CSP policy needs widening.",{"type":57,"value":1051}," New CDN, new analytics provider, new marketing tag the team rolled out and forgot to add to the allowlist.",{"type":52,"tag":233,"props":1053,"children":1054},{},[1055,1060,1062,1067,1069,1075],{"type":52,"tag":80,"props":1056,"children":1057},{},[1058],{"type":57,"value":1059},"Compromised — injected or third-party script indicating a security incident.",{"type":57,"value":1061}," Fresh domain nobody recognizes, especially script-src violations on a small number of high-traffic pages, especially with ",{"type":52,"tag":66,"props":1063,"children":1065},{"className":1064},[],[1066],{"type":57,"value":286},{"type":57,"value":1068}," and a ",{"type":52,"tag":66,"props":1070,"children":1072},{"className":1071},[],[1073],{"type":57,"value":1074},"source_file",{"type":57,"value":1076}," that points at the team's own JS bundle.",{"type":52,"tag":233,"props":1078,"children":1079},{},[1080,1085],{"type":52,"tag":80,"props":1081,"children":1082},{},[1083],{"type":57,"value":1084},"Third-party drift — vendor script the team should remove.",{"type":57,"value":1086}," Old analytics SDK still loaded from a deprecated bundle, ad pixel from a churned vendor, etc.",{"type":52,"tag":60,"props":1088,"children":1089},{},[1090,1092,1098],{"type":57,"value":1091},"File a report only when one of these lenses fits with high confidence. If you're genuinely unsure which of the three it is, write a ",{"type":52,"tag":66,"props":1093,"children":1095},{"className":1094},[],[1096],{"type":57,"value":1097},"pattern:csp_violations:\u003Centity>",{"type":57,"value":1099}," scratchpad entry for the next run and close out.",{"type":52,"tag":824,"props":1101,"children":1103},{"id":1102},"standing-enforced-first-party-block-no-freshness-required",[1104],{"type":57,"value":1105},"Standing enforced \u002F first-party block (no freshness required)",{"type":52,"tag":60,"props":1107,"children":1108},{},[1109,1111,1116,1117,1123,1125,1130,1132,1137],{"type":57,"value":1110},"The fresh-domain query above only fires for domains that ",{"type":52,"tag":80,"props":1112,"children":1113},{},[1114],{"type":57,"value":1115},"first appeared in the last 24h",{"type":57,"value":373},{"type":52,"tag":66,"props":1118,"children":1120},{"className":1119},[],[1121],{"type":57,"value":1122},"first_seen > now() - INTERVAL 24 HOUR",{"type":57,"value":1124},"). A policy that has been enforce-blocking a real endpoint for weeks never trips it, and its steady volume reads as \"baseline\" and closes out — so a high-reach, actively-enforced block can sit invisible indefinitely. This is the scout's biggest blind spot. Two ",{"type":52,"tag":80,"props":1126,"children":1127},{},[1128],{"type":57,"value":1129},"standing",{"type":57,"value":1131}," patterns deserve a finding even with zero freshness, because they are breaking functionality for real users ",{"type":52,"tag":801,"props":1133,"children":1134},{},[1135],{"type":57,"value":1136},"right now",{"type":57,"value":1138},":",{"type":52,"tag":1039,"props":1140,"children":1141},{},[1142,1166],{"type":52,"tag":233,"props":1143,"children":1144},{},[1145,1150,1152,1157,1159,1164],{"type":52,"tag":80,"props":1146,"children":1147},{},[1148],{"type":57,"value":1149},"High-reach enforced block.",{"type":57,"value":1151}," A ",{"type":52,"tag":66,"props":1153,"children":1155},{"className":1154},[],[1156],{"type":57,"value":286},{"type":57,"value":1158}," blocked domain with broad reach (many distinct users ",{"type":52,"tag":801,"props":1160,"children":1161},{},[1162],{"type":57,"value":1163},"and",{"type":57,"value":1165}," documents) is not baseline noise — it is a live, enforced block degrading those users. Surface it regardless of when it first appeared.",{"type":52,"tag":233,"props":1167,"children":1168},{},[1169,1174,1176,1182,1184,1189],{"type":52,"tag":80,"props":1170,"children":1171},{},[1172],{"type":57,"value":1173},"First-party \u002F own-infra block.",{"type":57,"value":1175}," A blocked domain that is the team's own surface (the blocked host equals or is a subdomain of a ",{"type":52,"tag":66,"props":1177,"children":1179},{"className":1178},[],[1180],{"type":57,"value":1181},"$csp_document_url",{"type":57,"value":1183}," host, or a known first-party domain) with high reach is an allowlist gap in the team's ",{"type":52,"tag":801,"props":1185,"children":1186},{},[1187],{"type":57,"value":1188},"own",{"type":57,"value":1190}," policy — a near-certain \"widen the policy\" fix.",{"type":52,"tag":860,"props":1192,"children":1194},{"className":862,"code":1193,"language":864,"meta":865,"style":865},"SELECT\n    JSONExtractString(properties, '$csp_disposition') AS disposition,\n    JSONExtractString(properties, '$csp_effective_directive') AS directive,\n    domain(JSONExtractString(properties, '$csp_blocked_url')) AS blocked_domain,\n    count() AS occurrences_7d,\n    uniq(person_id) AS distinct_users,\n    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents\nFROM events\nWHERE event = '$csp_violation'\n  AND timestamp > now() - INTERVAL 7 DAY\n  AND JSONExtractString(properties, '$csp_blocked_url') != ''\nGROUP BY disposition, directive, blocked_domain\nHAVING distinct_users >= 100          -- broad reach, not a single user \u002F extension\n   AND blocked_domain != ''           -- exclude inline\u002Feval\u002Fextension noise so named domains fill the limit\nORDER BY (disposition = 'enforce') DESC, distinct_users DESC\nLIMIT 30\n",[1195],{"type":52,"tag":66,"props":1196,"children":1197},{"__ignoreMap":865},[1198,1205,1213,1221,1228,1236,1243,1251,1258,1265,1273,1280,1288,1296,1304,1312],{"type":52,"tag":871,"props":1199,"children":1200},{"class":873,"line":874},[1201],{"type":52,"tag":871,"props":1202,"children":1203},{},[1204],{"type":57,"value":880},{"type":52,"tag":871,"props":1206,"children":1207},{"class":873,"line":883},[1208],{"type":52,"tag":871,"props":1209,"children":1210},{},[1211],{"type":57,"value":1212},"    JSONExtractString(properties, '$csp_disposition') AS disposition,\n",{"type":52,"tag":871,"props":1214,"children":1215},{"class":873,"line":892},[1216],{"type":52,"tag":871,"props":1217,"children":1218},{},[1219],{"type":57,"value":1220},"    JSONExtractString(properties, '$csp_effective_directive') AS directive,\n",{"type":52,"tag":871,"props":1222,"children":1223},{"class":873,"line":901},[1224],{"type":52,"tag":871,"props":1225,"children":1226},{},[1227],{"type":57,"value":889},{"type":52,"tag":871,"props":1229,"children":1230},{"class":873,"line":910},[1231],{"type":52,"tag":871,"props":1232,"children":1233},{},[1234],{"type":57,"value":1235},"    count() AS occurrences_7d,\n",{"type":52,"tag":871,"props":1237,"children":1238},{"class":873,"line":919},[1239],{"type":52,"tag":871,"props":1240,"children":1241},{},[1242],{"type":57,"value":907},{"type":52,"tag":871,"props":1244,"children":1245},{"class":873,"line":928},[1246],{"type":52,"tag":871,"props":1247,"children":1248},{},[1249],{"type":57,"value":1250},"    uniq(JSONExtractString(properties, '$csp_document_url')) AS distinct_documents\n",{"type":52,"tag":871,"props":1252,"children":1253},{"class":873,"line":937},[1254],{"type":52,"tag":871,"props":1255,"children":1256},{},[1257],{"type":57,"value":952},{"type":52,"tag":871,"props":1259,"children":1260},{"class":873,"line":946},[1261],{"type":52,"tag":871,"props":1262,"children":1263},{},[1264],{"type":57,"value":961},{"type":52,"tag":871,"props":1266,"children":1267},{"class":873,"line":955},[1268],{"type":52,"tag":871,"props":1269,"children":1270},{},[1271],{"type":57,"value":1272},"  AND timestamp > now() - INTERVAL 7 DAY\n",{"type":52,"tag":871,"props":1274,"children":1275},{"class":873,"line":27},[1276],{"type":52,"tag":871,"props":1277,"children":1278},{},[1279],{"type":57,"value":978},{"type":52,"tag":871,"props":1281,"children":1282},{"class":873,"line":972},[1283],{"type":52,"tag":871,"props":1284,"children":1285},{},[1286],{"type":57,"value":1287},"GROUP BY disposition, directive, blocked_domain\n",{"type":52,"tag":871,"props":1289,"children":1290},{"class":873,"line":981},[1291],{"type":52,"tag":871,"props":1292,"children":1293},{},[1294],{"type":57,"value":1295},"HAVING distinct_users >= 100          -- broad reach, not a single user \u002F extension\n",{"type":52,"tag":871,"props":1297,"children":1298},{"class":873,"line":990},[1299],{"type":52,"tag":871,"props":1300,"children":1301},{},[1302],{"type":57,"value":1303},"   AND blocked_domain != ''           -- exclude inline\u002Feval\u002Fextension noise so named domains fill the limit\n",{"type":52,"tag":871,"props":1305,"children":1306},{"class":873,"line":999},[1307],{"type":52,"tag":871,"props":1308,"children":1309},{},[1310],{"type":57,"value":1311},"ORDER BY (disposition = 'enforce') DESC, distinct_users DESC\n",{"type":52,"tag":871,"props":1313,"children":1314},{"class":873,"line":1008},[1315],{"type":52,"tag":871,"props":1316,"children":1317},{},[1318],{"type":57,"value":1319},"LIMIT 30\n",{"type":52,"tag":60,"props":1321,"children":1322},{},[1323],{"type":57,"value":1324},"Triage:",{"type":52,"tag":229,"props":1326,"children":1327},{},[1328,1353,1379],{"type":52,"tag":233,"props":1329,"children":1330},{},[1331,1336,1338,1344,1345,1351],{"type":52,"tag":80,"props":1332,"children":1333},{},[1334],{"type":57,"value":1335},"Enforce + high reach",{"type":57,"value":1337}," → report; these users are actively blocked. Highest priority when the directive is ",{"type":52,"tag":66,"props":1339,"children":1341},{"className":1340},[],[1342],{"type":57,"value":1343},"script-src",{"type":57,"value":99},{"type":52,"tag":66,"props":1346,"children":1348},{"className":1347},[],[1349],{"type":57,"value":1350},"connect-src",{"type":57,"value":1352}," (breaks behaviour, not just styling).",{"type":52,"tag":233,"props":1354,"children":1355},{},[1356,1361,1363,1369,1371,1377],{"type":52,"tag":80,"props":1357,"children":1358},{},[1359],{"type":57,"value":1360},"First-party blocked domain",{"type":57,"value":1362}," (own CDN, status page, replay proxy, internal endpoint) → file a report as \"policy allowlist gap — add ",{"type":52,"tag":66,"props":1364,"children":1366},{"className":1365},[],[1367],{"type":57,"value":1368},"{domain}",{"type":57,"value":1370}," to ",{"type":52,"tag":66,"props":1372,"children":1374},{"className":1373},[],[1375],{"type":57,"value":1376},"{directive}",{"type":57,"value":1378},"\". One report per domain.",{"type":52,"tag":233,"props":1380,"children":1381},{},[1382,1387,1389,1394,1396,1401],{"type":52,"tag":80,"props":1383,"children":1384},{},[1385],{"type":57,"value":1386},"Third-party, report-only, high reach but stable",{"type":57,"value":1388}," → report-only refinement case; remember (",{"type":52,"tag":66,"props":1390,"children":1392},{"className":1391},[],[1393],{"type":57,"value":395},{"type":57,"value":1395},"\u002F",{"type":52,"tag":66,"props":1397,"children":1399},{"className":1398},[],[1400],{"type":57,"value":424},{"type":57,"value":1402},") rather than report, unless it's a fresh domain (that's the fresh-domain path above).",{"type":52,"tag":60,"props":1404,"children":1405},{},[1406,1408,1414,1416,1422,1423,1429,1431,1437,1439,1445,1447,1452,1454,1460],{"type":57,"value":1407},"The ",{"type":52,"tag":66,"props":1409,"children":1411},{"className":1410},[],[1412],{"type":57,"value":1413},"blocked_domain != ''",{"type":57,"value":1415}," filter already drops the giant inline \u002F ",{"type":52,"tag":66,"props":1417,"children":1419},{"className":1418},[],[1420],{"type":57,"value":1421},"eval",{"type":57,"value":99},{"type":52,"tag":66,"props":1424,"children":1426},{"className":1425},[],[1427],{"type":57,"value":1428},"unsafe-inline",{"type":57,"value":1430}," and browser-extension clusters (non-empty ",{"type":52,"tag":66,"props":1432,"children":1434},{"className":1433},[],[1435],{"type":57,"value":1436},"$csp_blocked_url",{"type":57,"value":1438},", empty ",{"type":52,"tag":66,"props":1440,"children":1442},{"className":1441},[],[1443],{"type":57,"value":1444},"domain()",{"type":57,"value":1446},") — the baseline noise this surface always carries — so the limit is spent on the reach that matters: ",{"type":52,"tag":80,"props":1448,"children":1449},{},[1450],{"type":57,"value":1451},"named",{"type":57,"value":1453}," domains. Dedupe standing reports with ",{"type":52,"tag":66,"props":1455,"children":1457},{"className":1456},[],[1458],{"type":57,"value":1459},"addressed:csp_violations:{blocked_domain}-{directive}",{"type":57,"value":1461}," so a confirmed-and-allowlisted (or accepted) block doesn't re-surface every run.",{"type":52,"tag":824,"props":1463,"children":1465},{"id":1464},"per-directive-burst",[1466],{"type":57,"value":1467},"Per-directive burst",{"type":52,"tag":60,"props":1469,"children":1470},{},[1471,1473,1479,1481,1487],{"type":57,"value":1472},"Group by ",{"type":52,"tag":66,"props":1474,"children":1476},{"className":1475},[],[1477],{"type":57,"value":1478},"properties.$csp_effective_directive",{"type":57,"value":1480},". A directive whose recent 24h count is materially above its 7d-prior baseline (≥ 3×) with reach across multiple documents is a strong \"policy regression after deploy\" signal. Pair with ",{"type":52,"tag":66,"props":1482,"children":1484},{"className":1483},[],[1485],{"type":57,"value":1486},"advanced-activity-logs-list",{"type":57,"value":1488}," filtered to the last 24–48h — a deploy or hog-flow change correlating to the burst timestamp is the clean cross-source convergence.",{"type":52,"tag":60,"props":1490,"children":1491},{},[1492,1494,1499,1500,1506,1507,1513,1514,1520,1521,1526,1527,1533,1535,1540,1542,1547,1548,1553],{"type":57,"value":1493},"Top directives to expect (rough share-of-violations on a typical SPA): ",{"type":52,"tag":66,"props":1495,"children":1497},{"className":1496},[],[1498],{"type":57,"value":1343},{"type":57,"value":397},{"type":52,"tag":66,"props":1501,"children":1503},{"className":1502},[],[1504],{"type":57,"value":1505},"script-src-elem",{"type":57,"value":397},{"type":52,"tag":66,"props":1508,"children":1510},{"className":1509},[],[1511],{"type":57,"value":1512},"img-src",{"type":57,"value":397},{"type":52,"tag":66,"props":1515,"children":1517},{"className":1516},[],[1518],{"type":57,"value":1519},"style-src",{"type":57,"value":397},{"type":52,"tag":66,"props":1522,"children":1524},{"className":1523},[],[1525],{"type":57,"value":1350},{"type":57,"value":397},{"type":52,"tag":66,"props":1528,"children":1530},{"className":1529},[],[1531],{"type":57,"value":1532},"frame-src",{"type":57,"value":1534},". ",{"type":52,"tag":66,"props":1536,"children":1538},{"className":1537},[],[1539],{"type":57,"value":1343},{"type":57,"value":1541}," violations are weighted highest for security relevance; ",{"type":52,"tag":66,"props":1543,"children":1545},{"className":1544},[],[1546],{"type":57,"value":1512},{"type":57,"value":621},{"type":52,"tag":66,"props":1549,"children":1551},{"className":1550},[],[1552],{"type":57,"value":1519},{"type":57,"value":1554}," more often indicate vendor \u002F CDN drift.",{"type":52,"tag":824,"props":1556,"children":1558},{"id":1557},"document-scoped-regression",[1559],{"type":57,"value":1560},"Document-scoped regression",{"type":52,"tag":60,"props":1562,"children":1563},{},[1564,1565,1571,1573,1579,1580,1586,1587,1593],{"type":57,"value":1472},{"type":52,"tag":66,"props":1566,"children":1568},{"className":1567},[],[1569],{"type":57,"value":1570},"properties.$csp_document_url",{"type":57,"value":1572},". A document with no violations in the 7d-prior window and a sudden burst in the recent 24h is almost always a deploy regression on that route — a new script tag or inline style that the existing policy doesn't allow. High-value finding when the document is a critical funnel page (",{"type":52,"tag":66,"props":1574,"children":1576},{"className":1575},[],[1577],{"type":57,"value":1578},"\u002Fcheckout",{"type":57,"value":397},{"type":52,"tag":66,"props":1581,"children":1583},{"className":1582},[],[1584],{"type":57,"value":1585},"\u002Fsignup",{"type":57,"value":397},{"type":52,"tag":66,"props":1588,"children":1590},{"className":1589},[],[1591],{"type":57,"value":1592},"\u002Flogin",{"type":57,"value":1594},").",{"type":52,"tag":824,"props":1596,"children":1598},{"id":1597},"stuck-loop-single-user-noise",[1599],{"type":57,"value":1600},"Stuck loop \u002F single-user noise",{"type":52,"tag":60,"props":1602,"children":1603},{},[1604,1609,1611,1616,1618,1624],{"type":52,"tag":66,"props":1605,"children":1607},{"className":1606},[],[1608],{"type":57,"value":209},{"type":57,"value":1610}," very high but ",{"type":52,"tag":66,"props":1612,"children":1614},{"className":1613},[],[1615],{"type":57,"value":489},{"type":57,"value":1617}," ≤ 5 over the recent window. Almost always a single user with a misbehaving browser extension, or a bot probing the page. Skip — write a ",{"type":52,"tag":66,"props":1619,"children":1621},{"className":1620},[],[1622],{"type":57,"value":1623},"noise:csp_violations:\u003Cblocked_domain>",{"type":57,"value":1625}," scratchpad entry so future runs short-circuit.",{"type":52,"tag":60,"props":1627,"children":1628},{},[1629],{"type":57,"value":1630},"Common skippable patterns:",{"type":52,"tag":229,"props":1632,"children":1633},{},[1634,1659,1664],{"type":52,"tag":233,"props":1635,"children":1636},{},[1637,1643,1644,1650,1651,1657],{"type":52,"tag":66,"props":1638,"children":1640},{"className":1639},[],[1641],{"type":57,"value":1642},"chrome-extension:\u002F\u002F",{"type":57,"value":99},{"type":52,"tag":66,"props":1645,"children":1647},{"className":1646},[],[1648],{"type":57,"value":1649},"moz-extension:\u002F\u002F",{"type":57,"value":99},{"type":52,"tag":66,"props":1652,"children":1654},{"className":1653},[],[1655],{"type":57,"value":1656},"safari-extension:\u002F\u002F",{"type":57,"value":1658}," blocked URLs",{"type":52,"tag":233,"props":1660,"children":1661},{},[1662],{"type":57,"value":1663},"Brave \u002F DuckDuckGo \u002F privacy-browser injected scripts",{"type":52,"tag":233,"props":1665,"children":1666},{},[1667,1673,1674,1680],{"type":52,"tag":66,"props":1668,"children":1670},{"className":1669},[],[1671],{"type":57,"value":1672},"about:blank",{"type":57,"value":397},{"type":52,"tag":66,"props":1675,"children":1677},{"className":1676},[],[1678],{"type":57,"value":1679},"data:",{"type":57,"value":1681}," URIs from translation tooling or password managers",{"type":52,"tag":824,"props":1683,"children":1685},{"id":1684},"disposition-shift",[1686],{"type":57,"value":1687},"Disposition shift",{"type":52,"tag":60,"props":1689,"children":1690},{},[1691,1692,1698,1700,1705,1707,1712,1714,1719,1721,1727,1729,1734,1736,1741,1743,1749],{"type":57,"value":1472},{"type":52,"tag":66,"props":1693,"children":1695},{"className":1694},[],[1696],{"type":57,"value":1697},"properties.$csp_disposition",{"type":57,"value":1699},". A team running ",{"type":52,"tag":66,"props":1701,"children":1703},{"className":1702},[],[1704],{"type":57,"value":736},{"type":57,"value":1706}," for a long time and then flipping to ",{"type":52,"tag":66,"props":1708,"children":1710},{"className":1709},[],[1711],{"type":57,"value":752},{"type":57,"value":1713}," will see violations turn into actual blocks. If the project profile shows ",{"type":52,"tag":66,"props":1715,"children":1717},{"className":1716},[],[1718],{"type":57,"value":209},{"type":57,"value":1720}," for ",{"type":52,"tag":66,"props":1722,"children":1724},{"className":1723},[],[1725],{"type":57,"value":1726},"disposition='enforce'",{"type":57,"value":1728}," rising sharply (",{"type":52,"tag":66,"props":1730,"children":1732},{"className":1731},[],[1733],{"type":57,"value":217},{"type":57,"value":1735}," materially above baseline) while ",{"type":52,"tag":66,"props":1737,"children":1739},{"className":1738},[],[1740],{"type":57,"value":736},{"type":57,"value":1742}," shows a corresponding fall, the team has flipped enforcement — write a ",{"type":52,"tag":66,"props":1744,"children":1746},{"className":1745},[],[1747],{"type":57,"value":1748},"pattern:csp_violations:disposition-flip",{"type":57,"value":1750}," scratchpad entry and file a report only if a critical page is suddenly seeing enforced blocks.",{"type":52,"tag":349,"props":1752,"children":1754},{"id":1753},"save-memory-as-you-go",[1755],{"type":57,"value":1756},"Save memory as you go",{"type":52,"tag":60,"props":1758,"children":1759},{},[1760,1762,1767,1768,1773,1774,1779,1780,1785,1786,1791,1793,1799],{"type":57,"value":1761},"Memory is a continuous activity. Write a scratchpad entry whenever you observe something a future CSP run should know. Encode the \"category\" in the key prefix — ",{"type":52,"tag":66,"props":1763,"children":1765},{"className":1764},[],[1766],{"type":57,"value":395},{"type":57,"value":397},{"type":52,"tag":66,"props":1769,"children":1771},{"className":1770},[],[1772],{"type":57,"value":403},{"type":57,"value":397},{"type":52,"tag":66,"props":1775,"children":1777},{"className":1776},[],[1778],{"type":57,"value":410},{"type":57,"value":397},{"type":52,"tag":66,"props":1781,"children":1783},{"className":1782},[],[1784],{"type":57,"value":417},{"type":57,"value":397},{"type":52,"tag":66,"props":1787,"children":1789},{"className":1788},[],[1790],{"type":57,"value":424},{"type":57,"value":1792}," — so future runs find it with a single ",{"type":52,"tag":66,"props":1794,"children":1796},{"className":1795},[],[1797],{"type":57,"value":1798},"text=",{"type":57,"value":1800}," search:",{"type":52,"tag":229,"props":1802,"children":1803},{},[1804,1851,1887,1925,1956,2007,2031],{"type":52,"tag":233,"props":1805,"children":1806},{},[1807,1809,1815,1817],{"type":57,"value":1808},"key ",{"type":52,"tag":66,"props":1810,"children":1812},{"className":1811},[],[1813],{"type":57,"value":1814},"pattern:csp_violations:baseline",{"type":57,"value":1816}," — ",{"type":52,"tag":801,"props":1818,"children":1819},{},[1820,1822,1827,1829,1834,1836,1842,1843,1849],{"type":57,"value":1821},"\"Project's healthy ",{"type":52,"tag":66,"props":1823,"children":1825},{"className":1824},[],[1826],{"type":57,"value":71},{"type":57,"value":1828}," baseline: ~800\u002Fday across ~120 distinct users, mostly ",{"type":52,"tag":66,"props":1830,"children":1832},{"className":1831},[],[1833],{"type":57,"value":1512},{"type":57,"value":1835}," from ",{"type":52,"tag":66,"props":1837,"children":1839},{"className":1838},[],[1840],{"type":57,"value":1841},"*.googletagmanager.com",{"type":57,"value":621},{"type":52,"tag":66,"props":1844,"children":1846},{"className":1845},[],[1847],{"type":57,"value":1848},"*.googlesyndication.com",{"type":57,"value":1850},". Anything above 1.5× this baseline is fresh.\"",{"type":52,"tag":233,"props":1852,"children":1853},{},[1854,1855,1861,1862],{"type":57,"value":1808},{"type":52,"tag":66,"props":1856,"children":1858},{"className":1857},[],[1859],{"type":57,"value":1860},"allowlist:csp_violations:gtm",{"type":57,"value":1816},{"type":52,"tag":801,"props":1863,"children":1864},{},[1865,1867,1872,1873,1878,1879,1885],{"type":57,"value":1866},"\"",{"type":52,"tag":66,"props":1868,"children":1870},{"className":1869},[],[1871],{"type":57,"value":1841},{"type":57,"value":397},{"type":52,"tag":66,"props":1874,"children":1876},{"className":1875},[],[1877],{"type":57,"value":1848},{"type":57,"value":397},{"type":52,"tag":66,"props":1880,"children":1882},{"className":1881},[],[1883],{"type":57,"value":1884},"*.doubleclick.net",{"type":57,"value":1886}," are the team's expected analytics\u002Fads domains — known, vetted, do not re-surface.\"",{"type":52,"tag":233,"props":1888,"children":1889},{},[1890,1891,1897,1898],{"type":57,"value":1808},{"type":52,"tag":66,"props":1892,"children":1894},{"className":1893},[],[1895],{"type":57,"value":1896},"noise:csp_violations:chrome-extension-scheme",{"type":57,"value":1816},{"type":52,"tag":801,"props":1899,"children":1900},{},[1901,1903,1909,1911,1916,1917,1923],{"type":57,"value":1902},"\"Blocked URL pattern ",{"type":52,"tag":66,"props":1904,"children":1906},{"className":1905},[],[1907],{"type":57,"value":1908},"chrome-extension:\u002F\u002F*",{"type":57,"value":1910}," is a recurring browser-extension noise source for this team — skip unless ",{"type":52,"tag":66,"props":1912,"children":1914},{"className":1913},[],[1915],{"type":57,"value":286},{"type":57,"value":621},{"type":52,"tag":66,"props":1918,"children":1920},{"className":1919},[],[1921],{"type":57,"value":1922},"effective_directive=script-src",{"type":57,"value":1924},".\"",{"type":52,"tag":233,"props":1926,"children":1927},{},[1928,1929,1935,1936],{"type":57,"value":1808},{"type":52,"tag":66,"props":1930,"children":1932},{"className":1931},[],[1933],{"type":57,"value":1934},"addressed:csp_violations:cdn.suspicious.example.com",{"type":57,"value":1816},{"type":52,"tag":801,"props":1937,"children":1938},{},[1939,1941,1946,1948,1954],{"type":57,"value":1940},"\"Surfaced fresh ",{"type":52,"tag":66,"props":1942,"children":1944},{"className":1943},[],[1945],{"type":57,"value":1343},{"type":57,"value":1947}," cluster from ",{"type":52,"tag":66,"props":1949,"children":1951},{"className":1950},[],[1952],{"type":57,"value":1953},"cdn.suspicious.example.com",{"type":57,"value":1955}," on 2026-05-12; team confirmed it was a legitimate new vendor, allowlisted in policy on 2026-05-13. Do not re-file unless the domain re-appears after policy was widened.\"",{"type":52,"tag":233,"props":1957,"children":1958},{},[1959,1960,1966,1967],{"type":57,"value":1808},{"type":52,"tag":66,"props":1961,"children":1963},{"className":1962},[],[1964],{"type":57,"value":1965},"dedupe:csp_violations:a1b2c3d4",{"type":57,"value":1816},{"type":52,"tag":801,"props":1968,"children":1969},{},[1970,1972,1978,1979,1984,1986,1992,1993,1998,1999,2005],{"type":57,"value":1971},"\"Fingerprint ",{"type":52,"tag":66,"props":1973,"children":1975},{"className":1974},[],[1976],{"type":57,"value":1977},"a1b2c3d4...",{"type":57,"value":373},{"type":52,"tag":66,"props":1980,"children":1982},{"className":1981},[],[1983],{"type":57,"value":1343},{"type":57,"value":1985}," | ",{"type":52,"tag":66,"props":1987,"children":1989},{"className":1988},[],[1990],{"type":57,"value":1991},"evil.example.com\u002Fx.js",{"type":57,"value":1985},{"type":52,"tag":66,"props":1994,"children":1996},{"className":1995},[],[1997],{"type":57,"value":1578},{"type":57,"value":1985},{"type":52,"tag":66,"props":2000,"children":2002},{"className":2001},[],[2003],{"type":57,"value":2004},"bundle.js",{"type":57,"value":2006},") — surfaced 2026-05-08, report still open in inbox. If this exact fingerprint fires again, edit the existing report; don't author fresh.\"",{"type":52,"tag":233,"props":2008,"children":2009},{},[2010,2011,2017,2018],{"type":57,"value":1808},{"type":52,"tag":66,"props":2012,"children":2014},{"className":2013},[],[2015],{"type":57,"value":2016},"report:csp_violations:\u003Cblocked_domain>-\u003Cdirective>",{"type":57,"value":1816},{"type":52,"tag":801,"props":2019,"children":2020},{},[2021,2023,2029],{"type":57,"value":2022},"the ",{"type":52,"tag":66,"props":2024,"children":2026},{"className":2025},[],[2027],{"type":57,"value":2028},"report_id",{"type":57,"value":2030}," of a report you filed for a cluster on this domain\u002Fdirective, so the next run edits it (append_note with the fresh reach) instead of duplicating.",{"type":52,"tag":233,"props":2032,"children":2033},{},[2034,2035,2041,2042],{"type":57,"value":1808},{"type":52,"tag":66,"props":2036,"children":2038},{"className":2037},[],[2039],{"type":57,"value":2040},"reviewer:csp_violations:\u003Carea>",{"type":57,"value":1816},{"type":52,"tag":801,"props":2043,"children":2044},{},[2045],{"type":57,"value":2046},"a resolved owner (bare lowercase GitHub login) for the security \u002F frontend \u002F policy surface, so reports route to a human faster.",{"type":52,"tag":60,"props":2048,"children":2049},{},[2050],{"type":57,"value":2051},"By run #5 you'll have a per-team domain allowlist in the scratchpad, known browser-extension noise patterns, and the typical per-directive shape — and burn near-zero time on cold-start exploration.",{"type":52,"tag":349,"props":2053,"children":2055},{"id":2054},"decide",[2056],{"type":57,"value":2057},"Decide",{"type":52,"tag":60,"props":2059,"children":2060},{},[2061,2063,2069,2071,2076,2078,2083,2085,2090,2091,2096,2098,2103,2104,2109],{"type":57,"value":2062},"The generic report mechanics — searching the inbox for your own prior reports (via the ",{"type":52,"tag":66,"props":2064,"children":2066},{"className":2065},[],[2067],{"type":57,"value":2068},"report:csp_violations:*",{"type":57,"value":2070}," pointer, else an ",{"type":52,"tag":66,"props":2072,"children":2074},{"className":2073},[],[2075],{"type":57,"value":521},{"type":57,"value":2077}," search on the specific blocked domain \u002F directive, not a broad word like ",{"type":52,"tag":66,"props":2079,"children":2081},{"className":2080},[],[2082],{"type":57,"value":1343},{"type":57,"value":2084},"), edit-vs-author, the status rules, reviewer routing, non-idempotent dedup, and the ",{"type":52,"tag":66,"props":2086,"children":2088},{"className":2087},[],[2089],{"type":57,"value":120},{"type":57,"value":99},{"type":52,"tag":66,"props":2092,"children":2094},{"className":2093},[],[2095],{"type":57,"value":127},{"type":57,"value":2097}," fields — live in the harness prompt and in ",{"type":52,"tag":66,"props":2099,"children":2101},{"className":2100},[],[2102],{"type":57,"value":135},{"type":57,"value":137},{"type":52,"tag":66,"props":2105,"children":2107},{"className":2106},[],[2108],{"type":57,"value":143},{"type":57,"value":2110},". Do not re-derive them here. This section is only the CSP judgment layered on top:",{"type":52,"tag":229,"props":2112,"children":2113},{},[2114,2132,2208,2218],{"type":52,"tag":233,"props":2115,"children":2116},{},[2117,2122,2124,2130],{"type":52,"tag":80,"props":2118,"children":2119},{},[2120],{"type":57,"value":2121},"Edit",{"type":57,"value":2123}," when a still-live report already tracks the domain\u002Fdirective cluster — a fresh domain still blocked, an enforced block still degrading users, a directive burst still elevated. A persistent cluster is one report across runs: a new window confirming it's ongoing is a re-escalation (",{"type":52,"tag":66,"props":2125,"children":2127},{"className":2126},[],[2128],{"type":57,"value":2129},"append_note",{"type":57,"value":2131}," the fresh reach \u002F occurrences), not a fresh report per tick.",{"type":52,"tag":233,"props":2133,"children":2134},{},[2135,2140,2142,2148,2150,2156,2158,2164,2166,2171,2173,2178,2179,2184,2186,2191,2193,2198,2200,2206],{"type":52,"tag":80,"props":2136,"children":2137},{},[2138],{"type":57,"value":2139},"Author",{"type":57,"value":2141}," when nothing live covers the cluster. A report-worthy finding names the blocked domain, the effective directive(s), the document URL(s), the distinct-user count, and a time range in the ",{"type":52,"tag":66,"props":2143,"children":2145},{"className":2144},[],[2146],{"type":57,"value":2147},"evidence",{"type":57,"value":2149},", with an explicit lens (policy widen \u002F compromise \u002F vendor drift). These are investigations, not code fixes → ",{"type":52,"tag":66,"props":2151,"children":2153},{"className":2152},[],[2154],{"type":57,"value":2155},"actionability=requires_human_input",{"type":57,"value":2157}," + ",{"type":52,"tag":66,"props":2159,"children":2161},{"className":2160},[],[2162],{"type":57,"value":2163},"repository=NO_REPO",{"type":57,"value":2165},". Priority: a ",{"type":52,"tag":66,"props":2167,"children":2169},{"className":2168},[],[2170],{"type":57,"value":286},{"type":57,"value":2172}," block on a ",{"type":52,"tag":66,"props":2174,"children":2176},{"className":2175},[],[2177],{"type":57,"value":1343},{"type":57,"value":99},{"type":52,"tag":66,"props":2180,"children":2182},{"className":2181},[],[2183],{"type":57,"value":1350},{"type":57,"value":2185}," directive with broad reach, or a suspected compromise, is ",{"type":52,"tag":80,"props":2187,"children":2188},{},[2189],{"type":57,"value":2190},"P1–P2",{"type":57,"value":2192}," (functionality broken \u002F possible security incident); a policy-allowlist-gap or vendor-drift finding is ",{"type":52,"tag":80,"props":2194,"children":2195},{},[2196],{"type":57,"value":2197},"P2–P3",{"type":57,"value":2199}," by reach. After authoring, write the ",{"type":52,"tag":66,"props":2201,"children":2203},{"className":2202},[],[2204],{"type":57,"value":2205},"report:csp_violations:\u003Cdomain>-\u003Cdirective>",{"type":57,"value":2207}," pointer so the next run edits it.",{"type":52,"tag":233,"props":2209,"children":2210},{},[2211,2216],{"type":52,"tag":80,"props":2212,"children":2213},{},[2214],{"type":57,"value":2215},"Remember",{"type":57,"value":2217}," if below the bar but worth carrying forward (a fresh domain with only 3 distinct users — let it ripen), or to record what you ruled out.",{"type":52,"tag":233,"props":2219,"children":2220},{},[2221,2226,2228,2233,2234,2239,2240,2245,2246,2251],{"type":52,"tag":80,"props":2222,"children":2223},{},[2224],{"type":57,"value":2225},"Skip",{"type":57,"value":2227}," with a one-line note if a ",{"type":52,"tag":66,"props":2229,"children":2231},{"className":2230},[],[2232],{"type":57,"value":403},{"type":57,"value":99},{"type":52,"tag":66,"props":2235,"children":2237},{"className":2236},[],[2238],{"type":57,"value":424},{"type":57,"value":99},{"type":52,"tag":66,"props":2241,"children":2243},{"className":2242},[],[2244],{"type":57,"value":410},{"type":57,"value":99},{"type":52,"tag":66,"props":2247,"children":2249},{"className":2248},[],[2250],{"type":57,"value":417},{"type":57,"value":2252}," entry, or an existing inbox report, already covers it.",{"type":52,"tag":60,"props":2254,"children":2255},{},[2256,2261,2263,2268,2270,2275,2277,2282,2284,2289],{"type":52,"tag":80,"props":2257,"children":2258},{},[2259],{"type":57,"value":2260},"The push path is the key dedupe partner.",{"type":57,"value":2262}," The companion push emission (",{"type":52,"tag":66,"props":2264,"children":2266},{"className":2265},[],[2267],{"type":57,"value":166},{"type":57,"value":2269},") already drops one raw signal per violation fingerprint into the same inbox. Cross-check it (",{"type":52,"tag":66,"props":2271,"children":2273},{"className":2272},[],[2274],{"type":57,"value":521},{"type":57,"value":2276}," filtered to ",{"type":52,"tag":66,"props":2278,"children":2280},{"className":2279},[],[2281],{"type":57,"value":166},{"type":57,"value":2283},") before authoring: your aggregated report should ",{"type":52,"tag":80,"props":2285,"children":2286},{},[2287],{"type":57,"value":2288},"reference those raw signals as evidence",{"type":57,"value":2290}," (by fingerprint) rather than re-state them, and stay quiet when a single raw fingerprint already covers the whole story — author only when the aggregation adds cross-fingerprint context the push path can't see.",{"type":52,"tag":349,"props":2292,"children":2294},{"id":2293},"close-out",[2295],{"type":57,"value":2296},"Close out",{"type":52,"tag":60,"props":2298,"children":2299},{},[2300,2305,2307,2312,2314,2318],{"type":52,"tag":80,"props":2301,"children":2302},{},[2303],{"type":57,"value":2304},"Summarize the run",{"type":57,"value":2306}," — one paragraph: looked at what, which reports you authored or edited, remembered what, ruled out what. The harness writes that summary to the run row as searchable prose; future runs read it via ",{"type":52,"tag":66,"props":2308,"children":2310},{"className":2309},[],[2311],{"type":57,"value":450},{"type":57,"value":2313},". Do ",{"type":52,"tag":80,"props":2315,"children":2316},{},[2317],{"type":57,"value":278},{"type":57,"value":2319}," write a separate \"run metadata\" scratchpad entry — the run summary already serves that role.",{"type":52,"tag":178,"props":2321,"children":2323},{"id":2322},"disqualifiers-skip-these",[2324],{"type":57,"value":2325},"Disqualifiers (skip these)",{"type":52,"tag":229,"props":2327,"children":2328},{},[2329,2353,2387,2404,2420,2437],{"type":52,"tag":233,"props":2330,"children":2331},{},[2332,2337,2339,2344,2346,2351],{"type":52,"tag":80,"props":2333,"children":2334},{},[2335],{"type":57,"value":2336},"Single user, single document, single fingerprint",{"type":57,"value":2338}," — almost always a personal browser extension or a niche client. Low ",{"type":52,"tag":66,"props":2340,"children":2342},{"className":2341},[],[2343],{"type":57,"value":209},{"type":57,"value":2345}," AND ",{"type":52,"tag":66,"props":2347,"children":2349},{"className":2348},[],[2350],{"type":57,"value":489},{"type":57,"value":2352}," ≤ 2.",{"type":52,"tag":233,"props":2354,"children":2355},{},[2356,2385],{"type":52,"tag":80,"props":2357,"children":2358},{},[2359,2361,2366,2367,2372,2373,2379,2380],{"type":57,"value":2360},"Blocked URL scheme is ",{"type":52,"tag":66,"props":2362,"children":2364},{"className":2363},[],[2365],{"type":57,"value":1642},{"type":57,"value":99},{"type":52,"tag":66,"props":2368,"children":2370},{"className":2369},[],[2371],{"type":57,"value":1649},{"type":57,"value":99},{"type":52,"tag":66,"props":2374,"children":2376},{"className":2375},[],[2377],{"type":57,"value":2378},"about:",{"type":57,"value":99},{"type":52,"tag":66,"props":2381,"children":2383},{"className":2382},[],[2384],{"type":57,"value":1679},{"type":57,"value":2386}," — browser-side, not server-side; team can't fix.",{"type":52,"tag":233,"props":2388,"children":2389},{},[2390,2402],{"type":52,"tag":80,"props":2391,"children":2392},{},[2393,2395,2400],{"type":57,"value":2394},"Domain matches an ",{"type":52,"tag":66,"props":2396,"children":2398},{"className":2397},[],[2399],{"type":57,"value":424},{"type":57,"value":2401}," scratchpad entry",{"type":57,"value":2403}," — the team has already vetted this vendor; skip without re-surfacing.",{"type":52,"tag":233,"props":2405,"children":2406},{},[2407,2418],{"type":52,"tag":80,"props":2408,"children":2409},{},[2410,2416],{"type":52,"tag":66,"props":2411,"children":2413},{"className":2412},[],[2414],{"type":57,"value":2415},"disposition=report-only",{"type":57,"value":2417}," with no enforcement signal",{"type":57,"value":2419}," — the team is deliberately collecting violations to refine policy. File a report only when reach \u002F freshness \u002F domain novelty is exceptional.",{"type":52,"tag":233,"props":2421,"children":2422},{},[2423,2435],{"type":52,"tag":80,"props":2424,"children":2425},{},[2426,2428,2433],{"type":57,"value":2427},"Fingerprint matches a ",{"type":52,"tag":66,"props":2429,"children":2431},{"className":2430},[],[2432],{"type":57,"value":417},{"type":57,"value":2434}," scratchpad entry from an open inbox report",{"type":57,"value":2436}," — the push-emission path already covered it; don't double-up.",{"type":52,"tag":233,"props":2438,"children":2439},{},[2440,2459],{"type":52,"tag":80,"props":2441,"children":2442},{},[2443,2445,2451,2453],{"type":57,"value":2444},"Team has no ",{"type":52,"tag":66,"props":2446,"children":2448},{"className":2447},[],[2449],{"type":57,"value":2450},"signal_source_config",{"type":57,"value":2452}," row for ",{"type":52,"tag":66,"props":2454,"children":2456},{"className":2455},[],[2457],{"type":57,"value":2458},"csp_reporting",{"type":57,"value":2460}," — push emission is off for this team. Scout can still find clusters, but the user signal is \"team hasn't opted in to CSP signals yet\"; raise the bar accordingly — require exceptional reach before filing.",{"type":52,"tag":60,"props":2462,"children":2463},{},[2464],{"type":57,"value":2465},"When in doubt, write a memory entry instead of filing a report.",{"type":52,"tag":178,"props":2467,"children":2469},{"id":2468},"mcp-tools",[2470],{"type":57,"value":2471},"MCP tools",{"type":52,"tag":60,"props":2473,"children":2474},{},[2475],{"type":57,"value":2476},"Direct calls (read-only):",{"type":52,"tag":229,"props":2478,"children":2479},{},[2480,2543,2576,2599,2629,2640],{"type":52,"tag":233,"props":2481,"children":2482},{},[2483,2489,2491,2497,2499,2505,2507,2513,2514,2520,2521,2526,2527,2533,2535,2541],{"type":52,"tag":66,"props":2484,"children":2486},{"className":2485},[],[2487],{"type":57,"value":2488},"execute-sql",{"type":57,"value":2490}," against ",{"type":52,"tag":66,"props":2492,"children":2494},{"className":2493},[],[2495],{"type":57,"value":2496},"events",{"type":57,"value":2498}," (filtered to ",{"type":52,"tag":66,"props":2500,"children":2502},{"className":2501},[],[2503],{"type":57,"value":2504},"event = '$csp_violation'",{"type":57,"value":2506},") — primary drill-down. Group by ",{"type":52,"tag":66,"props":2508,"children":2510},{"className":2509},[],[2511],{"type":57,"value":2512},"domain($csp_blocked_url)",{"type":57,"value":397},{"type":52,"tag":66,"props":2515,"children":2517},{"className":2516},[],[2518],{"type":57,"value":2519},"$csp_effective_directive",{"type":57,"value":397},{"type":52,"tag":66,"props":2522,"children":2524},{"className":2523},[],[2525],{"type":57,"value":1181},{"type":57,"value":397},{"type":52,"tag":66,"props":2528,"children":2530},{"className":2529},[],[2531],{"type":57,"value":2532},"$csp_source_file",{"type":57,"value":2534},". The full property list is in ",{"type":52,"tag":66,"props":2536,"children":2538},{"className":2537},[],[2539],{"type":57,"value":2540},"posthog\u002Fapi\u002Fcsp.py",{"type":57,"value":2542},".",{"type":52,"tag":233,"props":2544,"children":2545},{},[2546,2552,2553,2559,2560,2566,2568,2574],{"type":52,"tag":66,"props":2547,"children":2549},{"className":2548},[],[2550],{"type":57,"value":2551},"read-data-schema",{"type":57,"value":373},{"type":52,"tag":66,"props":2554,"children":2556},{"className":2555},[],[2557],{"type":57,"value":2558},"kind: event_properties",{"type":57,"value":397},{"type":52,"tag":66,"props":2561,"children":2563},{"className":2562},[],[2564],{"type":57,"value":2565},"event_name: '$csp_violation'",{"type":57,"value":2567},") — discover the team's actual ",{"type":52,"tag":66,"props":2569,"children":2571},{"className":2570},[],[2572],{"type":57,"value":2573},"$csp_*",{"type":57,"value":2575}," property surface and sample values.",{"type":52,"tag":233,"props":2577,"children":2578},{},[2579,2584,2586,2591,2592,2597],{"type":52,"tag":66,"props":2580,"children":2582},{"className":2581},[],[2583],{"type":57,"value":1486},{"type":57,"value":2585}," — pair burst timestamps with recent deploys or feature-flag changes for cross-source convergence. Inbox & reviewer routing (mechanics in ",{"type":52,"tag":66,"props":2587,"children":2589},{"className":2588},[],[2590],{"type":57,"value":135},{"type":57,"value":137},{"type":52,"tag":66,"props":2593,"children":2595},{"className":2594},[],[2596],{"type":57,"value":143},{"type":57,"value":2598},"):",{"type":52,"tag":233,"props":2600,"children":2601},{},[2602,2607,2608,2613,2615,2620,2622,2627],{"type":52,"tag":66,"props":2603,"children":2605},{"className":2604},[],[2606],{"type":57,"value":521},{"type":57,"value":99},{"type":52,"tag":66,"props":2609,"children":2611},{"className":2610},[],[2612],{"type":57,"value":570},{"type":57,"value":2614}," — the reports already in the inbox. Check your own prior reports (",{"type":52,"tag":66,"props":2616,"children":2618},{"className":2617},[],[2619],{"type":57,"value":174},{"type":57,"value":2621},") so you edit instead of duplicating, and the push path's raw signals (",{"type":52,"tag":66,"props":2623,"children":2625},{"className":2624},[],[2626],{"type":57,"value":166},{"type":57,"value":2628},") so you don't re-state a fingerprint it already covers.",{"type":52,"tag":233,"props":2630,"children":2631},{},[2632,2638],{"type":52,"tag":66,"props":2633,"children":2635},{"className":2634},[],[2636],{"type":57,"value":2637},"inbox-report-artefacts-list",{"type":57,"value":2639}," — a comparable report's artefact log; reviewer precedent.",{"type":52,"tag":233,"props":2641,"children":2642},{},[2643,2649,2651,2657],{"type":52,"tag":66,"props":2644,"children":2646},{"className":2645},[],[2647],{"type":57,"value":2648},"scout-members-list",{"type":57,"value":2650}," — the in-run roster for routing ",{"type":52,"tag":66,"props":2652,"children":2654},{"className":2653},[],[2655],{"type":57,"value":2656},"suggested_reviewers",{"type":57,"value":2658}," to a security \u002F frontend \u002F policy owner.",{"type":52,"tag":60,"props":2660,"children":2661},{},[2662],{"type":57,"value":2663},"Harness-level:",{"type":52,"tag":229,"props":2665,"children":2666},{},[2667,2696,2712],{"type":52,"tag":233,"props":2668,"children":2669},{},[2670,2675,2676,2681,2682,2687,2688,2694],{"type":52,"tag":66,"props":2671,"children":2673},{"className":2672},[],[2674],{"type":57,"value":461},{"type":57,"value":99},{"type":52,"tag":66,"props":2677,"children":2679},{"className":2678},[],[2680],{"type":57,"value":371},{"type":57,"value":99},{"type":52,"tag":66,"props":2683,"children":2685},{"className":2684},[],[2686],{"type":57,"value":450},{"type":57,"value":99},{"type":52,"tag":66,"props":2689,"children":2691},{"className":2690},[],[2692],{"type":57,"value":2693},"scout-runs-retrieve",{"type":57,"value":2695}," — orientation + dedupe.",{"type":52,"tag":233,"props":2697,"children":2698},{},[2699,2704,2705,2710],{"type":52,"tag":66,"props":2700,"children":2702},{"className":2701},[],[2703],{"type":57,"value":97},{"type":57,"value":99},{"type":52,"tag":66,"props":2706,"children":2708},{"className":2707},[],[2709],{"type":57,"value":105},{"type":57,"value":2711}," — author a report \u002F edit an existing one (the report-channel contract is in the harness prompt).",{"type":52,"tag":233,"props":2713,"children":2714},{},[2715,2721],{"type":52,"tag":66,"props":2716,"children":2718},{"className":2717},[],[2719],{"type":57,"value":2720},"scout-scratchpad-remember",{"type":57,"value":2722}," — remember.",{"type":52,"tag":178,"props":2724,"children":2726},{"id":2725},"when-to-stop",[2727],{"type":57,"value":2728},"When to stop",{"type":52,"tag":229,"props":2730,"children":2731},{},[2732,2748,2778],{"type":52,"tag":233,"props":2733,"children":2734},{},[2735,2740,2742,2746],{"type":52,"tag":66,"props":2736,"children":2738},{"className":2737},[],[2739],{"type":57,"value":71},{"type":57,"value":2741}," row in profile is at baseline ",{"type":52,"tag":80,"props":2743,"children":2744},{},[2745],{"type":57,"value":1163},{"type":57,"value":2747}," the standing enforced \u002F first-party block check is clean → close out empty. A steady baseline alone is not enough — a standing high-reach enforced (or first-party) block is a live problem even with no fresh burst.",{"type":52,"tag":233,"props":2749,"children":2750},{},[2751,2753,2758,2759,2764,2765,2770,2771,2776],{"type":57,"value":2752},"A candidate matches a scratchpad entry with ",{"type":52,"tag":66,"props":2754,"children":2756},{"className":2755},[],[2757],{"type":57,"value":403},{"type":57,"value":99},{"type":52,"tag":66,"props":2760,"children":2762},{"className":2761},[],[2763],{"type":57,"value":424},{"type":57,"value":99},{"type":52,"tag":66,"props":2766,"children":2768},{"className":2767},[],[2769],{"type":57,"value":410},{"type":57,"value":99},{"type":52,"tag":66,"props":2772,"children":2774},{"className":2773},[],[2775],{"type":57,"value":417},{"type":57,"value":2777}," key prefix, or an existing inbox report → edit-or-skip with a one-line note.",{"type":52,"tag":233,"props":2779,"children":2780},{},[2781],{"type":57,"value":2782},"You've validated some hypotheses and filed reports for what's solid → close out, even if there's more you could look at. Fewer, better reports.",{"type":52,"tag":60,"props":2784,"children":2785},{},[2786],{"type":57,"value":2787},"\"Looked but found nothing meaningful\" is a real outcome.",{"type":52,"tag":178,"props":2789,"children":2791},{"id":2790},"how-this-relates-to-the-push-based-csp-source",[2792],{"type":57,"value":2793},"How this relates to the push-based CSP source",{"type":52,"tag":60,"props":2795,"children":2796},{},[2797,2799,2805,2807,2813,2815,2820,2822,2828],{"type":57,"value":2798},"The companion push path (",{"type":52,"tag":66,"props":2800,"children":2802},{"className":2801},[],[2803],{"type":57,"value":2804},"posthog\u002Ftasks\u002Fcsp_signal.py",{"type":57,"value":2806},", behind per-team ",{"type":52,"tag":66,"props":2808,"children":2810},{"className":2809},[],[2811],{"type":57,"value":2812},"SignalSourceConfig",{"type":57,"value":2814}," opt-in) emits ",{"type":52,"tag":80,"props":2816,"children":2817},{},[2818],{"type":57,"value":2819},"one raw signal per unique violation fingerprint",{"type":57,"value":2821}," with a 24h Redis dedup TTL. That gives the inbox raw coverage of every fresh ",{"type":52,"tag":66,"props":2823,"children":2825},{"className":2824},[],[2826],{"type":57,"value":2827},"(directive, blocked_url, document_url, source_file)",{"type":57,"value":2829}," tuple, but per-fingerprint and without cross-fingerprint context.",{"type":52,"tag":60,"props":2831,"children":2832},{},[2833,2835,2840],{"type":57,"value":2834},"This scout is the ",{"type":52,"tag":80,"props":2836,"children":2837},{},[2838],{"type":57,"value":2839},"aggregation layer above it.",{"type":57,"value":2841}," Its reports should:",{"type":52,"tag":229,"props":2843,"children":2844},{},[2845,2850,2855],{"type":52,"tag":233,"props":2846,"children":2847},{},[2848],{"type":57,"value":2849},"Bundle multiple raw fingerprints into a single aggregated report with shared root cause (one new domain across many pages, one deploy regression across many directives, one compromise pattern across many users).",{"type":52,"tag":233,"props":2851,"children":2852},{},[2853],{"type":57,"value":2854},"Use the push path's existing signals as evidence in the report's body (referenced by fingerprint \u002F source_id) rather than re-deriving them.",{"type":52,"tag":233,"props":2856,"children":2857},{},[2858],{"type":57,"value":2859},"Stay quiet when the push path's coverage is sufficient — a single raw fingerprint already in the inbox does not need a parallel scout report unless the aggregation adds new context.",{"type":52,"tag":2861,"props":2862,"children":2863},"style",{},[2864],{"type":57,"value":2865},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"items":2867,"total":3036},[2868,2885,2897,2910,2923,2938,2952,2969,2983,2998,3008,3026],{"slug":2869,"name":2869,"fn":2870,"description":2871,"org":2872,"tags":2873,"stars":2882,"repoUrl":2883,"updatedAt":2884},"analyzing-expensive-users","analyze expensive users in AI observability","Analyze the most expensive users in AI observability and explain why they cost so much. Use when the user asks about top spenders, expensive users, per-user LLM cost, user-level cost drivers, or patterns behind high AI observability spend.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2874,2877,2880,2881],{"name":2875,"slug":2876,"type":15},"Analytics","analytics",{"name":2878,"slug":2879,"type":15},"Cost Optimization","cost-optimization",{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},35568,"https:\u002F\u002Fgithub.com\u002FPostHog\u002Fposthog","2026-07-28T05:34:11.117757",{"slug":2886,"name":2886,"fn":2887,"description":2888,"org":2889,"tags":2890,"stars":2882,"repoUrl":2883,"updatedAt":2896},"auditing-endpoints","audit PostHog project endpoints","Audit every endpoint in a PostHog project for staleness, failed materialisations, and unused materialised versions. Use when the user asks \"what endpoints can I clean up?\", \"are any of my endpoints broken?\", \"which materialised versions are still being called?\", or wants a one-shot cleanup pass over the Endpoints product. Produces a prioritised report grouped by issue type, with recommended actions but does not modify anything without explicit confirmation.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2891,2892,2895],{"name":2875,"slug":2876,"type":15},{"name":2893,"slug":2894,"type":15},"Audit","audit",{"name":9,"slug":8,"type":15},"2026-06-08T08:08:33.693989",{"slug":2898,"name":2898,"fn":2899,"description":2900,"org":2901,"tags":2902,"stars":2882,"repoUrl":2883,"updatedAt":2909},"auditing-warehouse-source-health","audit PostHog data warehouse source health","Audit the health of a PostHog project's data warehouse sources and syncs — find every broken or degraded source connection, sync schema, and webhook channel. Use when the user asks \"why are my imports failing?\", \"what's broken with my sources?\", \"why is my warehouse data stale?\", or wants a one-shot triage of source\u002Fsync health before deciding where to dig in. Produces a prioritized report grouped by severity, with recommended next steps. For materialized-view health use `auditing-warehouse-view-health`; for a single failing sync use `diagnosing-failed-warehouse-syncs`.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2903,2904,2907,2908],{"name":2893,"slug":2894,"type":15},{"name":2905,"slug":2906,"type":15},"Data Warehouse","data-warehouse",{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},"2026-06-18T08:22:57.67984",{"slug":2911,"name":2911,"fn":2912,"description":2913,"org":2914,"tags":2915,"stars":2882,"repoUrl":2883,"updatedAt":2922},"auditing-warehouse-view-health","audit PostHog materialized view health","Audit the health of a PostHog project's materialized views (saved queries) — find every failed materialization and flag unused or stale materialized views that cost storage and compute. Use when the user asks \"which of my views are broken?\", \"why is this materialized view failing?\", \"are any of my views wasting compute?\", or wants a one-shot triage of view health. For source\u002Fsync health use `auditing-warehouse-source-health`.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2916,2917,2918,2921],{"name":2893,"slug":2894,"type":15},{"name":2905,"slug":2906,"type":15},{"name":2919,"slug":2920,"type":15},"Performance","performance",{"name":9,"slug":8,"type":15},"2026-06-18T08:25:10.936787",{"slug":2924,"name":2924,"fn":2925,"description":2926,"org":2927,"tags":2928,"stars":2882,"repoUrl":2883,"updatedAt":2937},"authoring-error-tracking-alerts","author PostHog error tracking alerts","Author error tracking alerts that fire when an issue is created, reopened, or starts spiking. Use when the user asks to set up error notifications, route exceptions to Slack\u002Fwebhook\u002FLinear, or evaluate which error events are worth alerting on. Covers trigger-event selection, integration choice, dedup against existing alerts, and shipping with the canonical message body shape.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2929,2932,2935,2936],{"name":2930,"slug":2931,"type":15},"Alerting","alerting",{"name":2933,"slug":2934,"type":15},"Debugging","debugging",{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},"2026-06-18T08:24:40.318583",{"slug":2939,"name":2939,"fn":2940,"description":2941,"org":2942,"tags":2943,"stars":2882,"repoUrl":2883,"updatedAt":2951},"authoring-log-alerts","author log alerts in PostHog","Author useful, low-noise log alerts on services in a PostHog project. Use when the user asks to set up alerts for their logs, suggest alerts they should add, or evaluate whether a service is worth monitoring. Covers service triage, baseline characterisation, threshold drafting, back-testing via simulate, and shipping with a notification destination.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2944,2945,2946,2947,2950],{"name":2875,"slug":2876,"type":15},{"name":21,"slug":22,"type":15},{"name":17,"slug":18,"type":15},{"name":2948,"slug":2949,"type":15},"Operations","operations",{"name":9,"slug":8,"type":15},"2026-07-18T05:10:54.430898",{"slug":2953,"name":2953,"fn":2954,"description":2955,"org":2956,"tags":2957,"stars":2882,"repoUrl":2883,"updatedAt":2968},"building-workflows","build and edit PostHog workflows","Build, edit, test, enable, and monitor PostHog workflows over MCP. Author the action\u002Fedge graph so it runs and opens cleanly in the visual editor, then change drafts surgically with patch operations. Use when asked to build, set up, automate, change, fix, or debug a workflow, campaign, broadcast, drip sequence, or event-triggered automation in the workflows product.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2958,2961,2964,2965],{"name":2959,"slug":2960,"type":15},"Automation","automation",{"name":2962,"slug":2963,"type":15},"MCP","mcp",{"name":9,"slug":8,"type":15},{"name":2966,"slug":2967,"type":15},"Workflow Automation","workflow-automation","2026-07-28T05:34:12.167015",{"slug":2970,"name":2970,"fn":2971,"description":2972,"org":2973,"tags":2974,"stars":2882,"repoUrl":2883,"updatedAt":2982},"check-posthog-loading","inspect PostHog SDK loading across URLs","Inspect how the PostHog JavaScript SDK is loaded across a list of URLs. Use to confirm consistent installation across pages, find pages missing the snippet, detect mismatched API keys or hosts between pages, and verify the load method (head snippet vs deferred vs array.js).\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2975,2976,2977,2980,2981],{"name":2875,"slug":2876,"type":15},{"name":2933,"slug":2934,"type":15},{"name":2978,"slug":2979,"type":15},"Frontend","frontend",{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},"2026-05-07T05:56:19.828048",{"slug":2984,"name":2984,"fn":2985,"description":2986,"org":2987,"tags":2988,"stars":2882,"repoUrl":2883,"updatedAt":2997},"consuming-endpoints-from-client-code","integrate PostHog endpoints into client applications","Wire a PostHog endpoint into a client app or SDK. Covers fetching the OpenAPI spec, generating a typed client with openapi-generator or @hey-api\u002Fopenapi-ts, sending the right auth header, shaping the variables payload (HogQL code_name vs insight breakdown property), handling rate-limit and materialised-endpoint error responses. Use when the user says \"how do I call my endpoint\", \"generate a client for this\", or \"what auth header do I use\".\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[2989,2992,2993,2994],{"name":2990,"slug":2991,"type":15},"API Development","api-development",{"name":2978,"slug":2979,"type":15},{"name":9,"slug":8,"type":15},{"name":2995,"slug":2996,"type":15},"SDK","sdk","2026-06-08T08:08:34.929454",{"slug":2999,"name":2999,"fn":3000,"description":3001,"org":3002,"tags":3003,"stars":2882,"repoUrl":2883,"updatedAt":3007},"copying-endpoints-across-projects","copy PostHog endpoints across projects","Copy a PostHog endpoint (a saved HogQL\u002Finsight query exposed as an API route) to another project in the same organization, or duplicate it under a new name in the same project. Use when the user wants to duplicate an endpoint, promote an endpoint from staging to production, replicate an endpoint's query\u002Fvariables\u002Ffreshness config in another workspace, or clone an endpoint to iterate on it. Unlike feature flags and experiments, endpoints have NO native cross-project copy tool — this skill covers the read-then-recreate flow (endpoint-get then endpoint-create), the active-project switching it requires, name-collision checks, and the safe defaults (land unmaterialised in the target, verify with endpoint-run). Does not cover editing endpoint versions (see managing-endpoint-versions) or authoring a brand-new endpoint from scratch (see creating-an-endpoint).\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3004,3005,3006],{"name":2990,"slug":2991,"type":15},{"name":2948,"slug":2949,"type":15},{"name":9,"slug":8,"type":15},"2026-07-15T05:29:58.442727",{"slug":3009,"name":3009,"fn":3010,"description":3011,"org":3012,"tags":3013,"stars":2882,"repoUrl":2883,"updatedAt":3025},"creating-ai-subscription","schedule recurring AI-generated PostHog reports","Create a recurring AI-generated PostHog report — schedule a free-text prompt to run on a cron, with the LLM-synthesized markdown delivered to email or Slack on each tick. Use when the user wants a recurring AI summary of X on any cadence (daily, weekly, monthly, yearly) rather than a one-off report. (To attach an AI summary to an existing insight\u002Fdashboard subscription instead of a free-text prompt, see `managing-subscriptions` and its `summary_enabled` option.)\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3014,3015,3018,3019,3022],{"name":2959,"slug":2960,"type":15},{"name":3016,"slug":3017,"type":15},"Email","email",{"name":9,"slug":8,"type":15},{"name":3020,"slug":3021,"type":15},"Reporting","reporting",{"name":3023,"slug":3024,"type":15},"Slack","slack","2026-06-09T07:32:27.935712",{"slug":3027,"name":3027,"fn":3028,"description":3029,"org":3030,"tags":3031,"stars":2882,"repoUrl":2883,"updatedAt":3035},"creating-an-endpoint","create PostHog API endpoints","Create a PostHog endpoint with the right shape on the first try — covers query kind choice, name conventions, what to expose as variables (HogQL code_name vs insight breakdown), data_freshness_seconds, and whether to materialise on day one. Use when the user says \"create an endpoint\", \"expose this query as an API\", \"turn this insight into an endpoint\", or asks for help structuring a new endpoint. Steers away from common mistakes: materialising a query with cohort breakdowns or compare mode, inline-only variables on a materialised endpoint, unbounded date ranges, ambiguous names.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3032,3033,3034],{"name":2875,"slug":2876,"type":15},{"name":2990,"slug":2991,"type":15},{"name":9,"slug":8,"type":15},"2026-06-08T08:08:29.624498",231,{"items":3038,"total":3143},[3039,3054,3070,3085,3097,3109,3127],{"slug":3040,"name":3040,"fn":3041,"description":3042,"org":3043,"tags":3044,"stars":23,"repoUrl":24,"updatedAt":3053},"analyzing-experiment-session-replays","analyze session replays for PostHog experiments","Analyze session replay patterns across experiment variants to understand user behavior differences. Use when the user wants to see how users interact with different experiment variants, identify usability issues, compare behavior patterns between control and test groups, or get qualitative insights to complement quantitative experiment results.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3045,3046,3049,3050],{"name":2875,"slug":2876,"type":15},{"name":3047,"slug":3048,"type":15},"Design","design",{"name":9,"slug":8,"type":15},{"name":3051,"slug":3052,"type":15},"User Research","user-research","2026-04-06T18:44:38.291781",{"slug":3055,"name":3055,"fn":3056,"description":3057,"org":3058,"tags":3059,"stars":23,"repoUrl":24,"updatedAt":3069},"assessing-heatmaps","analyze page heatmaps and suggest improvements","Assesses what a page's heatmap is telling you and recommends concrete changes. Pulls click \u002F rageclick \u002F scroll-depth data for a URL, names the hot elements by cross-referencing autocapture events on the same page, and can create a saved heatmap the user opens in PostHog, then summarizes the behavior and proposes improvements.\nTRIGGER when: user asks what a heatmap shows, why people aren't clicking something, where users rage-click, how far they scroll, what to change on a page based on heatmap\u002Fclick data, or to 'analyze\u002Fassess\u002Freview the heatmap' for a URL.\nDO NOT TRIGGER when: the user only wants to create a saved heatmap screenshot with no analysis (use heatmaps-saved-create directly), or is asking about session replay in general (use investigating-replay).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3060,3061,3062,3063,3066],{"name":2875,"slug":2876,"type":15},{"name":2978,"slug":2979,"type":15},{"name":9,"slug":8,"type":15},{"name":3064,"slug":3065,"type":15},"Product Management","product-management",{"name":3067,"slug":3068,"type":15},"UX Design","ux-design","2026-06-05T07:40:43.37798",{"slug":3071,"name":3071,"fn":3072,"description":3073,"org":3074,"tags":3075,"stars":23,"repoUrl":24,"updatedAt":3084},"auditing-experiments-flags","audit PostHog experiments and feature flags","Audit PostHog experiments and feature flags for configuration issues, staleness, and best-practice violations. Read when the user asks to audit, health-check, or review experiments or feature flags, check flag hygiene, or verify experiment setup.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3076,3077,3080,3081],{"name":2893,"slug":2894,"type":15},{"name":3078,"slug":3079,"type":15},"Feature Flags","feature-flags",{"name":9,"slug":8,"type":15},{"name":3082,"slug":3083,"type":15},"QA","qa","2026-04-06T18:44:30.657553",{"slug":135,"name":135,"fn":3086,"description":3087,"org":3088,"tags":3089,"stars":23,"repoUrl":24,"updatedAt":3096},"author and edit PostHog Signals scouts","How to author, edit, and adapt PostHog Signals scouts — the scheduled agents that scan a project and write reports into the Signals inbox. Use when a user wants to customize a canonical scout for their own setup (narrow its scope, retune its thresholds, add disqualifiers), tweak a scout's schedule or dry-run posture, or write a brand-new scout from scratch for a specific use case (a custom event, a product surface no canonical scout covers), or steer a scout without editing it at all by leaving it a note. Covers the scout SKILL.md anatomy, the report contract, the dedupe + scratchpad-memory conventions, the scout-notes steering channel, the per-team skills-store path vs the canonical in-repo path, and the write-and-inspect test loop (with dry-run as an optional safety net). Trigger on \"write\u002Fedit\u002Fcustomize a signals scout\", \"new scout for X\", \"tune my scout schedule\", \"make a scout that watches \u003Cevent>\", \"leave a note for \u002F give feedback to a scout\", \"tell the scouts about X\".\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3090,3093,3094,3095],{"name":3091,"slug":3092,"type":15},"Agents","agents",{"name":2959,"slug":2960,"type":15},{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},"2026-07-28T05:33:45.509154",{"slug":3098,"name":3098,"fn":3099,"description":3100,"org":3101,"tags":3102,"stars":23,"repoUrl":24,"updatedAt":3108},"building-a-dashboard","build and update PostHog dashboards","Build a new dashboard, or update an existing one, from a set of insights — the same job the in-app assistant does with its upsert-dashboard tool, but over MCP. Use when a user asks to create a dashboard, put several metrics\u002Fcharts together on one page, assemble a dashboard for a topic (product analytics, retention, revenue, activation, etc.), or add\u002Fremove\u002Freplace insights on a dashboard they already have. Covers deciding create vs update, reusing existing insights vs creating new ones, and using PostHog's vetted dashboard templates as reference for what a strong dashboard on a topic looks like.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3103,3104,3107],{"name":2875,"slug":2876,"type":15},{"name":3105,"slug":3106,"type":15},"Dashboards","dashboards",{"name":2962,"slug":2963,"type":15},"2026-07-21T06:07:38.060598",{"slug":3110,"name":3110,"fn":3111,"description":3112,"org":3113,"tags":3114,"stars":23,"repoUrl":24,"updatedAt":3126},"checking-deploy-timing","correlate PostHog deployments with GitHub commits","Determine when a PostHog code change reached a given environment by reading the hidden GIT deploy annotations in the project and correlating them with the merge commit on GitHub. Use when PostHog staff ask \"when was X deployed\", \"is my change live in the US\u002FEU yet\", \"has my PR shipped\", \"did the fix roll out to prod-us\", or otherwise want to know whether\u002Fwhen a commit, PR, or feature went out to a region. Do not answer deploy-timing questions from event\u002Fdata volume alone — that only shows when data changed, not when code shipped.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3115,3118,3121,3124,3125],{"name":3116,"slug":3117,"type":15},"Deployment","deployment",{"name":3119,"slug":3120,"type":15},"Git","git",{"name":3122,"slug":3123,"type":15},"GitHub","github",{"name":17,"slug":18,"type":15},{"name":9,"slug":8,"type":15},"2026-06-28T07:46:59.53536",{"slug":3128,"name":3128,"fn":3129,"description":3130,"org":3131,"tags":3132,"stars":23,"repoUrl":24,"updatedAt":3142},"choosing-trend-or-slope-view","visualize trends and growth over time","Clarify how to visualize change over a time range before building a trend. Use whenever the user asks how much something changed, grew, dropped, improved, or regressed between two points or periods — \"how much did X change from A to B\", \"before vs after\", \"start vs end\", \"week over week\", \"compare this month to last\", \"change over time\" — or mentions a \"slope chart\" \u002F \"slopegraph\". Two readings of \"change\" need different charts: the whole trend (a line, every interval) versus just the two endpoints (a slope, start vs end). Ask which they want, then render it. Not for choosing a saved insight ChartDisplayType in the insight editor.\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[3133,3134,3137,3140,3141],{"name":2875,"slug":2876,"type":15},{"name":3135,"slug":3136,"type":15},"Charts","charts",{"name":3138,"slug":3139,"type":15},"Data Visualization","data-visualization",{"name":9,"slug":8,"type":15},{"name":3020,"slug":3021,"type":15},"2026-06-18T08:18:57.960157",56]