managing-path-cleaning-rules — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited managing-path-cleaning-rules (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Path cleaning rules normalize $pathname and $entry_pathname so that pages sharing the same template (/users/123/profile, /users/456/profile, …) collapse into one row (/users/<id>/profile) in Web analytics tiles, Paths insights, and any HogQL query that calls apply_path_cleaning. They are the right answer when a breakdown is fragmented across thousands of near-identical URLs.
This skill teaches you how to:
regex + alias rules in re2 syntax with the project's placeholderconvention
Team.path_cleaning_filters is a JSON list of PathCleaningFilter objects:
{
"regex": "/users/\\d+/profile",
"alias": "/users/<id>/profile",
"order": 0
}need to escape /. Anchor with ^ / $ when you mean it.
(<id>, <slug>, <uuid>, <date>) by convention so the cleaned path stays human-readable. The alias is _not_ a regex template — backreferences are not supported.
order ascending,each rule's output feeds the next.
Application is replaceRegexpAll(pathname, regex, alias) per rule, chained. Source: posthog/hogql/property.py:613.
Ask yourself: is the user complaining about cardinality (too many distinct paths in a chart), or do they want a per-URL drill-down? Path cleaning is for the former. If they want per-URL data, suggest a property filter on $pathname instead.
Don't guess at patterns — query them. With the execute-sql MCP tool:
SELECT properties.$pathname AS path, count() AS views
FROM events
WHERE event = '$pageview'
AND timestamp > now() - INTERVAL 7 DAY
GROUP BY path
ORDER BY views DESC
LIMIT 200Scan the result for:
/users/123, /orders/4242/sessions/8f3c1a3b-…/posts/why-i-love-posthog/archive/2024-09-12/en-US/, /fr-FR/?page=3, /page/3/| Pattern | Example match | regex | alias |
|---|---|---|---|
| Numeric segment | /users/123/profile | /users/\d+/profile | /users/<id>/profile |
| UUID v4 | /sessions/8f3c1a3b-… | /sessions/[0-9a-f-]{36} | /sessions/<uuid> |
| Slug | /posts/why-posthog | /posts/[a-z0-9-]+$ | /posts/<slug> |
| ISO date | /archive/2024-09-12 | /archive/\d{4}-\d{2}-\d{2} | /archive/<date> |
| Locale prefix | /en-US/about | ^/[a-z]{2}-[A-Z]{2}/ | /<locale>/ |
| Trailing query/page | /blog?page=3 | \?page=\d+$ | (empty alias drops it) |
Anchoring rules of thumb:
^ only when the segment must be at the beginning ofthe path
$ to keep a generic rule (e.g. \d+$) from matching mid-pathsegments
Three options, pick one:
/settings/project#path_cleaning has a built-in"test path" input that replays the full ordered chain.
execute-sql): SELECT replaceRegexpAll('/users/42/profile', '/users/\d+/profile', '/users/<id>/profile')Chain replaceRegexpAll calls in the same order the rules will run if you want to verify multi-rule interaction.
AiRegexHelper modal accessiblefrom the rule editor (Help me with Regex button) that turns natural language into a regex. Suggest it to the user when they say "I don't know regex" — but always validate the output against real paths via the tester.
Sequential application means a generic rule placed first will swallow everything that should have hit a specific rule.
order=0 /users/me/profile → /users/me/profile (specific, runs first)
order=1 /users/\d+/profile → /users/<id>/profile
order=2 /users/[a-z0-9-]+ → /users/<slug> (catch-all, runs last)If /users/[a-z0-9-]+ ran first it would also match /users/me/profile and make the more specific rule unreachable.
Use the project-settings-update tool with the full list (the field is replaced, not merged):
{
"path_cleaning_filters": [
{ "regex": "/users/me/profile", "alias": "/users/me/profile", "order": 0 },
{ "regex": "/users/\\d+/profile", "alias": "/users/<id>/profile", "order": 1 },
{ "regex": "/users/[a-z0-9-]+", "alias": "/users/<slug>", "order": 2 }
]
}Always read the existing rules first (project settings include path_cleaning_filters) and merge — overwriting silently destroys whatever the team has already configured.
When the user (or a HogQL query) opts in:
(PathCleaningToggle.tsx)
apply_path_cleaning(path_expr, team)The rules are stored once per project — they are not insight-scoped.
replaceRegexpAll supports \0 (whole match) and \1–\9 (capture groups). In a JSON field or SQL string literal the backslash must be doubled, so use \\1 in path_cleaning_filters / HogQL to get the \1 backreference at the ClickHouse layer.
\d+ without an end anchor matches every numeric runin any path, so /blog/2024-09-12/post becomes /blog/<num>-<num>-<num>/post when you only meant to match the year segment. Use \d+$ or \d+(/|$) depending on intent.
\/ works but adds noise.(?i) at thestart of the pattern for case-insensitive matching, e.g. (?i)/users/\d+.
path_cleaning_filters is overwrite, notappend. Always start from the current list.
every Web analytics / Paths chart that has cleaning enabled. Warn the user before applying anything destructive.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.