Klaro ships no Google Consent Mode integration — you wire it yourself. Set gtag('consent','default', …) to denied before Klaro loads, then push an update from a Klaro watcher. Watch both the consents and saveConsents events, or returning visitors never get their stored consent restored to Google.
Our Klaro overview covers whether self-hosting is the right call. This post covers the part that actually breaks in production: making a self-hosted Klaro speak Google Consent Mode v2 correctly. Everything below is against Klaro 0.7.22.
Step 1: default to denied, before anything else
Consent Mode only works if Google’s tags see a denied state before they fire. That means this block goes in the <head>, above your GTM container and above Klaro:
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
analytics_storage: 'denied',
functionality_storage: 'denied',
personalization_storage:'denied',
security_storage: 'granted',
wait_for_update: 500
});
</script>
The ordering rules here are unforgiving and they are the single most common cause of a “compliant” setup that still leaks. We broke down the execution order in detail in default vs update: timing rules.
Step 2: model Consent Mode signals as Klaro services
Klaro thinks in services; Consent Mode thinks in signals. Don’t try to make one service per signal — map real vendors to services, then derive the signals:
services: [
{
name: 'google-analytics',
title: 'Google Analytics',
purposes: ['analytics'],
cookies: [/^_ga/, '_gid'],
},
{
name: 'google-ads',
title: 'Google Ads',
purposes: ['advertising'],
cookies: [/^_gcl/, { pattern: 'IDE', domain: '.doubleclick.net' }],
},
]
Step 3: the watcher (and the bug almost everyone ships)
Klaro’s consent manager exposes watch(), which takes an object with an update(manager, eventName, data) method. The trap: Klaro emits two different events and most guides only handle one.
consents— fires on every individual toggle, and, critically, whenloadConsents()restores a stored decision on page load.saveConsents— fires only when the visitor confirms, carrying{changes, consents, type}.
Handle only saveConsents and returning visitors get nothing: their consent is in the cookie, Klaro restores it silently, and Google never hears about it — so analytics stays denied for people who already said yes. Handle only consents and you fire an update on every checkbox flip. Handle both, and de-duplicate:
var lastPushed = null;
function syncConsentMode(consents) {
var analytics = consents['google-analytics'] ? 'granted' : 'denied';
var ads = consents['google-ads'] ? 'granted' : 'denied';
var payload = {
analytics_storage: analytics,
ad_storage: ads,
ad_user_data: ads,
ad_personalization: ads
};
var fingerprint = JSON.stringify(payload);
if (fingerprint === lastPushed) return; // nothing changed
lastPushed = fingerprint;
gtag('consent', 'update', payload);
}
klaro.getManager().watch({
update: function (manager, eventName) {
if (eventName === 'consents' || eventName === 'saveConsents') {
syncConsentMode(manager.consents);
}
}
});
Note that ad_user_data and ad_personalization are the two v2 signals — omit them and Google treats the setup as v1, which is what silently degrades remarketing audiences.
Step 4: blocking is separate from signalling
This is the conceptual split people miss. Consent Mode tells Google’s tags how to behave. It does not stop anything from loading. Non-Google scripts — Meta Pixel, HubSpot, TikTok — need real blocking, which in Klaro means rewriting their tags:
<script type="text/plain" data-type="text/javascript"
data-name="meta-pixel">
/* pixel code */
</script>
A site that sets Consent Mode perfectly and forgets this still fires trackers before consent. That is exactly the gap described in why most cookie audits fail.
Step 5: verify against a real browser
Klaro stores consent in a first-party cookie (klaro by default, 120 days), so testing in a tab that already consented tells you nothing. Test in a fresh profile, and check three things: no _ga/_gcl cookies before the banner is answered, a consent → default entry in the dataLayer before any tag, and an update after clicking accept.
Then confirm it from outside your own browser. CookieInspector’s Consent Mode checker loads the page as a first-time visitor and reports the actual default/update sequence and everything that fired in between — including the returning-visitor case, which is the one this integration usually gets wrong.
Klaro gives you a free, BSD-3-licensed CMP you fully own. It also gives you the job of wiring the signals yourself — do it in this order and it holds up.