Skip to content
Back to skills

Utm Lint

ASecurity

Lint a real corpus of tagged URLs against the way analytics platforms actually read them. Use when campaign reporting looks fragmented, when traffic is landing in Unassigned, before rolling out a UTM taxonomy, or when auditing an account someone else tagged. Finds case drift, near-duplicate campaign names, mediums that no channel grouping recognises, self-referrals, auto-tagging conflicts and personal data in campaign parameters. Runs on a CSV of URLs, needs no API access.

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 3, 2026
ai-agentspythonrustgobashgitapi

Works with

  • claude code
  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 3, 2026

npx -y skills add m7mdwb/martech-verify --skill utm-lint --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Utm Lint?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Utm Lint
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/m7mdwb-utm-lint/badge)](https://www.skillsdirectory.com/skills/m7mdwb-utm-lint)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: utm-lint
description: Lint a real corpus of tagged URLs against the way analytics platforms actually read them. Use when campaign reporting looks fragmented, when traffic is landing in Unassigned, before rolling out a UTM taxonomy, or when auditing an account someone else tagged. Finds case drift, near-duplicate campaign names, mediums that no channel grouping recognises, self-referrals, auto-tagging conflicts and personal data in campaign parameters. Runs on a CSV of URLs, needs no API access.
---

# utm-lint

## Agent workflow

This is an executable audit, not a taxonomy-writing exercise. When a usable URL export is
available, run the bundled linter instead of responding with generic UTM advice.

1. Identify the export containing full destination or landing-page URLs. If none is
   available, ask for that one file and explain that CSV, JSON Lines, a log, or one URL per
   line all work.
2. Resolve `scripts/lint.py` relative to this `SKILL.md` and run it with an available
   Python 3 interpreter. In Claude Code, `${CLAUDE_SKILL_DIR}` is this skill directory;
   in other hosts use the skill path supplied by the host. Quote every path.
3. Pass `--site DOMAIN` when the user's own domain is known. If it is not known, do not
   block the rest of the audit; state that self-referrals were not evaluated. Let column
   auto-detection run before adding `--column NAME`.
4. Interpret exit codes exactly: `0` means no configured rule fired on the supplied URLs,
   `1` means findings were produced, and `2` means the input could not support a trustworthy
   audit. On `2`, relay the actionable error and never describe the file as clean.
5. Group the result into privacy/attribution risks, taxonomy fragmentation, and mechanical
   cleanup. Preserve the tool's `should be:` values and distinguish facts found in the file
   from recommendations.

Never rewrite the user's source export or claim that historical analytics data was fixed.
If the user needs help preparing an export, read [references/input-guide.md](references/input-guide.md).

## When to use this

- "Why is one campaign showing up as three rows?"
- "Why is so much of our traffic in Unassigned?"
- "We're rolling out a new UTM convention" — measure the drift before writing the rules.
- Inheriting an account: this is the fastest read on how disciplined the previous team was.
- After an email send, a paid launch, or any hand-off to an agency.

Every marketing skill collection publishes UTM *advice* — naming conventions, a builder, a
taxonomy template. None of them will look at the URLs you already have.

## How to run it

```bash
python skills/utm-lint/scripts/lint.py <export> [--site yourdomain.com] [--column NAME] [--json] [--fail-on critical|high|medium]
```

Pass `--site` whenever you can. It is what turns "someone tagged a link with our own
domain" from invisible into a finding, and internal tagging is the single most expensive
mistake in this list because it reassigns conversions away from the channel that earned
them.

Where to get the corpus: GA4 Reports > Acquisition > Traffic acquisition with page location,
a BigQuery query over `page_location`, a server access log, your email platform's link
export, or the ad platforms' final URLs.

## The checks, and why each one costs money

| Rule | What it costs you |
|---|---|
| `case_drift` | `LinkedIn` and `linkedin` are two rows. Every campaign total is understated. |
| `near_duplicate` | `q3-demand`, `q3_demand`, `Q3 Demand` — one campaign, three lines, no total. |
| `unmapped_medium` | A medium no channel definition recognises lands in **Unassigned**, so the spend disappears from every channel report. `newsletter` is the classic: it feels like a medium and is not one. |
| `missing_required` | A campaign with no source or medium cannot be grouped with the rest of itself. |
| `whitespace` | A leading space produces a value that looks identical in the report and is not. |
| `duplicate_param` | The same parameter twice: which one wins is platform-dependent, so the answer is unreliable. |
| `self_referral` | Your own domain as `utm_source` on an internal link restarts the session and steals the conversion from the channel that actually earned it. |
| `autotagging_conflict` | A `gclid` beside manual tags. With auto-tagging on the click id wins, so the taxonomy in your reports is not the one you designed. |
| `source_equals_medium` | Usually one field filled in by mistake. |
| `empty_param` | Present-but-empty is not the same as absent; it can create a blank dimension value. |
| `pii_in_utm` | An email address in `utm_content`, which email tools produce when a merge field ends up in the link. A privacy problem before it is a tagging one. |

## Reading the output

Findings are grouped and counted, and anything fixable carries a `should be:` line with the
corrected value. For `case_drift` and `near_duplicate` the suggestion is the **most common
form already in your data**, lower-cased — the taxonomy you actually have rather than one
invented for you.

Nothing correct is reported. Clean tagging produces silence, which is the property that
makes a linter worth running more than once.

## What to tell the user afterwards

Fix in this order, because later steps are wasted without earlier ones:

1. **Stop the source of new bad tags.** A link builder, a template, an agency brief. Drift
   comes from people typing links by hand.
2. **Decide the canonical value for each cluster** the linter found, and apply it going
   forward. History cannot be rewritten in the platform — but if warehouse or BigQuery data
   is available, a mapping table applied at query time recovers the historical totals.
3. **Turn off manual tagging on Google Ads links** if auto-tagging is on, rather than
   maintaining both.
4. **Remove the tags from internal links** entirely. Internal campaign tagging should be
   replaced by a separate internal-promotion parameter that does not reset attribution.

## What it will not do

- It does not fix your URLs. It tells you what each should be and leaves the edit to you.
- It does not know your taxonomy. It infers the canonical form from what is most common in
  your own corpus rather than imposing a convention.
- It does not read your analytics account. It reads a file, on purpose.
- Channel classification mirrors the standard default channel definitions. A property with
  custom channel groups may legitimately use mediums flagged here as unmapped.

## Tests

```bash
python tests/test_utm_lint.py
```

Fourteen planted problems in the fixture; the test asserts exactly those are found and
nothing else, that correctly tagged URLs stay silent, and that the email planted in
`utm_content` is never printed in full.

Files in this skill

  • SKILL.md6.6 KB
  • agents/openai.yaml227 B
  • references/input-guide.md1.3 KB
  • scripts/_shared.py12.7 KB
  • scripts/lint.py14.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.