Google audit counts ad-group negative keywords as enabled keywords, producing false "self-blocked keywords" and "keyword bloat" findings
Summary
The /ads google audit reported that 133 enabled keywords "cannot serve" because they are blocked by the account's own negatives, and that 185 of 261 enabled keywords had zero impressions in 90 days. Both findings are artefacts of the keyword inventory including ad-group negative keywords (ad_group_criterion.negative = TRUE) as if they were positive keywords. The account has 129 enabled positive keywords, zero of them mirrored by a negative in the same ad group, and one keyword text with a genuine (and intended) negative conflict.
Environment
- claude-ads at
ac21644(2026-09-11), installed withinstall.shinto Claude Code, run headless withclaude -p "Run /ads google."and the officialgoogle-ads-mcpserver. - Account under audit: three enabled Search campaigns, seven enabled ad groups, nine shared negative lists attached to one campaign. Details available privately on request.
What the report said
Finding H-4 (severity high, "confidence: high"): "133 enabled keywords cannot serve — blocked by the account's own negatives in the same ad group/campaign. 134 matched, 133 with zero impressions in 90 days. Cross-check of 261 live enabled keywords against 209 ad-group and 168 campaign negatives." The evidence section lists the "self-blocked" terms per ad group (93, 30, 5, 5, 1) and states that each "exists simultaneously as an enabled positive keyword and as a phrase negative in the same ad group", giving single-word examples such as pdf, checkliste, training, auditor. Recommendation 8 in the action plan: "export and remove the inert positives".
Finding M-7: "185 of 261 enabled keywords in live ad groups (70.9%) had zero impressions in 90 days." Finding M-6 also states one ad group "holds 130 enabled keywords".
What the account actually contains
Pulled directly from the Google Ads API (v30 client) with ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.status = 'ENABLED' AND ad_group.status = 'ENABLED' AND campaign.status = 'ENABLED':
| Set | Count |
|---|---|
Positive keywords (negative = FALSE) |
129 |
Ad-group negative keywords (negative = TRUE) |
132 |
Positives with zero impressions in 90 days (keyword_view) |
53 |
| Ad-group negatives whose text is also a positive in the same ad group | 0 |
| Genuine conflicts (positive blocked by an in-scope negative, match-type aware) | 2 rows, one keyword text |
The arithmetic lines up exactly with the report: 129 positives + 132 negatives = 261 "enabled keywords"; 132 negatives (which never receive impressions) + 53 real zero-impression positives = 185. The per-ad-group "self-blocked" counts (93, 30, 5, 5, 1) are the sizes of those ad groups' negative lists, and the example words in the report exist in the account only as negatives. The ad group described as holding 130 keywords has 39 positives and 91 negatives.
Why it happens
Nothing in the skill instructs the model to filter on ad_group_criterion.negative. ads/references/google-audit.md lists G-KW1 ("Zero-impression keywords") and G-KW2 only as "Conditional evidence control: establish applicability and evaluate from current account evidence", and the G-KW1 entry in control-plane/manifests/control-registry.json carries no evidence recipe. A natural inventory query against ad_group_criterion with status = 'ENABLED' therefore returns negatives alongside positives, and the "self-blocked" cross-check matches every ad-group negative against itself. The exact GAQL the run used is not written to the log, so this is inferred from the counts, but the counts leave no other explanation.
Impact
The report presents a high-severity, high-confidence finding whose recommended fix ("remove the 133 inert positives") would delete the account's ad-group negative keywords. The same inventory error inflates M-6 and M-7, so three findings and one action-plan item are wrong on an otherwise careful report.
Suggested fix
- Give G-KW1, G-KW2 and the negative-conflict check an explicit evidence recipe: keyword inventory from
ad_group_criterionmust includead_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE; negatives come from the same resource withnegative = TRUE, fromcampaign_criterion, and fromshared_criterionjoined throughcampaign_shared_set. - Make the conflict check match-type aware and scoped: a broad negative blocks a positive when all its words appear, a phrase negative when its words appear contiguously in order, an exact negative only on identical text, and only negatives from the same ad group, the same campaign, or a shared list attached to that campaign apply.
- Add a plausibility guard: if the number of "self-blocked" keywords equals the number of ad-group negatives in scope, or if the inventory count exceeds the
keyword_viewrow count (which never contains negatives), the finding should be downgraded tounknownrather than reported as high confidence.
Minimal inventory query that avoids the problem:
SELECT campaign.id, ad_group.id, ad_group_criterion.criterion_id,
ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type
FROM ad_group_criterion
WHERE ad_group_criterion.type = 'KEYWORD'
AND ad_group_criterion.negative = FALSE
AND ad_group_criterion.status = 'ENABLED'
AND ad_group.status = 'ENABLED'
AND campaign.status = 'ENABLED'Happy to share the verification script or an anonymised extract of the report if that helps.
Source: AgriciDaniel/claude-ads