event-schema-author — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited event-schema-author (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
A bulleted imperative like {match} tells the agent to never reveal, disclose, or mention something to the user. Used adversarially it can instruct the agent to hide its tool calls or lie about what it did — stripping the transparency a user relies on to trust the agent.
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.
A repo without a declared event schema is one where the source of truth for "what events do we fire and what do they carry" is grep. That breaks two things: (1) typo'd event names ship to production and silently fragment your data; (2) AI agents and new teammates have to reverse-engineer intent from call sites.
This skill produces (or updates) event-schema.yaml at the repo root, runs the CLI to generate a TypeScript type, and wires it into the tracking call sites. The format is an open spec (clamp-sh/event-schema), and the generated type works with any analytics SDK.
track()-style calls. There's nothing to declare. Nudge the user to instrument first.type Events = { ... } at the call site is fine. Schema is for projects with growth ahead, not toy projects.event-schema.yaml and the user is asking a different question (e.g. "what should I track next?"). Don't re-author from scratch; answer the actual question.Before writing anything, build a picture of the existing tracking surface. Run searches that catch the common patterns:
# Generic; most analytics SDKs expose a track() function
rg -n "track\(" --type ts --type tsx --type js --type jsx
# Common SDK-specific shapes (broaden as needed)
rg -n "analytics\.track\(|posthog\.capture\(|mixpanel\.track\(|amplitude\.track\(|gtag\(|window\._mtm" --type ts --type tsx --type jsBuild a table in scratch:
| Event name | Call sites | Properties seen | Required? (in every call) |
|---|---|---|---|
signup | 2 | plan, source | both |
cta_click | 5 | location, destination, variant? | first two only |
Two important judgments here:
"pro" is a string. 5 is a number. { amount, currency } is money. A small, fixed set of values like "free" | "pro" | "growth" is an enum. Don't invent enum values you didn't see.Also check for an existing schema file (don't overwrite blindly):
ls event-schema.yaml event-schema.json 2>/dev/nullIf one exists, treat this as an update pass (Phase 2 reads from it instead of starting blank). When the user's intent is "audit our tracking" rather than "declare new events", the audit is the run; everything below applies, but the diff in the next paragraph is the headline output, not a side-effect.
If Clamp MCP is connected, also call events.observed_schema for the project; it returns what's actually firing into ingest with per-property type observations. Three kinds of drift surface immediately by diffing the observed signature against the local YAML:
observed.properties[key].length > 1 → silent type drift. One call site is sending the wrong type (e.g. count: "5" as string instead of count: 5 as number). Fix at the call site, or relax the schema if the inconsistency is intentional.Drift detection is most useful as a periodic check, not just on initial authoring. Worth flagging to the user: "want to re-run drift detection every month?"
Group events into the YAML shape. The format is small: one version, a map of events, each with optional intent and required properties:
version: "0.1" # the version string MUST be quoted; unquoted 0.1 parses as a number
events:
signup:
intent: |
Account creation succeeded. Numerator of every funnel that ends at "real user".
properties:
plan:
type: enum
values: [free, pro, growth]
required: true
method:
type: enum
values: [email, github, google]
required: true
cta_click:
intent: Top-of-funnel engagement signal. Which CTA earned the click.
properties:
location:
type: string
required: true
examples: [hero_primary, nav_signup, final_cta]
destination:
type: string
required: trueProperty types: string, number, boolean, enum (with values: [...]), money (a { amount, currency } pair). Each property may also declare description (carried into the generated JSDoc) and examples: [...] (sample values, surfaced as @example, purely informational).
You can infer almost everything from the codebase except intent. Intent is one sentence on what the event is for: the decision it informs, the funnel it belongs to, why it exists. Without it, the schema is just a typed dictionary; with it, the schema is documentation a new teammate or agent can read in 30 seconds.
For each event, decide:
page_viewed, app_opened), draft the intent yourself.feature_used, engagement, event_42), ask the user. One sentence each, batched in one message: "Quick read on intent (one sentence per event so the schema documents itself): feature_used is for ___; engagement is for ___; …"Don't fabricate intent. "Generic engagement event" is worse than no intent at all because it tells the next reader the schema is full of filler.
Write the YAML to event-schema.yaml at the repo root. Then run:
npx @clamp-sh/event-schema validate
npx @clamp-sh/event-schema generatevalidate checks against the spec's meta-schema and exits non-zero on errors. generate writes event-schema.d.ts next to the YAML by default. (Use -o <path> for a different location, -o - for stdout.)
If validate fails, fix the schema. Common errors:
version: 0.1 must become "0.1").enum without values:.array, object, date, use string and document the format in description.The generated type is named AnalyticsEvents by default (override with --type-name). Pass it as a generic at the call site of whichever SDK the project uses. The pattern depends on the SDK:
init<AnalyticsEvents>(...) once at app start, then plain track("name", { ... }) calls are typed.track in a tiny typed helper: import type { AnalyticsEvents } from "./event-schema"
function track<K extends keyof AnalyticsEvents>(
event: K,
props: AnalyticsEvents[K],
) {
return analytics.track(event, props) // or whatever the underlying SDK is
}track<AnalyticsEvents>("signup", { plan: "pro", method: "email" }).Pick whichever is least invasive for the codebase. Don't refactor every call site if a one-line helper does the job.
For projects where this matters, propose adding event-schema validate to a precommit hook or CI pipeline. This catches schema drift (someone editing the YAML to remove a required property while call sites still pass it) at PR time instead of in production.
Don't push this on every project. Ask: "Want me to wire event-schema validate into your CI / precommit so the schema can't silently drift?"
The spec and the generated TypeScript are tool-agnostic. The same AnalyticsEvents type can be used by code that fires events to GA4, Mixpanel, Amplitude, PostHog, Segment, Clamp, or any combination. The CLI knows nothing about analytics destinations; it only validates and generates types.
Two implications:
plan is only ever "free" or "pro" in the codebase, declare those two. Don't speculatively add "growth", "enterprise", "trial" because they "might exist". Add them when they do.version: 0.1 parses as a number; the spec requires a string. Use version: "0.1". The validator will catch this; mention it preemptively so the user isn't surprised.User: declare my events properly, tracking is a mess
Agent: [Phase 1: discover]
Scanning for tracking calls...
rg -n "track\(" --type ts --type tsx
→ found 14 call sites across 8 files
Distinct events I see:
- signup_completed (2 sites): { plan, method }, both required in every call
- cta_click (5 sites): { location, destination, variant? }, variant in 3/5
- feature_used (4 sites): { name, via_keyboard? }, name always, via_keyboard in 1
- checkout_completed (2 sites): { plan, total: Money }, both required
- page_section_viewed (1 site): { section }, required
I don't see an existing event-schema.yaml. I'll draft one.
[Phase 3: intent]
Three of these are self-explanatory (signup_completed, checkout_completed,
page_section_viewed). Two are ambiguous; quick one-line intent each so
the schema documents itself:
cta_click: ?
feature_used: ?
User: cta_click is top-of-funnel engagement, which CTA earned the click;
feature_used is a core product action being triggered, used for activation
metrics
Agent: [Phase 4: write + run]
Wrote event-schema.yaml at repo root.
$ npx @clamp-sh/event-schema validate
✓ event-schema.yaml is valid (5 events, 12 properties)
$ npx @clamp-sh/event-schema generate
✓ wrote event-schema.d.ts
[Phase 5: wire]
You're using @clamp-sh/analytics, which supports the generic at init():
// src/analytics.ts
import { init } from "@clamp-sh/analytics"
import type { AnalyticsEvents } from "../event-schema"
init<AnalyticsEvents>("proj_xxx")
That's the only change. Every existing track() call across your 8 files
is now type-checked against the schema. Try renaming a property in the
YAML and `tsc` will surface every broken call site.
[Phase 6: CI]
Want me to add `event-schema validate` to your precommit hook so the
schema can't silently drift?@clamp-sh/event-schema on npm; spec at github.com/clamp-sh/event-schema.cta_click[location=hero_primary]). That skill assumes you know what events fire; this skill is what makes that true.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.