# Repo and export signals

What to read before asking anything, and how to record it. Every item here is read-only. Strings and menu paths were checked against vendor docs on 2026-10-17; confirm them live before quoting them to the user.

## Recording rule

Every fact goes into the inventory worksheet with:

- `value`: what was found, with public identifiers only.
- `provenance`: `observed` (file path and line, export file name and date, or tool call), `inferred` (the reasoning), or `stated` (role and date).
- `confidence`: `confirmed` only after a person agrees or two independent sources match.

Keep observation and inference apart. A measurement ID read from a container export is `observed`. That it belongs to the production property is `inferred` until someone confirms it. Record them as two facts.

An inferred fact can be drafted into a file only with an `(inferred, confirm)` marker. Other skills must treat it as unconfirmed.

## Files never to open

- `.env`, `.env.local`, `.env.production`, any credentials file, service account JSON, `*.pem`, `*.key`, keychains, CI secret stores.
- Read `.env.example` or `.env.sample` for variable names only (`cut -d= -f1 .env.example`). A variable name such as `GA4_API_SECRET` tells you a Measurement Protocol sender exists; the value is never needed.

## How to scan without printing secrets

A plain `grep -r` reads `.env` files and prints matched lines, values included. Use one of these, which skip the never-open files and print path and line number only. Tested on 2026-10-17 with git 2.50 and ripgrep.

```bash
# git grep searches tracked files only; the pathspecs exclude secret files that were committed anyway
X=(-- . ':!*.env' ':!*.env.*' ':!*.pem' ':!*.key' ':!*credentials*' ':!*service-account*')
git grep -n -I -E 'googletagmanager\.com|mp/collect|GTM-' "${X[@]}" | cut -d: -f1,2

# ripgrep alternative (respects .gitignore and skips hidden files by default)
rg -n -g '!*.env' -g '!*.env.*' -g '!*.pem' -g '!*.key' -g '!*credentials*' -g '!*service-account*' -e 'googletagmanager\.com' -e 'mp/collect' . | cut -d: -f1,2

# Secret variable names only: -o prints the name, never the value after it
git grep -n -I -o -E '[A-Z0-9_]*(SECRET|TOKEN|KEY)[A-Z0-9_]*' "${X[@]}"
```

- Open a matched file only to read the lines around a tag or SDK call, never a line that assigns a credential.
- If a match comes from a file on the never-open list (a committed `.env`, a key file), stop. Report the path only, and tell the user its owner should rotate whatever it holds.

## Repo search patterns

Search source, templates, layouts and package manifests. A match is evidence that a tool is installed somewhere, not that it is live in production: confirm the environment.

Collection:

- Google Tag Manager web container: `googletagmanager.com/gtm.js`, `ns.html?id=`, `GTM-`. The head block pushes `gtm.start` into `dataLayer`.
- Data layer contract: `dataLayer.push(` calls, and the event names they push.
- Google tag: `googletagmanager.com/gtag/js`, `gtag('config'`. Tag IDs start `GT-` (every newly created Google tag) or the legacy prefixes `G-` (GA4), `AW-` (Google Ads) and `DC-` (Floodlight). One Google tag can carry several IDs (for example `AW-` and `GT-`), and a `GT-` tag can route to several destinations. Map a `GT-` ID to its destinations in the Google tag settings, or ask the owner, before deciding which destinations have a second sender.
- GA4 Measurement Protocol sender: `google-analytics.com/mp/collect` or `region1.google-analytics.com/mp/collect` in server code. Its `api_secret` is private by Google's own guidance; record where it is stored, never its value.
- Segment: `@segment/analytics-next` in `package.json`, or `cdn.segment.com/analytics.js/v1/`.
- RudderStack: `@rudderstack/analytics-js`.
- Mixpanel: `mixpanel-browser` (the server package is `mixpanel`), `mixpanel.init(`, `cdn.mxpnl.com`.
- Amplitude: `@amplitude/analytics-browser` or `@amplitude/unified`, `amplitude.init(`.
- PostHog: `posthog-js`, `posthog.init(`, an `api_host` such as `us.i.posthog.com`.
- HubSpot tracking code: the docs show `js.hs-scripts.com`; search for `hs-scripts.com` on any subdomain so an installed snippet on another loader host is not missed. Also the element ID `hs-script-loader` and the `_hsq` queue.
- Meta Pixel: `connect.facebook.net/en_US/fbevents.js`, `fbq('init'`.
- LinkedIn Insight Tag: `snap.licdn.com`, variables starting `_linkedin_`.
- TikTok pixel, Microsoft Advertising UET and other ad tags: take the script host from the base code in the vendor's current tag setup page before searching. Do not conclude a tag is absent from a guessed host.
- Lifecycle and messaging SDKs: Braze web SDK `@braze/web-sdk`; Klaviyo onsite script `static.klaviyo.com/onsite/js/` (the path segment is the public site key, safe to record).

Consent:

- Google consent mode: `gtag('consent', 'default'` in code; in a GTM export, a CMP template or tags calling `setDefaultConsentState`. Basic or advanced mode is not visible from the snippet alone; confirm with the privacy owner.
- OneTrust: `otSDKStub.js` (also self-hosted on the site's own domain).
- Cookiebot: `consent.cookiebot.com/uc.js`; the `data-consentmode` attribute set to `disabled` turns off Google consent mode.
- Usercentrics: `web.cmp.usercentrics.eu/ui/loader.js`, script ID `usercentrics-cmp`.
- When any of these is present, round 3's CMP and consent mode questions become confirmation questions ("We found Cookiebot with consent mode enabled. Is the mode advanced in the EU?").

Routing:

- Server-side tagging: `server_container_url` in a `gtag('config'` call, or a Google tag configuration variable with that parameter in a GTM export. The value is the server container host; record it in `stack.md`.
- Tag scripts or collection requests on the site's own domain, a subdomain or a first-party path instead of `googletagmanager.com`: a server container or Google tag gateway. Record as inferred until the owner confirms.
- Google tag gateway can be switched on at the CDN (for example Cloudflare) without any change to the snippet in the repo. No first-party host in code does not rule a gateway out: ask the owner.

Mobile apps (Android Gradle files; for iOS, search the Podfile, `Package.swift` or `Package.resolved` for the vendor name):

- AppsFlyer: `com.appsflyer:af-android-sdk`.
- Adjust: `com.adjust.sdk:adjust-android`.
- Branch: `io.branch.sdk.android:library`.
- Firebase Analytics: `com.google.firebase:firebase-analytics`.
- Record the SDK version; mobile-attribution-fundamentals and braze-implementation-qa read SDK versions from `stack.md`.

Storage and modelling:

- dbt: `dbt_project.yml`; semantic models and metrics in model YAML. Current dbt docs put a `semantic_model` block inside each model entry; older projects and tutorials use a different layout, so check the installed dbt version before reading them. Record mart model names, sources and any `contract:` blocks. A branch or a local compile is not production: see the production artifacts export below.
- Snowflake semantic views: `CREATE SEMANTIC VIEW` in SQL or migration files, `SYSTEM$CREATE_SEMANTIC_VIEW_FROM_YAML` calls, and the semantic model YAML files they read. Views created in Snowsight leave no repo footprint, so `SHOW SEMANTIC VIEWS` under a read role is the authoritative list.
- Looker: `*.lkml` files; model files (`*.model.lkml`) hold `explore:` blocks, view files (`*.view.lkml`) hold `dimension:` and `measure:` blocks.

Existing context:

- `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`, a `context/` folder, `docs/**/kpi*`, `docs/**/metric*`, glossary pages, dbt `docs` blocks. What to do with each: `references/existing-context.md`.

## Exports the user can produce

Ask for the export, not for access. Each export below is a definition or configuration file, not customer data.

- GTM: Admin, then Container, then Export Container. Choose the latest published version, not a workspace, when the question is what is live; a workspace export shows unpublished work. The file is JSON. A GA4 Configuration tag in an older container now appears as a Google tag (Google upgraded them automatically), so look for a Google tag with the `G-` or `GT-` ID, not a "GA4 configuration" tag.
- GTM through the API, if a read-only connection exists: `accounts.containers.versions.live` returns the published version and accepts the `tagmanager.readonly` scope.
- HubSpot properties: settings icon, then Properties, then "Export all properties". Needs the Export permission. Columns include name, internal name, type, description, group, options, creator, usages and fill rate. Low fill rate and zero usages are candidates for "deprecated" in the dictionary.
- Mixpanel Lexicon: Data, then Lexicon, then Export. Owners, admins and analysts can export. The CSV arrives by email; the link lasts 14 days.
- Segment Protocols: Protocols, then the tracking plan row menu, then Download Tracking Plan; collect the CSV from Download History.
- Google Ads conversion actions: a list of each action with its goal and whether it is primary or secondary. Primary actions are reported in the Conversions column and used for bidding; secondary actions are reported in All conversions and are not used for bidding, except inside a custom goal. This fills the "Ad platform conversion actions" block in `metrics.md`.
- Meta: the dataset (pixel) ID and whether it receives events from the browser pixel, the Conversions API or both. Read it in Events Manager; verify the menu path live.
- dbt production artifacts: `manifest.json` and `semantic_manifest.json` from the last successful production job run (the run's artifacts in the dbt platform). `semantic_manifest.json` lists semantic models and metrics. If the dbt MCP Admin API tools are connected, `list_job_run_artifacts` and `get_job_run_artifacts` read them.
- UTM and campaign values: an export of source, medium and campaign values from the analytics tool for the last 90 days, with sessions or clicks per value, plus campaign names from each ad platform. These give the observed vocabulary and the traffic weight to compare with stated rules.

## Read-only tool calls, when a connection already exists

Do not connect a new MCP server as part of this skill. If one is already connected, call only read tools. Review any new connection first (publisher, scopes, write tools).

Servers that expose only read tools:

- GA4 (Google's `google-analytics-mcp`, labelled Experimental, local, `analytics.readonly` scope): `get_account_summaries`, `get_property_details`, `get_custom_dimensions_and_metrics`, `list_google_ads_links`. The Admin API Property resource carries `timeZone` (changes apply going forward only), `currencyCode` and `serviceLevel` (`GOOGLE_ANALYTICS_STANDARD` or `GOOGLE_ANALYTICS_360`).
- Google Ads (Google's MCP server; its developer page says the implementation is strictly read-only): `list_accessible_customers`, `search`, `get_resource_metadata`. With the default configuration, clients show them as `customers_list_accessible_customers`, `search_search` and `metadata_get_resource_metadata`.
- These two servers expose only read tools, but the credential they use is not read-only: the Google Ads server needs the full `adwords` scope, and both setup guides also request `cloud-platform`. Store and scope that credential as a write-capable one.

Servers with a read-only mode or permission:

- PostHog MCP: add `?readonly=true` to the URL or send the header `x-posthog-read-only: true` (`1` also works). The header wins if both are set.
- Amplitude MCP: read and write are separate per-project permissions ("Use MCP (read)" and "Use MCP (write)"). Use a role with read only.
- BigQuery (Google-managed remote MCP): use `execute_sql_readonly`, `list_dataset_ids`, `list_table_ids`, `get_table_info`. `execute_sql` can write; an IAM deny policy can block it.
- dbt MCP: not read-only. The Admin API tools `trigger_job_run`, `retry_job_run` and `cancel_job_run` run on local and remote servers; the CLI tools `build`, `run` and `test` can modify warehouse objects; `execute_sql` does not block writes. Disable the groups (`DISABLE_DBT_CLI=true`, `DISABLE_ADMIN_API=true`, keep `DISABLE_SQL=true`, per the dbt MCP environment variables reference) or call only the Semantic Layer tools (`list_metrics`, `get_dimensions`, `get_entities`) and Discovery tools (`get_all_models`, `get_mart_models`, `get_all_sources`, `get_lineage`). Do not call `query_metrics` or SQL tools for inventory; definitions are enough.
- Snowflake: `SHOW SEMANTIC VIEWS` and `DESCRIBE SEMANTIC VIEW` list and describe semantic views; run them under a read role.

Servers with write tools and no documented read-only mode (checked 2026-10-17):

- RudderStack MCP: reads tracking plans, violations, the Data Catalog, sources and destinations. Write tools can switch the active workspace, create or update transformations and audiences, and connect them to destinations; with audience activation on, they can create destinations and Reverse ETL syncs and trigger syncs. Sensitive execution tools run transformation code that can make outbound requests and submit temporary warehouse work. Write tools do not delete sources or destinations. Call read tools only.
- Mixpanel MCP: write tools include Lexicon edits and `Update-Business-Context`. Prefer the Lexicon CSV export; if the MCP is used, call read tools only.
- HubSpot remote MCP: acts as the connected user and respects their permissions. Its write tools cover CRM records and activities, landing pages, website pages and blog posts (including publishing), campaigns, marketing email drafts, pipelines, custom properties and segments. For inventory, prefer the property export; if the MCP is already connected, `get_properties` returns full property definitions with types and enumeration values, and `search_properties` finds properties by keyword.
- GTM: community and third-party GTM MCP servers can create and publish changes. Use the container export, or the API's `versions.live` under the read-only scope.

## Turning signals into stack entries

- One entry per tool per environment when environments differ (a staging GA4 property is its own entry under the same tool).
- Two collection paths for one destination (gtag in code and a Google tag in GTM, or a CDP route plus a direct tag) are recorded as two senders. Flag it: double counting is the usual result.
- A tool found in code with no owner is an open item, not an orphan to delete.
- A tool the user names that has no footprint anywhere is `stated`; ask for one piece of evidence (an export, a screenshot of the account header with IDs, a contract) before marking it confirmed.
