From be94fa241b57603e2c9f9d4c955f982be6a097e1 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 3 Jun 2026 23:30:56 +0200 Subject: [PATCH 01/23] feat(expo): add anonymous usage telemetry and skill feedback Adds an opt-out, anonymous PostHog telemetry layer plus a feedback path so we can see which Expo skills get used and improve them. - Shared, zero-dependency Node/Bun scripts in skills/skill-feedback/scripts (telemetry_common, skill-event, skill-feedback, telemetry toggle), launched via run.sh which picks node or bun and no-ops if neither is present. - Plugin-level Read hook (hooks/hooks.json) emits skill_read; a per-skill PostToolUse hook on all 16 skills emits skill_activated. Both are Claude Code only and scoped to this plugin. - Every skill gains a harness-agnostic "Expo Skill Feedback" footer for manual skill_feedback; a new skill-feedback skill documents the whole system. - Anonymous (hashed local install id; no PII, code, or paths). Opt-out via `telemetry.js --off`, EXPO_SKILLS_TELEMETRY=0, or DO_NOT_TRACK. Fully inert until a PostHog project key is configured. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 31 +++ README.md | 16 ++ plugins/expo/.claude-plugin/plugin.json | 2 +- plugins/expo/hooks/hooks.json | 16 ++ plugins/expo/skills/add-app-clip/SKILL.md | 18 ++ .../expo/skills/building-native-ui/SKILL.md | 18 ++ .../expo/skills/eas-update-insights/SKILL.md | 18 ++ plugins/expo/skills/expo-api-routes/SKILL.md | 18 ++ plugins/expo/skills/expo-brownfield/SKILL.md | 18 ++ .../expo/skills/expo-cicd-workflows/SKILL.md | 18 ++ plugins/expo/skills/expo-deployment/SKILL.md | 18 ++ plugins/expo/skills/expo-dev-client/SKILL.md | 18 ++ plugins/expo/skills/expo-module/SKILL.md | 18 ++ plugins/expo/skills/expo-observe/SKILL.md | 18 ++ .../expo/skills/expo-tailwind-setup/SKILL.md | 18 ++ .../skills/expo-ui-jetpack-compose/SKILL.md | 18 ++ plugins/expo/skills/expo-ui-swift-ui/SKILL.md | 18 ++ .../expo/skills/native-data-fetching/SKILL.md | 18 ++ plugins/expo/skills/skill-feedback/SKILL.md | 137 +++++++++++ .../skill-feedback/scripts/package.json | 6 + .../expo/skills/skill-feedback/scripts/run.sh | 18 ++ .../skill-feedback/scripts/skill-event.js | 224 ++++++++++++++++++ .../skill-feedback/scripts/skill-feedback.js | 144 +++++++++++ .../skill-feedback/scripts/telemetry.js | 57 +++++ .../scripts/telemetry_common.js | 171 +++++++++++++ plugins/expo/skills/upgrading-expo/SKILL.md | 18 ++ plugins/expo/skills/use-dom/SKILL.md | 18 ++ 27 files changed, 1109 insertions(+), 1 deletion(-) create mode 100644 plugins/expo/hooks/hooks.json create mode 100644 plugins/expo/skills/skill-feedback/SKILL.md create mode 100644 plugins/expo/skills/skill-feedback/scripts/package.json create mode 100644 plugins/expo/skills/skill-feedback/scripts/run.sh create mode 100644 plugins/expo/skills/skill-feedback/scripts/skill-event.js create mode 100644 plugins/expo/skills/skill-feedback/scripts/skill-feedback.js create mode 100644 plugins/expo/skills/skill-feedback/scripts/telemetry.js create mode 100644 plugins/expo/skills/skill-feedback/scripts/telemetry_common.js diff --git a/CLAUDE.md b/CLAUDE.md index 1dfa70b..698dcd9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -241,3 +241,34 @@ Users install plugins from this marketplace: - **Author emails**: Use `@expo.io` or `@expo.dev` domain - **License**: MIT for all plugins - **README.md**: Include in each plugin with brief description and license + +## Usage Telemetry & Feedback + +The `expo` plugin ships anonymous usage telemetry + a feedback path. The shared, +zero-dependency Node/Bun scripts live in `plugins/expo/skills/skill-feedback/scripts/` +(`telemetry_common.js`, `skill-event.js`, `skill-feedback.js`, launched via `run.sh`), +and the plugin-level `skill_read` hook is in `plugins/expo/hooks/hooks.json`. See the +`skill-feedback` skill for the full design, privacy model, opt-out, and PostHog config. + +**Every new skill must include two things** (copy from any existing skill): + +1. A `skill_activated` hook in the `SKILL.md` frontmatter: + + ```yaml + hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 + ``` + +2. An `## Expo Skill Feedback` footer at the end of the `SKILL.md` (harness-agnostic; + `--skill `). + +Use the skill's **folder name** for `--skill`. Telemetry is anonymous and opt-out: a +persistent toggle (`telemetry.js --off`, writing `~/.expo-skills/opt-out`) or the +`EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1` env vars. The PostHog *project* key in +`telemetry_common.js` is a public, write-only ingestion key (safe to commit); override +per-env with `EXPO_SKILLS_POSTHOG_KEY`. Never commit a personal API key (`phx_...`). diff --git a/README.md b/README.md index 304cb22..2ebb55f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,22 @@ bunx skills add expo/skills > This will extract the skills individually so you'll need to manually upgrade them. +## Usage telemetry & feedback + +These skills send **anonymous** usage events so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. + +- **Tracked in Claude Code:** when a skill's `SKILL.md` is read (`skill_read`) and when a skill drives its first action (`skill_activated`). +- **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. +- **Other agents (Codex, Cursor, …):** automatic tracking needs Claude Code hooks, so it's off there; feedback still works by running the bundled script. + +Turn it off any time — the simplest way is to ask your agent: **"turn off Expo skills telemetry"** (it runs the bundled toggle, which writes `~/.expo-skills/opt-out` — a persistent switch that works regardless of how the agent was launched). For a global or CI opt-out, set an env var instead: + +```bash +export DO_NOT_TRACK=1 # or: export EXPO_SKILLS_TELEMETRY=0 +``` + +See the `skill-feedback` skill for full details, the event shape, and how to configure the PostHog project key. + ## License MIT diff --git a/plugins/expo/.claude-plugin/plugin.json b/plugins/expo/.claude-plugin/plugin.json index f23f61d..7f217f6 100644 --- a/plugins/expo/.claude-plugin/plugin.json +++ b/plugins/expo/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "expo", - "version": "1.0.0", + "version": "1.1.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo apps", "author": { "name": "Expo Team", diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json new file mode 100644 index 0000000..0bbeb45 --- /dev/null +++ b/plugins/expo/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Read", + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_read --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --agent-harness claude-code --quiet", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index bc502be..6b272e3 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -1,6 +1,13 @@ --- name: add-app-clip description: Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app. +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill add-app-clip --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Add an App Clip to an Expo App @@ -278,3 +285,14 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## References - ./references/native-module.md — Local Expo module to detect App Clip context and present the SKOverlay install prompt + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill add-app-clip --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 9a9df4c..75a810a 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -3,6 +3,13 @@ name: building-native-ui description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs. version: 1.0.1 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill building-native-ui --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Expo UI Guidelines @@ -319,3 +326,14 @@ export default function Layout({ segment }) { ); } ``` + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill building-native-ui --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 840673e..e2857da 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -4,6 +4,13 @@ description: "Check the health of published EAS Updates: crash rates, install/la version: 1.0.0 license: MIT allowed-tools: "Bash(eas *)" +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill eas-update-insights --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # EAS Update Insights @@ -226,3 +233,14 @@ Human-readable group details plus 30 days of launches/failures per platform — - **Fresh publishes** may show zeros for a short period while the metrics pipeline catches up. - **Installs are downloads, not launches**: the `installs` / "Launches" field counts users who downloaded the manifest and launch asset. A confirmed run only registers on the user's *next* update check (typically up to 24h later, depending on the app's update policy). So metrics lag the real-world state slightly. - **Crashes are self-reported**: `failedInstalls` / "Crashes" counts updates that errored during install/launch and were reported on the next update check. Crashes that don't trigger an update request (e.g. process kill before recovery) won't appear. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill eas-update-insights --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 85cb198..efefd52 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -3,6 +3,13 @@ name: expo-api-routes description: Guidelines for creating API routes in Expo Router with EAS Hosting version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-api-routes --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- ## When to Use API Routes @@ -366,3 +373,14 @@ export async function GET(request: Request) { - Keep API routes focused — one responsibility per endpoint - Use TypeScript for type safety - Log errors server-side for debugging + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-api-routes --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index bbd0c62..761971d 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -1,6 +1,13 @@ --- name: expo-brownfield description: Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR/XCFramework, or adding Expo to an existing Kotlin/Swift project. Covers both the isolated approach and the integrated approach. +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-brownfield --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Expo Brownfield @@ -52,3 +59,14 @@ npx create-expo-app@latest my-project --template default@sdk-55 ``` Pin the same Expo SDK across both the RN project and any embedded dependencies. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-brownfield --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 48c8a57..a3db238 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -4,6 +4,13 @@ description: Helps understand and write EAS workflow YAML files for Expo project allowed-tools: "Read,Write,Bash(node:*)" version: 1.0.0 license: MIT License +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-cicd-workflows --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # EAS Workflows Skill @@ -90,3 +97,14 @@ The validator fetches the latest schema and checks the YAML structure. Fix any r ## Answering Questions When users ask about available options (job types, triggers, runner types, etc.), fetch the schema and derive the answer from it rather than relying on potentially outdated information. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-cicd-workflows --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 114aa91..39bfa9e 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -3,6 +3,13 @@ name: expo-deployment description: Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-deployment --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Deployment @@ -188,3 +195,14 @@ eas build:view # View submission status eas submit:list ``` + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-deployment --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index 84a1cf0..f356388 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -3,6 +3,13 @@ name: expo-dev-client description: Build and distribute Expo development clients locally or via TestFlight version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-dev-client --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- Use EAS Build to create development clients for testing native code changes on physical devices. Use this for creating custom Expo Go clients for testing branches of your app. @@ -162,3 +169,14 @@ eas build -p ios --profile development --clear-cache eas --version eas update ``` + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-dev-client --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index dbd2f29..c4f8c73 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -3,6 +3,13 @@ name: expo-module description: Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo. version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-module --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Writing Expo Modules @@ -139,3 +146,14 @@ export function hello(name: string): string { ``` Note: iOS uses just the class name; Android uses the fully-qualified class name (package + class). See `references/module-config.md` for all fields. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-module --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index d08e85d..1c9b149 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -3,6 +3,13 @@ name: expo-observe description: Use for anything related to EAS Observe — adding `expo-observe` to an Expo project (AppMetricsRoot/ObserveRoot HOC, markInteractive, the useObserve hook, and the Expo Router / React Navigation integrations for per-route metrics), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold/warm launch, TTR, TTI, navigation cold/warm TTR, update download, and the TTI frameRate params for triaging slow startups). version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-observe --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # EAS Observe @@ -27,3 +34,14 @@ The three reference files in `./references/` cover the three things people typic - Expo Router integration: https://docs.expo.dev/eas/observe/integrations/expo-router/ - React Navigation integration: https://docs.expo.dev/eas/observe/integrations/react-navigation/ - Configuration: https://docs.expo.dev/eas/observe/configuration/ + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-observe --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index d37fe32..471c9d7 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -3,6 +3,13 @@ name: expo-tailwind-setup description: Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-tailwind-setup --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Tailwind CSS Setup for Expo with react-native-css @@ -478,3 +485,14 @@ Add className to component props: ```tsx type Props = React.ComponentProps & { className?: string }; ``` + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-tailwind-setup --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md index 283f237..019d742 100644 --- a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md +++ b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md @@ -1,6 +1,13 @@ --- name: Expo UI Jetpack Compose description: "`@expo/ui/jetpack-compose` package lets you use Jetpack Compose Views and modifiers in your app." +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-jetpack-compose --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- > The instructions in this skill apply to SDK 55 only. For other SDK versions, refer to the Expo UI Jetpack Compose docs for that version for the most accurate information. @@ -38,3 +45,14 @@ import { fillMaxWidth, paddingAll } from "@expo/ui/jetpack-compose/modifiers"; - **LazyColumn** — Use instead of react-native `ScrollView`/`FlatList` for scrollable lists. Wrap in ``. - **Icon** — Use `` with Android XML vector drawables. To get icons: go to [Material Symbols](https://fonts.google.com/icons), select an icon, choose the Android platform, and download the XML vector drawable. Save these as `.xml` files in your project's `assets/` directory (e.g. `assets/icons/wifi.xml`). Metro bundles `.xml` assets automatically — no metro config changes needed. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-jetpack-compose --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md index ad6accd..811001c 100644 --- a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md @@ -1,6 +1,13 @@ --- name: Expo UI SwiftUI description: "`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app." +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-swift-ui --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- > The instructions in this skill apply to SDK 55 only. For other SDK versions, refer to the Expo UI SwiftUI docs for that version for the most accurate information. @@ -37,3 +44,14 @@ import { Pressable } from "react-native"; ``` - If a required modifier or View is missing in Expo UI, it can be extended via a local Expo module. See: https://docs.expo.dev/guides/expo-ui-swift-ui/extending/index.md. Confirm with the user before extending. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-swift-ui --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index d59cb38..498c68d 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -3,6 +3,13 @@ name: native-data-fetching description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`). version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill native-data-fetching --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- # Expo Networking @@ -505,3 +512,14 @@ User: "Where should I put my API key?" User: "How do I load data for a page in Expo Router?" -> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill native-data-fetching --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md new file mode 100644 index 0000000..9f0e14a --- /dev/null +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -0,0 +1,137 @@ +--- +name: skill-feedback +description: Submit feedback about Expo skills, and how the bundled usage telemetry works. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving. +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill skill-feedback --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 +--- + +# Skill Feedback + +The shared feedback + usage-telemetry helper for the Expo skills. Every skill ends +with an `Expo Skill Feedback` footer that points here. It sends anonymous events to +PostHog so the Expo team can see how skills are used and where they need work. + +No setup, no env vars, and no user token are required. On first send it creates one +random local installation ID at `~/.expo-skills/installation-id` and sends only a +short **hash** of it. Hostnames, usernames, emails, file paths, source code, prompts, +tool inputs, and machine identifiers are **never** sent. + +## Runtime + +Scripts are plain JavaScript and run under **Node or Bun** — both ship the built-ins +they use (`crypto`, `https`, `fs`). `scripts/run.sh` picks whichever is installed +(`node`, then `bun`) and does nothing if neither is found, so a missing runtime never +blocks a tool call. Node/Bun is a hard requirement for Expo development, so it is +effectively always present where these skills run. + +## Events + +| Event | When | How | +| --- | --- | --- | +| `skill_read` | A skill's `SKILL.md` is read | Plugin `Read` hook (Claude Code), scoped to this plugin | +| `skill_activated` | A skill is active and the agent makes its first tool call | Per-skill `PostToolUse` hook (Claude Code), deduped per session | +| `skill_feedback` | You submit feedback | Manual — the command below | + +## Submitting feedback + +Keep it to 1-3 short sentences, name the skill, and prefer concrete observations. + +**Claude Code** (`${CLAUDE_SKILL_DIR}` resolves to the active skill's folder): + +```bash +sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" \ + "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" \ + --skill expo-deployment --rating useful --agent-harness claude-code \ + --text "TestFlight steps were clear and worked first try." +``` + +**Other agents (Codex, etc.)** — run the same bundled script with `node` or `bun`, +using its path on disk and your harness name: + +```bash +node skill-feedback/scripts/skill-feedback.js \ + --skill deslop --rating confusing --agent-harness codex \ + --text "The skill should say which files it inspected before changing code." +``` + +Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Optional `--context key=value` +(repeatable) adds small metadata. Add `--dry-run` to print the payload without sending. + +**Never include** secrets, private data, source code, long prompts, stack traces, API +keys, or tokens. + +## Harness support + +- **Claude Code** — `skill_read` and `skill_activated` are automatic via hooks. +- **Codex / Cursor / other agents** — no hook system, so automatic events do not fire. + The skills still work fully; only `skill_feedback` is available (run the script + directly). If a harness later exposes a per-tool hook adapter, the same scripts wire + straight into it. + +## Turning it off + +Telemetry is anonymous and on by default. The reliable, launch-independent way to opt +out is the bundled toggle — it writes `~/.expo-skills/opt-out`, which every script checks +before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run +it directly: + +```bash +# Claude Code: +sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/telemetry.js" --off +# Other agents: +node skill-feedback/scripts/telemetry.js --off # --on to re-enable, --status to check +``` + +For CI or a global opt-out, set an env var instead. Env vars are convenient but only +apply where they're exported and don't always reach hook subprocesses, so prefer the +toggle for a persistent opt-out: + +```bash +export EXPO_SKILLS_TELEMETRY=0 # or: false / off / no +export DO_NOT_TRACK=1 # cross-tool standard, https://consoledonottrack.com +``` + +When disabled, no installation ID is created and nothing is sent. Uninstalling the plugin +removes everything. + +## Configuring the PostHog project + +The project key lives in `scripts/telemetry_common.js`. It is a **write-only, public** +ingestion key (`phc_...`) — the same kind embedded in browser snippets — so it is safe +to commit. Override it per environment (e.g. a staging project) without editing the file: + +```bash +export EXPO_SKILLS_POSTHOG_KEY="phc_your_project_key" +``` + +Only ever use a *project* key (`phc_...`) here. Never a *personal* API key (`phx_...`) — +those are secret and used only for reading/querying (e.g. the PostHog MCP). + +## PostHog event shape + +- `event`: `skill_read` | `skill_activated` | `skill_feedback` +- `distinct_id`: `expo-skills-installation:` +- `properties.source`: `expo-skills` +- `properties.$process_person_profile`: `false` +- `properties.skill`: skill folder name (e.g. `expo-deployment`) +- `properties.agent_harness`: `claude-code`, `codex`, … (defaults to `unknown`) +- `properties.model_config`: model/config string when the harness exposes it, else `unknown` +- `properties.installation_id_hash`: anonymous hash of the local random installation ID +- `properties.session_id_hash`: short hash only; raw session IDs are never sent +- `skill_feedback` adds `properties.rating` and `properties.feedback_text` + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" --skill skill-feedback --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/scripts/package.json b/plugins/expo/skills/skill-feedback/scripts/package.json new file mode 100644 index 0000000..0ebb97d --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/package.json @@ -0,0 +1,6 @@ +{ + "name": "expo-skills-telemetry", + "private": true, + "type": "commonjs", + "description": "Zero-dependency telemetry scripts run by node or bun. No install step needed." +} diff --git a/plugins/expo/skills/skill-feedback/scripts/run.sh b/plugins/expo/skills/skill-feedback/scripts/run.sh new file mode 100644 index 0000000..6ca9d44 --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/run.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Launch a bundled telemetry script with whatever JS runtime is available. +# +# Expo development requires a JS runtime, but it may be Node *or* Bun (and the +# binary may not be named `node`). This picks the first one present and runs the +# script with it. If neither is installed, it exits 0 and does nothing, so a +# missing runtime never blocks a tool call. +# +# Usage: sh run.js.sh [args...] + +script="$1" +[ -n "$script" ] || exit 0 +shift + +if command -v node >/dev/null 2>&1; then exec node "$script" "$@"; fi +if command -v bun >/dev/null 2>&1; then exec bun "$script" "$@"; fi + +exit 0 diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js new file mode 100644 index 0000000..12de1de --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -0,0 +1,224 @@ +#!/usr/bin/env node +// Submit skill lifecycle telemetry (skill_read, skill_activated) to PostHog. +// +// Runs from Claude Code hooks. Reads the hook payload from stdin (when present) +// and never blocks: on any error it exits 0 under --quiet. + +const crypto = require("crypto"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); + +const { + POSTHOG_PROJECT_API_KEY, + SOURCE, + SCHEMA_VERSION, + telemetryDisabled, + telemetryConfigured, + shortHash, + stableStringify, + parseContext, + telemetryIdentity, + sendToPosthog, +} = require("./telemetry_common.js"); + +const EVENTS = ["skill_read", "skill_activated"]; + +function parseArgs(argv) { + const args = { + skill: "", + event: "", + agentHarness: "unknown", + modelConfig: "unknown", + context: [], + pluginRoot: "", + dryRun: false, + quiet: false, + }; + for (let i = 0; i < argv.length; i++) { + const flag = argv[i]; + const next = () => argv[++i] || ""; + switch (flag) { + case "--skill": args.skill = next(); break; + case "--event": args.event = next(); break; + case "--agent-harness": args.agentHarness = next(); break; + case "--model-config": args.modelConfig = next(); break; + case "--context": args.context.push(next()); break; + case "--plugin-root": args.pluginRoot = next(); break; + case "--dry-run": args.dryRun = true; break; + case "--quiet": args.quiet = true; break; + default: break; // ignore unknown flags + } + } + return args; +} + +function readHookInput() { + try { + if (process.stdin.isTTY) return {}; + const raw = fs.readFileSync(0, "utf8").trim(); // fd 0 = stdin + if (!raw) return {}; + const parsed = JSON.parse(raw); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function hookFilePath(hookInput) { + const toolInput = hookInput.tool_input; + if (!toolInput || typeof toolInput !== "object") return ""; + return String(toolInput.file_path || toolInput.path || "").trim(); +} + +function skillFromFilePath(filePath) { + if (!filePath) return ""; + const parts = filePath.replace(/\\/g, "/").split("/").filter(Boolean); + if (parts[parts.length - 1] !== "SKILL.md") return ""; + for (let i = 0; i < parts.length; i++) { + if (parts[i] !== "skills") continue; + if (i + 2 < parts.length && parts[i + 2] === "SKILL.md") return parts[i + 1]; + if (i + 3 < parts.length && parts[i + 3] === "SKILL.md") return parts[i + 2]; + } + return ""; +} + +// Only emit skill_read for files that live under this plugin, so we never +// track SKILL.md reads from unrelated plugins or projects. +function isUnderPluginRoot(filePath, pluginRoot) { + if (!pluginRoot) return true; + const resolve = (p) => { + try { return fs.realpathSync(path.resolve(p)); } catch { return path.resolve(p); } + }; + const file = resolve(filePath); + const root = resolve(pluginRoot); + return file === root || file.startsWith(root + path.sep); +} + +function resolveEvent(args) { + if (args.event) return args.event; + throw new Error("--event is required"); +} + +function eventPayload(args, hookInput) { + const eventName = resolveEvent(args); + let skill = args.skill.trim(); + if (eventName === "skill_read" && skill === "auto") { + skill = skillFromFilePath(hookFilePath(hookInput)); + } + + const agentHarness = args.agentHarness.trim() || "unknown"; + const modelConfig = args.modelConfig.trim() || "unknown"; + + if (!skill) throw new Error("--skill cannot be empty"); + if (!EVENTS.includes(eventName)) throw new Error(`--event must be one of: ${EVENTS.join(", ")}`); + + const timestamp = new Date().toISOString(); + const sessionIdHash = shortHash(hookInput.session_id); + const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { + createInstallation: !args.dryRun, + }); + + const insertSource = stableStringify({ + skill, + event: eventName, + agent_harness: agentHarness, + model_config: modelConfig, + session_id_hash: sessionIdHash, + timestamp, + }); + + const properties = { + $process_person_profile: false, + $insert_id: `${eventName}:` + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), + source: SOURCE, + schema_version: SCHEMA_VERSION, + skill, + agent_harness: agentHarness, + model_config: modelConfig, + ...identityProperties, + }; + if (sessionIdHash) properties.session_id_hash = sessionIdHash; + + const context = parseContext(args.context); + if (Object.keys(context).length) properties.context = context; + + return { + api_key: POSTHOG_PROJECT_API_KEY, + event: eventName, + distinct_id: distinctId, + timestamp, + properties, + }; +} + +function activationMarkerPath(skill, sessionIdHash) { + const markerId = crypto.createHash("sha256").update(`${skill}:${sessionIdHash}`).digest("hex").slice(0, 24); + return path.join(os.tmpdir(), `expo-skills-activated-${markerId}`); +} + +// skill_activated should fire once per skill per session. Use an atomic marker +// file so concurrent hook calls don't double-send. +function shouldSendActivation(args, hookInput, dryRun) { + if (resolveEvent(args) !== "skill_activated") return false; + const sessionIdHash = shortHash(hookInput.session_id); + if (!sessionIdHash) return true; + if (dryRun) return true; + + const marker = activationMarkerPath(args.skill.trim(), sessionIdHash); + try { + const fd = fs.openSync(marker, "wx", 0o600); + try { fs.writeFileSync(fd, new Date().toISOString()); } finally { fs.closeSync(fd); } + return true; + } catch (err) { + if (err && err.code === "EEXIST") return false; + return true; + } +} + +async function main(argv) { + const args = parseArgs(argv); + if (telemetryDisabled()) return 0; + if (!telemetryConfigured() && !args.dryRun) return 0; // no key set → fully inert + const hookInput = readHookInput(); + + let payload; + try { + const eventName = resolveEvent(args); + + if (eventName === "skill_read" && args.skill.trim() === "auto") { + const filePath = hookFilePath(hookInput); + if (!skillFromFilePath(filePath)) return 0; + if (!isUnderPluginRoot(filePath, args.pluginRoot)) return 0; + } + + if (eventName === "skill_activated" && !shouldSendActivation(args, hookInput, args.dryRun)) { + if (!args.quiet) console.log(`skill-event: skipped duplicate activation event for ${args.skill}`); + return 0; + } + + payload = eventPayload(args, hookInput); + } catch (err) { + if (!args.quiet) console.error(`skill-event: ${err.message}`); + return 2; + } + + if (args.dryRun) { + console.log(JSON.stringify({ ...payload, api_key: "phc_..." }, null, 2)); + return 0; + } + + try { + await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 5000 }); + } catch (err) { + if (!args.quiet) console.error(`skill-event: ${err.message}`); + return args.quiet ? 0 : 1; + } + + if (!args.quiet) console.log(`sent ${payload.event}: ${payload.properties.skill}`); + return 0; +} + +main(process.argv.slice(2)) + .then((code) => process.exit(code)) + .catch(() => process.exit(0)); diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js new file mode 100644 index 0000000..77a2b5e --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -0,0 +1,144 @@ +#!/usr/bin/env node +// Submit feedback about an Expo skill to PostHog. +// +// Usage: +// node skill-feedback.js --skill --rating --text "..." \ +// [--agent-harness ] [--model-config ] [--context k=v] [--dry-run] + +const crypto = require("crypto"); + +const { + POSTHOG_PROJECT_API_KEY, + SOURCE, + SCHEMA_VERSION, + telemetryDisabled, + telemetryConfigured, + stableStringify, + parseContext, + telemetryIdentity, + sendToPosthog, +} = require("./telemetry_common.js"); + +const EVENT_NAME = "skill_feedback"; +const RATINGS = ["useful", "confusing", "bug", "idea", "other"]; +const MAX_FEEDBACK_CHARS = 4000; + +function parseArgs(argv) { + const args = { + skill: "", + rating: "", + text: "", + agentHarness: "unknown", + modelConfig: "unknown", + context: [], + dryRun: false, + }; + for (let i = 0; i < argv.length; i++) { + const flag = argv[i]; + const next = () => argv[++i] || ""; + switch (flag) { + case "--skill": args.skill = next(); break; + case "--rating": args.rating = next(); break; + case "--text": args.text = next(); break; + case "--agent-harness": args.agentHarness = next(); break; + case "--model-config": args.modelConfig = next(); break; + case "--context": args.context.push(next()); break; + case "--dry-run": args.dryRun = true; break; + default: break; + } + } + return args; +} + +function eventPayload(args) { + const feedback = args.text.trim().slice(0, MAX_FEEDBACK_CHARS); + const skill = args.skill.trim(); + const agentHarness = args.agentHarness.trim() || "unknown"; + const modelConfig = args.modelConfig.trim() || "unknown"; + + if (!feedback) throw new Error("--text cannot be empty"); + if (!skill) throw new Error("--skill cannot be empty"); + if (!RATINGS.includes(args.rating)) throw new Error(`--rating must be one of: ${RATINGS.join(", ")}`); + + const timestamp = new Date().toISOString(); + const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { + createInstallation: !args.dryRun, + }); + + const insertSource = stableStringify({ + agent_harness: agentHarness, + model_config: modelConfig, + skill, + rating: args.rating, + feedback, + timestamp, + }); + + const properties = { + $process_person_profile: false, + $insert_id: "skill-feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), + source: SOURCE, + schema_version: SCHEMA_VERSION, + ...identityProperties, + agent_harness: agentHarness, + model_config: modelConfig, + skill, + rating: args.rating, + feedback_text: feedback, + }; + + const context = parseContext(args.context); + if (Object.keys(context).length) properties.context = context; + + return { + api_key: POSTHOG_PROJECT_API_KEY, + event: EVENT_NAME, + distinct_id: distinctId, + timestamp, + properties, + }; +} + +async function main(argv) { + const args = parseArgs(argv); + + if (telemetryDisabled()) { + console.error("skill-feedback: telemetry is disabled (opt-out file or EXPO_SKILLS_TELEMETRY/DO_NOT_TRACK); nothing sent. Re-enable with telemetry.js --on."); + return 0; + } + + if (!telemetryConfigured() && !args.dryRun) { + console.error("skill-feedback: no PostHog key configured (placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or the key in telemetry_common.js."); + return 0; + } + + let payload; + try { + payload = eventPayload(args); + } catch (err) { + console.error(`skill-feedback: ${err.message}`); + return 2; + } + + if (args.dryRun) { + console.log(JSON.stringify({ ...payload, api_key: "phc_..." }, null, 2)); + return 0; + } + + try { + await sendToPosthog(payload, { userAgent: "expo-skills/skill-feedback", timeoutMs: 10000 }); + } catch (err) { + console.error(`skill-feedback: ${err.message}`); + return 1; + } + + console.log(`sent skill feedback: ${payload.properties.skill}`); + return 0; +} + +main(process.argv.slice(2)) + .then((code) => process.exit(code)) + .catch((err) => { + console.error(`skill-feedback: ${err && err.message ? err.message : err}`); + process.exit(1); + }); diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry.js b/plugins/expo/skills/skill-feedback/scripts/telemetry.js new file mode 100644 index 0000000..ad7e8cd --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +// Turn Expo skills usage telemetry on or off, or check its status. +// +// Usage: +// node telemetry.js --status # show whether telemetry is on/off and why +// node telemetry.js --off # disable (writes the opt-out file) +// node telemetry.js --on # re-enable (removes the opt-out file) +// +// The opt-out file is the reliable switch: it works no matter how the agent was +// launched. The DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY env vars also disable +// telemetry (handy for CI), but env vars don't always reach hook subprocesses. + +const fs = require("fs"); +const path = require("path"); +const { OPT_OUT_PATH, telemetryConfigured } = require("./telemetry_common.js"); + +function disabledByEnv() { + const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); + if (["0", "false", "off", "no"].includes(flag)) return "EXPO_SKILLS_TELEMETRY"; + const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); + if (dnt && dnt !== "0" && dnt !== "false") return "DO_NOT_TRACK"; + return null; +} + +function printStatus() { + const byFile = fs.existsSync(OPT_OUT_PATH); + const byEnv = disabledByEnv(); + if (byFile || byEnv) { + const reasons = []; + if (byFile) reasons.push(`opt-out file (${OPT_OUT_PATH})`); + if (byEnv) reasons.push(`env var ${byEnv}`); + console.log(`Expo skills telemetry: DISABLED — via ${reasons.join(" and ")}.`); + } else if (!telemetryConfigured()) { + console.log("Expo skills telemetry: ENABLED but NOT CONFIGURED — no PostHog key set (placeholder), so nothing is created or sent. Disable permanently with: telemetry.js --off"); + } else { + console.log("Expo skills telemetry: ENABLED (anonymous). Disable with: telemetry.js --off"); + } +} + +const cmd = process.argv[2]; + +if (cmd === "--off" || cmd === "--disable") { + fs.mkdirSync(path.dirname(OPT_OUT_PATH), { recursive: true, mode: 0o700 }); + fs.writeFileSync(OPT_OUT_PATH, "Expo skills telemetry disabled by user.\n"); + console.log(`Telemetry disabled — wrote ${OPT_OUT_PATH}`); + console.log("Re-enable any time with: telemetry.js --on"); +} else if (cmd === "--on" || cmd === "--enable") { + try { fs.rmSync(OPT_OUT_PATH, { force: true }); } catch {} + const byEnv = disabledByEnv(); + console.log("Telemetry re-enabled — removed the opt-out file."); + if (byEnv) console.log(`Note: still disabled by env var ${byEnv}; unset it to fully re-enable.`); +} else if (cmd === "--status" || cmd === undefined) { + printStatus(); +} else { + console.error(`Unknown option: ${cmd}\nUsage: telemetry.js [--status | --off | --on]`); + process.exit(2); +} diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js new file mode 100644 index 0000000..1a6eb9a --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -0,0 +1,171 @@ +// Shared helpers for Expo skills PostHog telemetry. +// +// Zero npm dependencies — Node.js built-ins only (crypto, fs, https, os, path). +// Node is a hard requirement for Expo development, so it is available wherever +// these skills are used. If `node` is ever missing, the calling hook simply +// fails non-blocking and no telemetry is sent. + +const crypto = require("crypto"); +const fs = require("fs"); +const https = require("https"); +const os = require("os"); +const path = require("path"); + +const POSTHOG_HOST = "https://us.i.posthog.com"; + +// PostHog project API key. This is a *write-only, public* ingestion key — the +// same kind embedded in browser snippets — so it is safe to commit. Override +// per environment with EXPO_SKILLS_POSTHOG_KEY (e.g. a staging project). +// NOTE: never put a PostHog *personal* API key (phx_...) here — those are secret. +const POSTHOG_PROJECT_API_KEY = + process.env.EXPO_SKILLS_POSTHOG_KEY || "phc_w8xRytdAAwkV3oExnuUozqH64PMzCmDLnyoChpPBcNXs"; + +const SOURCE = "expo-skills"; +const SCHEMA_VERSION = 1; +const INSTALLATION_ID_PATH = path.join( + os.homedir(), + ".expo-skills", + "installation-id" +); + +// Persistent opt-out marker. Checked before anything is sent, so it works +// regardless of how the agent was launched (env vars don't always reach hook +// subprocesses). Toggle it with scripts/telemetry.js --off / --on. +const OPT_OUT_PATH = path.join(os.homedir(), ".expo-skills", "opt-out"); + +// Opt-out switch. Disabled when EXPO_SKILLS_TELEMETRY is 0/false/off/no, or +// when the cross-tool DO_NOT_TRACK convention (https://consoledonottrack.com) +// is set to anything other than 0/false. +function telemetryDisabled() { + // 1) Persistent opt-out file — the reliable, launch-independent switch. + try { if (fs.existsSync(OPT_OUT_PATH)) return true; } catch {} + // 2) Env vars — for CI / global opt-out. + const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); + if (["0", "false", "off", "no"].includes(flag)) return true; + const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); + if (dnt && dnt !== "0" && dnt !== "false") return true; + return false; +} + +// True only when a real PostHog project key is set. Until then the plugin is +// fully inert: no installation ID is created and no network request is made. +function telemetryConfigured() { + const key = String(POSTHOG_PROJECT_API_KEY || "").trim(); + return key.length > 0 && key !== "phc_REPLACE_ME"; +} + +function shortHash(value, length = 16) { + if (!value) return null; + return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length); +} + +// Deterministic JSON with sorted keys, so $insert_id is stable across runs. +function stableStringify(value) { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"; + const keys = Object.keys(value).sort(); + return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}"; +} + +function parseContext(values) { + const context = {}; + for (const value of values) { + const idx = value.indexOf("="); + if (idx === -1) throw new Error("--context must be key=value"); + const key = value.slice(0, idx).trim(); + if (!key) throw new Error("--context key cannot be empty"); + context[key] = value.slice(idx + 1).trim().slice(0, 500); + } + return context; +} + +function readInstallationId(create = true) { + try { + if (fs.existsSync(INSTALLATION_ID_PATH)) { + const existing = fs.readFileSync(INSTALLATION_ID_PATH, "utf8").trim(); + if (existing) return existing; + } + if (!create) return null; + + fs.mkdirSync(path.dirname(INSTALLATION_ID_PATH), { recursive: true, mode: 0o700 }); + try { fs.chmodSync(path.dirname(INSTALLATION_ID_PATH), 0o700); } catch {} + + const installationId = crypto.randomUUID().replace(/-/g, ""); + try { + // 'wx' = O_CREAT | O_EXCL | O_WRONLY — atomic create, fails if it exists. + const fd = fs.openSync(INSTALLATION_ID_PATH, "wx", 0o600); + try { fs.writeFileSync(fd, installationId + "\n"); } finally { fs.closeSync(fd); } + return installationId; + } catch (err) { + if (err && err.code === "EEXIST") { + const existing = fs.readFileSync(INSTALLATION_ID_PATH, "utf8").trim(); + return existing || null; + } + throw err; + } + } catch { + return null; + } +} + +function installationIdHash(create = true) { + return shortHash(readInstallationId(create), 32); +} + +function telemetryIdentity(agentHarness, { createInstallation = true } = {}) { + const installHash = installationIdHash(createInstallation); + if (installHash) { + return [`expo-skills-installation:${installHash}`, { installation_id_hash: installHash }]; + } + return [`expo-skills-events:${agentHarness}`, {}]; +} + +function sendToPosthog(payload, { userAgent, timeoutMs }) { + return new Promise((resolve, reject) => { + const url = new URL("/i/v0/e/", POSTHOG_HOST); + const body = Buffer.from(JSON.stringify(payload), "utf8"); + const req = https.request( + url, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": body.length, + "User-Agent": userAgent, + }, + timeout: timeoutMs, + }, + (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + const status = res.statusCode || 0; + if (status >= 200 && status < 300) return resolve(); + reject(new Error(`HTTP ${status} ${Buffer.concat(chunks).toString("utf8")}`)); + }); + } + ); + req.on("timeout", () => req.destroy(new Error("request timed out"))); + req.on("error", reject); + req.write(body); + req.end(); + }); +} + +module.exports = { + POSTHOG_HOST, + POSTHOG_PROJECT_API_KEY, + SOURCE, + SCHEMA_VERSION, + INSTALLATION_ID_PATH, + OPT_OUT_PATH, + telemetryDisabled, + telemetryConfigured, + shortHash, + stableStringify, + parseContext, + readInstallationId, + installationIdHash, + telemetryIdentity, + sendToPosthog, +}; diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index 5769946..c571082 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -3,6 +3,13 @@ name: upgrading-expo description: Guidelines for upgrading Expo SDK versions and fixing dependency issues version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill upgrading-expo --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- ## References @@ -132,3 +139,14 @@ Since SDK 55, users can opt-in to use Hermes engine v1 for improved runtime perf ## New Architecture The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53. + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill upgrading-expo --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index b11eafa..2841645 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -3,6 +3,13 @@ name: use-dom description: Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally. version: 1.0.0 license: MIT +hooks: + PostToolUse: + - matcher: "*" + hooks: + - type: command + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill use-dom --event skill_activated --agent-harness claude-code --quiet' + timeout: 5 --- ## What are DOM Components? @@ -415,3 +422,14 @@ On web, the `dom` prop is ignored since no webview is needed. - Test on all platforms — web rendering may differ slightly from native webviews - Large DOM components may impact performance — profile if needed - The webview has its own JavaScript context — cannot directly share state with native + +--- + +## Expo Skill Feedback + +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): + +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill use-dom --rating idea --agent-harness claude-code --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. + +Never include secrets, private data, source code, long prompts, or stack traces. From 1a99b31ce84a403caf7e15453d5d62a075688363 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 4 Jun 2026 14:12:31 +0200 Subject: [PATCH 02/23] refactor(expo): auto-detect harness, self-derive plugin root, slim docs Addresses PR review feedback: - skill-event/skill-feedback now auto-detect the agent harness from env (CLAUDECODE -> claude-code; CODEX_SANDBOX / AGENT=codex -> codex; else unknown), removing the hardcoded `--agent-harness claude-code` from hooks.json, all 16 per-skill hooks, and every feedback footer. - skill-event derives the plugin root from its own location, so the Read hook no longer needs the `--plugin-root` flag. - Trim the CLAUDE.md telemetry section to just what it does and how to turn it off (full details live in the skill-feedback skill). Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 29 ++----------------- plugins/expo/hooks/hooks.json | 2 +- plugins/expo/skills/add-app-clip/SKILL.md | 8 ++--- .../expo/skills/building-native-ui/SKILL.md | 8 ++--- .../expo/skills/eas-update-insights/SKILL.md | 8 ++--- plugins/expo/skills/expo-api-routes/SKILL.md | 8 ++--- plugins/expo/skills/expo-brownfield/SKILL.md | 8 ++--- .../expo/skills/expo-cicd-workflows/SKILL.md | 8 ++--- plugins/expo/skills/expo-deployment/SKILL.md | 8 ++--- plugins/expo/skills/expo-dev-client/SKILL.md | 8 ++--- plugins/expo/skills/expo-module/SKILL.md | 8 ++--- plugins/expo/skills/expo-observe/SKILL.md | 8 ++--- .../expo/skills/expo-tailwind-setup/SKILL.md | 8 ++--- .../skills/expo-ui-jetpack-compose/SKILL.md | 8 ++--- plugins/expo/skills/expo-ui-swift-ui/SKILL.md | 8 ++--- .../expo/skills/native-data-fetching/SKILL.md | 8 ++--- plugins/expo/skills/skill-feedback/SKILL.md | 15 +++++----- .../skill-feedback/scripts/skill-event.js | 9 ++++-- .../skill-feedback/scripts/skill-feedback.js | 5 ++-- .../scripts/telemetry_common.js | 16 ++++++++++ plugins/expo/skills/upgrading-expo/SKILL.md | 8 ++--- plugins/expo/skills/use-dom/SKILL.md | 8 ++--- 22 files changed, 100 insertions(+), 104 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 698dcd9..2c9042e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,31 +244,6 @@ Users install plugins from this marketplace: ## Usage Telemetry & Feedback -The `expo` plugin ships anonymous usage telemetry + a feedback path. The shared, -zero-dependency Node/Bun scripts live in `plugins/expo/skills/skill-feedback/scripts/` -(`telemetry_common.js`, `skill-event.js`, `skill-feedback.js`, launched via `run.sh`), -and the plugin-level `skill_read` hook is in `plugins/expo/hooks/hooks.json`. See the -`skill-feedback` skill for the full design, privacy model, opt-out, and PostHog config. - -**Every new skill must include two things** (copy from any existing skill): - -1. A `skill_activated` hook in the `SKILL.md` frontmatter: - - ```yaml - hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill --event skill_activated --agent-harness claude-code --quiet' - timeout: 5 - ``` - -2. An `## Expo Skill Feedback` footer at the end of the `SKILL.md` (harness-agnostic; - `--skill `). +The `expo` plugin sends **anonymous** usage telemetry and collects skill feedback (details in the `skill-feedback` skill). When adding a skill, copy the `skill_activated` frontmatter hook and the `## Expo Skill Feedback` footer from any existing skill. -Use the skill's **folder name** for `--skill`. Telemetry is anonymous and opt-out: a -persistent toggle (`telemetry.js --off`, writing `~/.expo-skills/opt-out`) or the -`EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1` env vars. The PostHog *project* key in -`telemetry_common.js` is a public, write-only ingestion key (safe to commit); override -per-env with `EXPO_SKILLS_POSTHOG_KEY`. Never commit a personal API key (`phx_...`). +**Turn it off:** run `telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index 0bbeb45..ad31745 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_read --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --agent-harness claude-code --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_read --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index 6b272e3..e1cb2f7 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -6,7 +6,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill add-app-clip --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill add-app-clip --event skill_activated --quiet' timeout: 5 --- @@ -290,9 +290,9 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill add-app-clip --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill add-app-clip --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 75a810a..919a3ca 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill building-native-ui --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill building-native-ui --event skill_activated --quiet' timeout: 5 --- @@ -331,9 +331,9 @@ export default function Layout({ segment }) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill building-native-ui --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill building-native-ui --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index e2857da..089f26c 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -9,7 +9,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill eas-update-insights --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill eas-update-insights --event skill_activated --quiet' timeout: 5 --- @@ -238,9 +238,9 @@ Human-readable group details plus 30 days of launches/failures per platform — ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill eas-update-insights --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill eas-update-insights --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index efefd52..53f0884 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-api-routes --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-api-routes --event skill_activated --quiet' timeout: 5 --- @@ -378,9 +378,9 @@ export async function GET(request: Request) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-api-routes --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-api-routes --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 761971d..62b394d 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -6,7 +6,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-brownfield --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-brownfield --event skill_activated --quiet' timeout: 5 --- @@ -64,9 +64,9 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-brownfield --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-brownfield --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index a3db238..7808b8c 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -9,7 +9,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-cicd-workflows --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-cicd-workflows --event skill_activated --quiet' timeout: 5 --- @@ -102,9 +102,9 @@ When users ask about available options (job types, triggers, runner types, etc.) ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-cicd-workflows --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-cicd-workflows --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 39bfa9e..5171d81 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-deployment --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-deployment --event skill_activated --quiet' timeout: 5 --- @@ -200,9 +200,9 @@ eas submit:list ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-deployment --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-deployment --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index f356388..9606773 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-dev-client --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-dev-client --event skill_activated --quiet' timeout: 5 --- @@ -174,9 +174,9 @@ eas update ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-dev-client --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-dev-client --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index c4f8c73..d606894 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-module --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-module --event skill_activated --quiet' timeout: 5 --- @@ -151,9 +151,9 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-module --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-module --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index 1c9b149..f6a041f 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-observe --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-observe --event skill_activated --quiet' timeout: 5 --- @@ -39,9 +39,9 @@ The three reference files in `./references/` cover the three things people typic ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-observe --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-observe --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 471c9d7..57da8ba 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-tailwind-setup --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-tailwind-setup --event skill_activated --quiet' timeout: 5 --- @@ -490,9 +490,9 @@ type Props = React.ComponentProps & { className?: string }; ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-tailwind-setup --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-tailwind-setup --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md index 019d742..2310192 100644 --- a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md +++ b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md @@ -6,7 +6,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-jetpack-compose --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-jetpack-compose --event skill_activated --quiet' timeout: 5 --- @@ -50,9 +50,9 @@ import { fillMaxWidth, paddingAll } from "@expo/ui/jetpack-compose/modifiers"; ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-jetpack-compose --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-jetpack-compose --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md index 811001c..db90208 100644 --- a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md @@ -6,7 +6,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-swift-ui --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-swift-ui --event skill_activated --quiet' timeout: 5 --- @@ -49,9 +49,9 @@ import { Pressable } from "react-native"; ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-swift-ui --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui-swift-ui --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index 498c68d..8b2ae41 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill native-data-fetching --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill native-data-fetching --event skill_activated --quiet' timeout: 5 --- @@ -517,9 +517,9 @@ User: "How do I load data for a page in Expo Router?" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill native-data-fetching --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill native-data-fetching --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 9f0e14a..43b470f 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -6,7 +6,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill skill-feedback --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill skill-feedback --event skill_activated --quiet' timeout: 5 --- @@ -46,12 +46,13 @@ Keep it to 1-3 short sentences, name the skill, and prefer concrete observations ```bash sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" \ "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" \ - --skill expo-deployment --rating useful --agent-harness claude-code \ + --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` **Other agents (Codex, etc.)** — run the same bundled script with `node` or `bun`, -using its path on disk and your harness name: +using its path on disk (the harness auto-detects; `--agent-harness` below is an +explicit override): ```bash node skill-feedback/scripts/skill-feedback.js \ @@ -119,7 +120,7 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). - `properties.source`: `expo-skills` - `properties.$process_person_profile`: `false` - `properties.skill`: skill folder name (e.g. `expo-deployment`) -- `properties.agent_harness`: `claude-code`, `codex`, … (defaults to `unknown`) +- `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` - `properties.model_config`: model/config string when the harness exposes it, else `unknown` - `properties.installation_id_hash`: anonymous hash of the local random installation ID - `properties.session_id_hash`: short hash only; raw session IDs are never sent @@ -129,9 +130,9 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" --skill skill-feedback --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" --skill skill-feedback --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index 12de1de..2d035b5 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -15,6 +15,7 @@ const { SCHEMA_VERSION, telemetryDisabled, telemetryConfigured, + detectHarness, shortHash, stableStringify, parseContext, @@ -28,7 +29,7 @@ function parseArgs(argv) { const args = { skill: "", event: "", - agentHarness: "unknown", + agentHarness: "", modelConfig: "unknown", context: [], pluginRoot: "", @@ -107,7 +108,7 @@ function eventPayload(args, hookInput) { skill = skillFromFilePath(hookFilePath(hookInput)); } - const agentHarness = args.agentHarness.trim() || "unknown"; + const agentHarness = args.agentHarness.trim() || detectHarness(); const modelConfig = args.modelConfig.trim() || "unknown"; if (!skill) throw new Error("--skill cannot be empty"); @@ -189,7 +190,9 @@ async function main(argv) { if (eventName === "skill_read" && args.skill.trim() === "auto") { const filePath = hookFilePath(hookInput); if (!skillFromFilePath(filePath)) return 0; - if (!isUnderPluginRoot(filePath, args.pluginRoot)) return 0; + // Default the plugin root to this script's location (/skills/skill-feedback/scripts). + const pluginRoot = args.pluginRoot || path.resolve(__dirname, "..", "..", ".."); + if (!isUnderPluginRoot(filePath, pluginRoot)) return 0; } if (eventName === "skill_activated" && !shouldSendActivation(args, hookInput, args.dryRun)) { diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js index 77a2b5e..549bfeb 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -13,6 +13,7 @@ const { SCHEMA_VERSION, telemetryDisabled, telemetryConfigured, + detectHarness, stableStringify, parseContext, telemetryIdentity, @@ -28,7 +29,7 @@ function parseArgs(argv) { skill: "", rating: "", text: "", - agentHarness: "unknown", + agentHarness: "", modelConfig: "unknown", context: [], dryRun: false, @@ -53,7 +54,7 @@ function parseArgs(argv) { function eventPayload(args) { const feedback = args.text.trim().slice(0, MAX_FEEDBACK_CHARS); const skill = args.skill.trim(); - const agentHarness = args.agentHarness.trim() || "unknown"; + const agentHarness = args.agentHarness.trim() || detectHarness(); const modelConfig = args.modelConfig.trim() || "unknown"; if (!feedback) throw new Error("--text cannot be empty"); diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js index 1a6eb9a..6dad138 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -54,6 +54,21 @@ function telemetryConfigured() { return key.length > 0 && key !== "phc_REPLACE_ME"; } +// Best-effort agent-harness detection from environment signals — used as the +// default when --agent-harness isn't passed. Claude Code sets CLAUDECODE; Codex +// sets CODEX_SANDBOX (in sandboxed mode) and is moving toward AGENT=codex. +function detectHarness() { + if (process.env.CLAUDECODE) return "claude-code"; + if ( + process.env.CODEX_SANDBOX || + process.env.CODEX_SANDBOX_NETWORK_DISABLED || + String(process.env.AGENT || "").toLowerCase() === "codex" + ) { + return "codex"; + } + return "unknown"; +} + function shortHash(value, length = 16) { if (!value) return null; return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length); @@ -161,6 +176,7 @@ module.exports = { OPT_OUT_PATH, telemetryDisabled, telemetryConfigured, + detectHarness, shortHash, stableStringify, parseContext, diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index c571082..0e381b8 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill upgrading-expo --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill upgrading-expo --event skill_activated --quiet' timeout: 5 --- @@ -144,9 +144,9 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill upgrading-expo --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill upgrading-expo --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 2841645..4ff6a51 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -8,7 +8,7 @@ hooks: - matcher: "*" hooks: - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill use-dom --event skill_activated --agent-harness claude-code --quiet' + command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill use-dom --event skill_activated --quiet' timeout: 5 --- @@ -427,9 +427,9 @@ On web, the `dom` prop is ignored since no webview is needed. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script. Set `--agent-harness` to your agent (`claude-code`, `codex`, …): +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill use-dom --rating idea --agent-harness claude-code --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags plus `--agent-harness `. +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill use-dom --rating idea --text "..."` +- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). Never include secrets, private data, source code, long prompts, or stack traces. From bfca26b29d96fd63c42750403a2b9ac8ca5a8e93 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 4 Jun 2026 14:43:52 +0200 Subject: [PATCH 03/23] feat(expo): skip telemetry in CI, add os/arch, first-run notice Adopts proven patterns from entireio/cli: - Skip telemetry in CI (CI=true plus GitHub/GitLab/CircleCI/Travis/Buildkite/ Jenkins/TeamCity/Azure signals) so usage reflects real humans; surfaced in `telemetry.js --status`. - Add non-PII `os` / `arch` to every event. - Print a one-time "anonymous telemetry" notice to stderr on the first real send, gated by ~/.expo-skills/notice-shown. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/expo/skills/skill-feedback/SKILL.md | 4 +- .../skill-feedback/scripts/skill-event.js | 4 ++ .../skill-feedback/scripts/skill-feedback.js | 4 ++ .../skill-feedback/scripts/telemetry.js | 6 ++- .../scripts/telemetry_common.js | 49 ++++++++++++++++++- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 43b470f..6d4d0b6 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -19,7 +19,8 @@ PostHog so the Expo team can see how skills are used and where they need work. No setup, no env vars, and no user token are required. On first send it creates one random local installation ID at `~/.expo-skills/installation-id` and sends only a short **hash** of it. Hostnames, usernames, emails, file paths, source code, prompts, -tool inputs, and machine identifiers are **never** sent. +tool inputs, and machine identifiers are **never** sent. Telemetry is **skipped +automatically in CI**, and the first real send prints a one-time notice to stderr. ## Runtime @@ -122,6 +123,7 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). - `properties.skill`: skill folder name (e.g. `expo-deployment`) - `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` - `properties.model_config`: model/config string when the harness exposes it, else `unknown` +- `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) - `properties.installation_id_hash`: anonymous hash of the local random installation ID - `properties.session_id_hash`: short hash only; raw session IDs are never sent - `skill_feedback` adds `properties.rating` and `properties.feedback_text` diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index 2d035b5..ffd7e01 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -16,6 +16,8 @@ const { telemetryDisabled, telemetryConfigured, detectHarness, + platformProps, + maybeShowFirstRunNotice, shortHash, stableStringify, parseContext, @@ -137,6 +139,7 @@ function eventPayload(args, hookInput) { skill, agent_harness: agentHarness, model_config: modelConfig, + ...platformProps(), ...identityProperties, }; if (sessionIdHash) properties.session_id_hash = sessionIdHash; @@ -211,6 +214,7 @@ async function main(argv) { return 0; } + maybeShowFirstRunNotice(); try { await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 5000 }); } catch (err) { diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js index 549bfeb..e8c42af 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -14,6 +14,8 @@ const { telemetryDisabled, telemetryConfigured, detectHarness, + platformProps, + maybeShowFirstRunNotice, stableStringify, parseContext, telemetryIdentity, @@ -83,6 +85,7 @@ function eventPayload(args) { ...identityProperties, agent_harness: agentHarness, model_config: modelConfig, + ...platformProps(), skill, rating: args.rating, feedback_text: feedback, @@ -126,6 +129,7 @@ async function main(argv) { return 0; } + maybeShowFirstRunNotice(); try { await sendToPosthog(payload, { userAgent: "expo-skills/skill-feedback", timeoutMs: 10000 }); } catch (err) { diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry.js b/plugins/expo/skills/skill-feedback/scripts/telemetry.js index ad7e8cd..69389a9 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry.js @@ -12,7 +12,7 @@ const fs = require("fs"); const path = require("path"); -const { OPT_OUT_PATH, telemetryConfigured } = require("./telemetry_common.js"); +const { OPT_OUT_PATH, telemetryConfigured, isCI } = require("./telemetry_common.js"); function disabledByEnv() { const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); @@ -25,9 +25,11 @@ function disabledByEnv() { function printStatus() { const byFile = fs.existsSync(OPT_OUT_PATH); const byEnv = disabledByEnv(); - if (byFile || byEnv) { + const byCI = isCI(); + if (byFile || byEnv || byCI) { const reasons = []; if (byFile) reasons.push(`opt-out file (${OPT_OUT_PATH})`); + if (byCI) reasons.push("CI environment"); if (byEnv) reasons.push(`env var ${byEnv}`); console.log(`Expo skills telemetry: DISABLED — via ${reasons.join(" and ")}.`); } else if (!telemetryConfigured()) { diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js index 6dad138..58a48fb 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -33,13 +33,35 @@ const INSTALLATION_ID_PATH = path.join( // subprocesses). Toggle it with scripts/telemetry.js --off / --on. const OPT_OUT_PATH = path.join(os.homedir(), ".expo-skills", "opt-out"); +// Marker so the one-time "we collect anonymous telemetry" notice prints once. +const NOTICE_PATH = path.join(os.homedir(), ".expo-skills", "notice-shown"); + +// CI detection — skip telemetry in automated environments so usage data reflects +// real humans. Honors the common CI=true convention plus major providers' signals. +function isCI() { + const ci = String(process.env.CI || "").trim().toLowerCase(); + if (ci && ci !== "0" && ci !== "false") return true; + return Boolean( + process.env.GITHUB_ACTIONS || + process.env.GITLAB_CI || + process.env.CIRCLECI || + process.env.TRAVIS || + process.env.BUILDKITE || + process.env.JENKINS_URL || + process.env.TEAMCITY_VERSION || + process.env.TF_BUILD + ); +} + // Opt-out switch. Disabled when EXPO_SKILLS_TELEMETRY is 0/false/off/no, or // when the cross-tool DO_NOT_TRACK convention (https://consoledonottrack.com) // is set to anything other than 0/false. function telemetryDisabled() { // 1) Persistent opt-out file — the reliable, launch-independent switch. try { if (fs.existsSync(OPT_OUT_PATH)) return true; } catch {} - // 2) Env vars — for CI / global opt-out. + // 2) Skip automated / CI environments. + if (isCI()) return true; + // 3) Env vars — for global opt-out. const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); if (["0", "false", "off", "no"].includes(flag)) return true; const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); @@ -69,6 +91,28 @@ function detectHarness() { return "unknown"; } +// Friendly OS + CPU arch for event properties (non-PII). +function platformProps() { + const osName = { darwin: "macos", win32: "windows" }[process.platform] || process.platform; + return { os: osName, arch: process.arch }; +} + +// Print a one-time transparency notice the first time real telemetry is sent. +// Best-effort and gated by an atomic marker, so it appears at most once per machine. +function maybeShowFirstRunNotice() { + try { + fs.mkdirSync(path.dirname(NOTICE_PATH), { recursive: true, mode: 0o700 }); + const fd = fs.openSync(NOTICE_PATH, "wx", 0o600); // succeeds only the first time + fs.closeSync(fd); + process.stderr.write( + "expo-skills: sending anonymous usage analytics (skill name only — no code, prompts, " + + "or personal data). Turn off with `telemetry.js --off` or EXPO_SKILLS_TELEMETRY=0.\n" + ); + } catch { + // Already shown, or filesystem unavailable — stay silent. + } +} + function shortHash(value, length = 16) { if (!value) return null; return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length); @@ -177,6 +221,9 @@ module.exports = { telemetryDisabled, telemetryConfigured, detectHarness, + isCI, + platformProps, + maybeShowFirstRunNotice, shortHash, stableStringify, parseContext, From ebc0249019da6d12eea80ba93315f1b16563bdd1 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 4 Jun 2026 17:31:03 +0200 Subject: [PATCH 04/23] fix(expo): track skill_invoked via the Skill tool (Read hook never fired) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skills load through Claude Code's Skill tool, not the Read tool — confirmed by inspecting the binary (skill_invoke / activeSkill, no read path) and a real `claude -p` run where invoking expo-observe fired the Skill tool with skill="expo:expo-observe" and zero Read calls. The old skill_read (PostToolUse:Read) hook therefore never fired on real usage. - Repoint the plugin hook to matcher "Skill" / event skill_invoked; read the skill name from tool_input.skill (strip the "plugin:" namespace) and scope to this plugin's own skills (/skills//SKILL.md must exist). - Keep per-skill skill_activated (verified it fires). - Update docs (skill-feedback SKILL.md, README). Verified end-to-end: a real claude -p invoking expo-observe landed both skill_invoked and skill_activated in PostHog project 375452. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- plugins/expo/hooks/hooks.json | 4 +- plugins/expo/skills/skill-feedback/SKILL.md | 6 +- .../skill-feedback/scripts/skill-event.js | 59 +++++++++---------- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 2ebb55f..dbc1358 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ bunx skills add expo/skills These skills send **anonymous** usage events so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. -- **Tracked in Claude Code:** when a skill's `SKILL.md` is read (`skill_read`) and when a skill drives its first action (`skill_activated`). +- **Tracked in Claude Code:** when a skill is invoked (`skill_invoked`) and when it drives its first action (`skill_activated`). - **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. - **Other agents (Codex, Cursor, …):** automatic tracking needs Claude Code hooks, so it's off there; feedback still works by running the bundled script. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index ad31745..43ed78a 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -2,11 +2,11 @@ "hooks": { "PostToolUse": [ { - "matcher": "Read", + "matcher": "Skill", "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_read --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 6d4d0b6..4ee92f8 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -34,7 +34,7 @@ effectively always present where these skills run. | Event | When | How | | --- | --- | --- | -| `skill_read` | A skill's `SKILL.md` is read | Plugin `Read` hook (Claude Code), scoped to this plugin | +| `skill_invoked` | A skill is invoked (the `Skill` tool runs) | Plugin `Skill` hook (Claude Code), scoped to this plugin | | `skill_activated` | A skill is active and the agent makes its first tool call | Per-skill `PostToolUse` hook (Claude Code), deduped per session | | `skill_feedback` | You submit feedback | Manual — the command below | @@ -69,7 +69,7 @@ keys, or tokens. ## Harness support -- **Claude Code** — `skill_read` and `skill_activated` are automatic via hooks. +- **Claude Code** — `skill_invoked` and `skill_activated` are automatic via hooks. - **Codex / Cursor / other agents** — no hook system, so automatic events do not fire. The skills still work fully; only `skill_feedback` is available (run the script directly). If a harness later exposes a per-tool hook adapter, the same scripts wire @@ -116,7 +116,7 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). ## PostHog event shape -- `event`: `skill_read` | `skill_activated` | `skill_feedback` +- `event`: `skill_invoked` | `skill_activated` | `skill_feedback` - `distinct_id`: `expo-skills-installation:` - `properties.source`: `expo-skills` - `properties.$process_person_profile`: `false` diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index ffd7e01..738df1c 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Submit skill lifecycle telemetry (skill_read, skill_activated) to PostHog. +// Submit skill lifecycle telemetry (skill_invoked, skill_activated) to PostHog. // // Runs from Claude Code hooks. Reads the hook payload from stdin (when present) // and never blocks: on any error it exits 0 under --quiet. @@ -25,7 +25,7 @@ const { sendToPosthog, } = require("./telemetry_common.js"); -const EVENTS = ["skill_read", "skill_activated"]; +const EVENTS = ["skill_invoked", "skill_activated"]; function parseArgs(argv) { const args = { @@ -68,34 +68,30 @@ function readHookInput() { } } -function hookFilePath(hookInput) { +// Claude Code loads skills via the Skill tool (NOT the Read tool), and its input +// carries the invoked skill name. Plugin skills may be namespaced (e.g. +// "expo:expo-observe") — take the final segment as the folder name. +function skillFromToolInput(hookInput) { const toolInput = hookInput.tool_input; if (!toolInput || typeof toolInput !== "object") return ""; - return String(toolInput.file_path || toolInput.path || "").trim(); + const raw = String(toolInput.skill || "").trim(); + return raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw; } -function skillFromFilePath(filePath) { - if (!filePath) return ""; - const parts = filePath.replace(/\\/g, "/").split("/").filter(Boolean); - if (parts[parts.length - 1] !== "SKILL.md") return ""; - for (let i = 0; i < parts.length; i++) { - if (parts[i] !== "skills") continue; - if (i + 2 < parts.length && parts[i + 2] === "SKILL.md") return parts[i + 1]; - if (i + 3 < parts.length && parts[i + 3] === "SKILL.md") return parts[i + 2]; - } - return ""; +function pluginRootFor(args) { + // Self-derive from this script's location (/skills/skill-feedback/scripts). + return args.pluginRoot || path.resolve(__dirname, "..", "..", ".."); } -// Only emit skill_read for files that live under this plugin, so we never -// track SKILL.md reads from unrelated plugins or projects. -function isUnderPluginRoot(filePath, pluginRoot) { - if (!pluginRoot) return true; - const resolve = (p) => { - try { return fs.realpathSync(path.resolve(p)); } catch { return path.resolve(p); } - }; - const file = resolve(filePath); - const root = resolve(pluginRoot); - return file === root || file.startsWith(root + path.sep); +// Only emit for skills that belong to THIS plugin, so we never track other +// plugins' or the user's own skills. Confirms /skills//SKILL.md exists. +function skillBelongsToPlugin(skill, pluginRoot) { + if (!skill || !pluginRoot) return false; + try { + return fs.existsSync(path.join(pluginRoot, "skills", skill, "SKILL.md")); + } catch { + return false; + } } function resolveEvent(args) { @@ -106,8 +102,8 @@ function resolveEvent(args) { function eventPayload(args, hookInput) { const eventName = resolveEvent(args); let skill = args.skill.trim(); - if (eventName === "skill_read" && skill === "auto") { - skill = skillFromFilePath(hookFilePath(hookInput)); + if (eventName === "skill_invoked" && skill === "auto") { + skill = skillFromToolInput(hookInput); } const agentHarness = args.agentHarness.trim() || detectHarness(); @@ -190,12 +186,11 @@ async function main(argv) { try { const eventName = resolveEvent(args); - if (eventName === "skill_read" && args.skill.trim() === "auto") { - const filePath = hookFilePath(hookInput); - if (!skillFromFilePath(filePath)) return 0; - // Default the plugin root to this script's location (/skills/skill-feedback/scripts). - const pluginRoot = args.pluginRoot || path.resolve(__dirname, "..", "..", ".."); - if (!isUnderPluginRoot(filePath, pluginRoot)) return 0; + if (eventName === "skill_invoked" && args.skill.trim() === "auto") { + const skill = skillFromToolInput(hookInput); + if (!skill) return 0; + // Only track this plugin's own skills. + if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; } if (eventName === "skill_activated" && !shouldSendActivation(args, hookInput, args.dryRun)) { From 3395c907c3391045db724753b3ac232447ec5234 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Fri, 5 Jun 2026 17:18:51 +0200 Subject: [PATCH 05/23] feat(expo): split skill_invoked by initiator (AI vs user) Track skill usage from both invocation paths, tagged with an `initiator`: - AI / model: PostToolUse `Skill` hook -> initiator=ai - user `/slash`: new UserPromptExpansion hook (reads command_name) -> initiator=user Verified against the binary + real `claude -p`: model invocation goes through the Skill tool (tool_input.skill), while user slash commands fire UserPromptExpansion with command_name (plugin skills namespaced "expo:expo-observe", stripped to the folder name). Both scoped to this plugin's skills; non-Expo slashes (e.g. /clear) are ignored. Adds `properties.initiator` to skill_invoked. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- plugins/expo/hooks/hooks.json | 13 ++++++++++- plugins/expo/skills/skill-feedback/SKILL.md | 3 ++- .../skill-feedback/scripts/skill-event.js | 22 ++++++++++++------- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dbc1358..83959f8 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ bunx skills add expo/skills These skills send **anonymous** usage events so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. -- **Tracked in Claude Code:** when a skill is invoked (`skill_invoked`) and when it drives its first action (`skill_activated`). +- **Tracked in Claude Code:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator`) — and when it drives its first action (`skill_activated`). - **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. - **Other agents (Codex, Cursor, …):** automatic tracking needs Claude Code hooks, so it's off there; feedback still works by running the bundled script. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index 43ed78a..8f6c254 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,18 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --initiator ai --quiet", + "timeout": 5 + } + ] + } + ], + "UserPromptExpansion": [ + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --initiator user --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 4ee92f8..7cf9281 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -34,7 +34,7 @@ effectively always present where these skills run. | Event | When | How | | --- | --- | --- | -| `skill_invoked` | A skill is invoked (the `Skill` tool runs) | Plugin `Skill` hook (Claude Code), scoped to this plugin | +| `skill_invoked` | A skill is invoked — by the **AI** (`Skill` tool) or a **user** `/slash` command | Plugin `Skill` + `UserPromptExpansion` hooks; tagged `initiator: ai`\|`user`, scoped to this plugin | | `skill_activated` | A skill is active and the agent makes its first tool call | Per-skill `PostToolUse` hook (Claude Code), deduped per session | | `skill_feedback` | You submit feedback | Manual — the command below | @@ -122,6 +122,7 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). - `properties.$process_person_profile`: `false` - `properties.skill`: skill folder name (e.g. `expo-deployment`) - `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` +- `properties.initiator` (on `skill_invoked`): `ai` (Claude invoked via the `Skill` tool) or `user` (a `/slash` command) - `properties.model_config`: model/config string when the harness exposes it, else `unknown` - `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) - `properties.installation_id_hash`: anonymous hash of the local random installation ID diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index 738df1c..a19b13d 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -35,6 +35,7 @@ function parseArgs(argv) { modelConfig: "unknown", context: [], pluginRoot: "", + initiator: "", dryRun: false, quiet: false, }; @@ -48,6 +49,7 @@ function parseArgs(argv) { case "--model-config": args.modelConfig = next(); break; case "--context": args.context.push(next()); break; case "--plugin-root": args.pluginRoot = next(); break; + case "--initiator": args.initiator = next(); break; case "--dry-run": args.dryRun = true; break; case "--quiet": args.quiet = true; break; default: break; // ignore unknown flags @@ -68,13 +70,16 @@ function readHookInput() { } } -// Claude Code loads skills via the Skill tool (NOT the Read tool), and its input -// carries the invoked skill name. Plugin skills may be namespaced (e.g. -// "expo:expo-observe") — take the final segment as the folder name. -function skillFromToolInput(hookInput) { +// Resolve the invoked skill name from either invocation path: +// - AI/model: the `Skill` tool's `tool_input.skill` +// - user: a `/slash` command via UserPromptExpansion's `command_name` +// Plugin skills may be namespaced (e.g. "expo:expo-observe") — keep the final segment. +function skillFromHook(hookInput) { const toolInput = hookInput.tool_input; - if (!toolInput || typeof toolInput !== "object") return ""; - const raw = String(toolInput.skill || "").trim(); + let raw = toolInput && typeof toolInput === "object" ? String(toolInput.skill || "").trim() : ""; + if (!raw && hookInput.expansion_type === "slash_command") { + raw = String(hookInput.command_name || "").trim(); + } return raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw; } @@ -103,7 +108,7 @@ function eventPayload(args, hookInput) { const eventName = resolveEvent(args); let skill = args.skill.trim(); if (eventName === "skill_invoked" && skill === "auto") { - skill = skillFromToolInput(hookInput); + skill = skillFromHook(hookInput); } const agentHarness = args.agentHarness.trim() || detectHarness(); @@ -135,6 +140,7 @@ function eventPayload(args, hookInput) { skill, agent_harness: agentHarness, model_config: modelConfig, + ...(args.initiator.trim() ? { initiator: args.initiator.trim() } : {}), ...platformProps(), ...identityProperties, }; @@ -187,7 +193,7 @@ async function main(argv) { const eventName = resolveEvent(args); if (eventName === "skill_invoked" && args.skill.trim() === "auto") { - const skill = skillFromToolInput(hookInput); + const skill = skillFromHook(hookInput); if (!skill) return 0; // Only track this plugin's own skills. if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; From 1a03c1b3f21e18ff8f58f5a9a927a18dc9e7e6da Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Sat, 6 Jun 2026 16:24:48 +0200 Subject: [PATCH 06/23] refactor(expo): simplify telemetry to skill_invoked + skill_feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that skill_invoked (AI Skill tool + user UserPromptExpansion, tagged with `initiator`) fully covers usage, drop the redundant machinery: - Remove the `skill_activated` event and the per-skill `PostToolUse` frontmatter hooks from all 16 skills (+ skill-feedback) — they fired at the same moment as skill_invoked and added no distinct signal. - Drop `model_config` (always "unknown") and `--context`/`parseContext` (unused). - skill-event.js loses the activation dedup markers; one plugin-level hooks.json does all auto-tracking; skills now carry only the feedback footer. Two events remain — skill_invoked {skill, initiator, install-hash, agent_harness, session-hash, os/arch} and skill_feedback — which answer: most-used skills, AI-vs-user, unique installs, frequency/retention, and quality. Verified both paths dry-run correctly and `claude plugin validate` passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 2 +- README.md | 2 +- plugins/expo/hooks/hooks.json | 4 +- plugins/expo/skills/add-app-clip/SKILL.md | 7 - .../expo/skills/building-native-ui/SKILL.md | 7 - .../expo/skills/eas-update-insights/SKILL.md | 7 - plugins/expo/skills/expo-api-routes/SKILL.md | 7 - plugins/expo/skills/expo-brownfield/SKILL.md | 7 - .../expo/skills/expo-cicd-workflows/SKILL.md | 7 - plugins/expo/skills/expo-deployment/SKILL.md | 7 - plugins/expo/skills/expo-dev-client/SKILL.md | 7 - plugins/expo/skills/expo-module/SKILL.md | 7 - plugins/expo/skills/expo-observe/SKILL.md | 7 - .../expo/skills/expo-tailwind-setup/SKILL.md | 7 - .../skills/expo-ui-jetpack-compose/SKILL.md | 7 - plugins/expo/skills/expo-ui-swift-ui/SKILL.md | 7 - .../expo/skills/native-data-fetching/SKILL.md | 7 - plugins/expo/skills/skill-feedback/SKILL.md | 16 +- .../skill-feedback/scripts/skill-event.js | 150 ++++-------------- .../skill-feedback/scripts/skill-feedback.js | 61 ++----- .../scripts/telemetry_common.js | 13 -- plugins/expo/skills/upgrading-expo/SKILL.md | 7 - plugins/expo/skills/use-dom/SKILL.md | 7 - 23 files changed, 55 insertions(+), 305 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2c9042e..c2bfe6c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,6 @@ Users install plugins from this marketplace: ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous** usage telemetry and collects skill feedback (details in the `skill-feedback` skill). When adding a skill, copy the `skill_activated` frontmatter hook and the `## Expo Skill Feedback` footer from any existing skill. +The `expo` plugin sends **anonymous** usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Usage tracking is automatic via the plugin's `hooks/hooks.json` (no per-skill hooks). When adding a skill, just copy the `## Expo Skill Feedback` footer from any existing skill. **Turn it off:** run `telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/README.md b/README.md index 83959f8..76fff63 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ bunx skills add expo/skills These skills send **anonymous** usage events so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. -- **Tracked in Claude Code:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator`) — and when it drives its first action (`skill_activated`). +- **Tracked in Claude Code:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator` = `ai` or `user`). - **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. - **Other agents (Codex, Cursor, …):** automatic tracking needs Claude Code hooks, so it's off there; feedback still works by running the bundled script. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index 8f6c254..af8c285 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --initiator ai --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator ai --quiet", "timeout": 5 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --event skill_invoked --initiator user --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator user --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index e1cb2f7..335120e 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -1,13 +1,6 @@ --- name: add-app-clip description: Add an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app. -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill add-app-clip --event skill_activated --quiet' - timeout: 5 --- # Add an App Clip to an Expo App diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 919a3ca..377b5f0 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -3,13 +3,6 @@ name: building-native-ui description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs. version: 1.0.1 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill building-native-ui --event skill_activated --quiet' - timeout: 5 --- # Expo UI Guidelines diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 089f26c..8d13755 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -4,13 +4,6 @@ description: "Check the health of published EAS Updates: crash rates, install/la version: 1.0.0 license: MIT allowed-tools: "Bash(eas *)" -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill eas-update-insights --event skill_activated --quiet' - timeout: 5 --- # EAS Update Insights diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 53f0884..2673500 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -3,13 +3,6 @@ name: expo-api-routes description: Guidelines for creating API routes in Expo Router with EAS Hosting version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-api-routes --event skill_activated --quiet' - timeout: 5 --- ## When to Use API Routes diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 62b394d..878354c 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -1,13 +1,6 @@ --- name: expo-brownfield description: Integrate Expo and React Native into an existing native iOS or Android app. Use when the user mentions brownfield, embedding React Native in a native app, AAR/XCFramework, or adding Expo to an existing Kotlin/Swift project. Covers both the isolated approach and the integrated approach. -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-brownfield --event skill_activated --quiet' - timeout: 5 --- # Expo Brownfield diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 7808b8c..38f081c 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -4,13 +4,6 @@ description: Helps understand and write EAS workflow YAML files for Expo project allowed-tools: "Read,Write,Bash(node:*)" version: 1.0.0 license: MIT License -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-cicd-workflows --event skill_activated --quiet' - timeout: 5 --- # EAS Workflows Skill diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 5171d81..71a5a43 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -3,13 +3,6 @@ name: expo-deployment description: Deploying Expo apps to iOS App Store, Android Play Store, web hosting, and API routes version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-deployment --event skill_activated --quiet' - timeout: 5 --- # Deployment diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index 9606773..b176bb4 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -3,13 +3,6 @@ name: expo-dev-client description: Build and distribute Expo development clients locally or via TestFlight version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-dev-client --event skill_activated --quiet' - timeout: 5 --- Use EAS Build to create development clients for testing native code changes on physical devices. Use this for creating custom Expo Go clients for testing branches of your app. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index d606894..be50f29 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -3,13 +3,6 @@ name: expo-module description: Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo. version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-module --event skill_activated --quiet' - timeout: 5 --- # Writing Expo Modules diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index f6a041f..ac6fcc6 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -3,13 +3,6 @@ name: expo-observe description: Use for anything related to EAS Observe — adding `expo-observe` to an Expo project (AppMetricsRoot/ObserveRoot HOC, markInteractive, the useObserve hook, and the Expo Router / React Navigation integrations for per-route metrics), querying via the EAS CLI (`eas observe:metrics-summary`, `observe:metrics`, `observe:routes`, `observe:events`, `observe:versions`), or interpreting the resulting metrics (cold/warm launch, TTR, TTI, navigation cold/warm TTR, update download, and the TTI frameRate params for triaging slow startups). version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-observe --event skill_activated --quiet' - timeout: 5 --- # EAS Observe diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 57da8ba..52846f0 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -3,13 +3,6 @@ name: expo-tailwind-setup description: Set up Tailwind CSS v4 in Expo with react-native-css and NativeWind v5 for universal styling version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-tailwind-setup --event skill_activated --quiet' - timeout: 5 --- # Tailwind CSS Setup for Expo with react-native-css diff --git a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md index 2310192..77fb26e 100644 --- a/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md +++ b/plugins/expo/skills/expo-ui-jetpack-compose/SKILL.md @@ -1,13 +1,6 @@ --- name: Expo UI Jetpack Compose description: "`@expo/ui/jetpack-compose` package lets you use Jetpack Compose Views and modifiers in your app." -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-jetpack-compose --event skill_activated --quiet' - timeout: 5 --- > The instructions in this skill apply to SDK 55 only. For other SDK versions, refer to the Expo UI Jetpack Compose docs for that version for the most accurate information. diff --git a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md index db90208..2b8a5b3 100644 --- a/plugins/expo/skills/expo-ui-swift-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui-swift-ui/SKILL.md @@ -1,13 +1,6 @@ --- name: Expo UI SwiftUI description: "`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app." -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill expo-ui-swift-ui --event skill_activated --quiet' - timeout: 5 --- > The instructions in this skill apply to SDK 55 only. For other SDK versions, refer to the Expo UI SwiftUI docs for that version for the most accurate information. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index 8b2ae41..c8ce2f0 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -3,13 +3,6 @@ name: native-data-fetching description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`). version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill native-data-fetching --event skill_activated --quiet' - timeout: 5 --- # Expo Networking diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 7cf9281..a7081b5 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -1,13 +1,6 @@ --- name: skill-feedback description: Submit feedback about Expo skills, and how the bundled usage telemetry works. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving. -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill skill-feedback --event skill_activated --quiet' - timeout: 5 --- # Skill Feedback @@ -35,7 +28,6 @@ effectively always present where these skills run. | Event | When | How | | --- | --- | --- | | `skill_invoked` | A skill is invoked — by the **AI** (`Skill` tool) or a **user** `/slash` command | Plugin `Skill` + `UserPromptExpansion` hooks; tagged `initiator: ai`\|`user`, scoped to this plugin | -| `skill_activated` | A skill is active and the agent makes its first tool call | Per-skill `PostToolUse` hook (Claude Code), deduped per session | | `skill_feedback` | You submit feedback | Manual — the command below | ## Submitting feedback @@ -61,15 +53,14 @@ node skill-feedback/scripts/skill-feedback.js \ --text "The skill should say which files it inspected before changing code." ``` -Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Optional `--context key=value` -(repeatable) adds small metadata. Add `--dry-run` to print the payload without sending. +Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Add `--dry-run` to print the payload without sending. **Never include** secrets, private data, source code, long prompts, stack traces, API keys, or tokens. ## Harness support -- **Claude Code** — `skill_invoked` and `skill_activated` are automatic via hooks. +- **Claude Code** — `skill_invoked` is automatic via hooks (the AI's `Skill` tool + user `/slash` commands). - **Codex / Cursor / other agents** — no hook system, so automatic events do not fire. The skills still work fully; only `skill_feedback` is available (run the script directly). If a harness later exposes a per-tool hook adapter, the same scripts wire @@ -116,14 +107,13 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). ## PostHog event shape -- `event`: `skill_invoked` | `skill_activated` | `skill_feedback` +- `event`: `skill_invoked` | `skill_feedback` - `distinct_id`: `expo-skills-installation:` - `properties.source`: `expo-skills` - `properties.$process_person_profile`: `false` - `properties.skill`: skill folder name (e.g. `expo-deployment`) - `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` - `properties.initiator` (on `skill_invoked`): `ai` (Claude invoked via the `Skill` tool) or `user` (a `/slash` command) -- `properties.model_config`: model/config string when the harness exposes it, else `unknown` - `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) - `properties.installation_id_hash`: anonymous hash of the local random installation ID - `properties.session_id_hash`: short hash only; raw session IDs are never sent diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index a19b13d..a7c0ec3 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -1,12 +1,13 @@ #!/usr/bin/env node -// Submit skill lifecycle telemetry (skill_invoked, skill_activated) to PostHog. +// Submit a `skill_invoked` event to PostHog. // -// Runs from Claude Code hooks. Reads the hook payload from stdin (when present) -// and never blocks: on any error it exits 0 under --quiet. +// Fires from two Claude Code hooks (see ../../../hooks/hooks.json): +// - PostToolUse[Skill] -> the AI invoked a skill (--initiator ai) +// - UserPromptExpansion -> a user ran a /slash command (--initiator user) +// Reads the hook payload from stdin; never blocks (exits 0 under --quiet). const crypto = require("crypto"); const fs = require("fs"); -const os = require("os"); const path = require("path"); const { @@ -20,36 +21,22 @@ const { maybeShowFirstRunNotice, shortHash, stableStringify, - parseContext, telemetryIdentity, sendToPosthog, } = require("./telemetry_common.js"); -const EVENTS = ["skill_invoked", "skill_activated"]; +const EVENT = "skill_invoked"; function parseArgs(argv) { - const args = { - skill: "", - event: "", - agentHarness: "", - modelConfig: "unknown", - context: [], - pluginRoot: "", - initiator: "", - dryRun: false, - quiet: false, - }; + const args = { skill: "", agentHarness: "", initiator: "", pluginRoot: "", dryRun: false, quiet: false }; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const next = () => argv[++i] || ""; switch (flag) { case "--skill": args.skill = next(); break; - case "--event": args.event = next(); break; case "--agent-harness": args.agentHarness = next(); break; - case "--model-config": args.modelConfig = next(); break; - case "--context": args.context.push(next()); break; - case "--plugin-root": args.pluginRoot = next(); break; case "--initiator": args.initiator = next(); break; + case "--plugin-root": args.pluginRoot = next(); break; case "--dry-run": args.dryRun = true; break; case "--quiet": args.quiet = true; break; default: break; // ignore unknown flags @@ -70,10 +57,10 @@ function readHookInput() { } } -// Resolve the invoked skill name from either invocation path: -// - AI/model: the `Skill` tool's `tool_input.skill` -// - user: a `/slash` command via UserPromptExpansion's `command_name` -// Plugin skills may be namespaced (e.g. "expo:expo-observe") — keep the final segment. +// The invoked skill name comes straight from Claude Code's hook payload: +// - AI: the Skill tool's `tool_input.skill` +// - user: UserPromptExpansion's `command_name` (slash command) +// Plugin skills are namespaced (e.g. "expo:expo-observe") — keep the final segment. function skillFromHook(hookInput) { const toolInput = hookInput.tool_input; let raw = toolInput && typeof toolInput === "object" ? String(toolInput.skill || "").trim() : ""; @@ -84,131 +71,58 @@ function skillFromHook(hookInput) { } function pluginRootFor(args) { - // Self-derive from this script's location (/skills/skill-feedback/scripts). + // Self-derive from this script's location: /skills/skill-feedback/scripts. return args.pluginRoot || path.resolve(__dirname, "..", "..", ".."); } -// Only emit for skills that belong to THIS plugin, so we never track other -// plugins' or the user's own skills. Confirms /skills//SKILL.md exists. +// Only emit for skills that belong to THIS plugin (so we never track other plugins' +// or the user's own skills). Confirms /skills//SKILL.md exists. function skillBelongsToPlugin(skill, pluginRoot) { if (!skill || !pluginRoot) return false; - try { - return fs.existsSync(path.join(pluginRoot, "skills", skill, "SKILL.md")); - } catch { - return false; - } -} - -function resolveEvent(args) { - if (args.event) return args.event; - throw new Error("--event is required"); + try { return fs.existsSync(path.join(pluginRoot, "skills", skill, "SKILL.md")); } + catch { return false; } } -function eventPayload(args, hookInput) { - const eventName = resolveEvent(args); - let skill = args.skill.trim(); - if (eventName === "skill_invoked" && skill === "auto") { - skill = skillFromHook(hookInput); - } - +function eventPayload(skill, args, hookInput) { const agentHarness = args.agentHarness.trim() || detectHarness(); - const modelConfig = args.modelConfig.trim() || "unknown"; - - if (!skill) throw new Error("--skill cannot be empty"); - if (!EVENTS.includes(eventName)) throw new Error(`--event must be one of: ${EVENTS.join(", ")}`); - + const initiator = args.initiator.trim(); const timestamp = new Date().toISOString(); const sessionIdHash = shortHash(hookInput.session_id); - const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { - createInstallation: !args.dryRun, - }); + const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { createInstallation: !args.dryRun }); const insertSource = stableStringify({ - skill, - event: eventName, - agent_harness: agentHarness, - model_config: modelConfig, - session_id_hash: sessionIdHash, - timestamp, + skill, event: EVENT, agent_harness: agentHarness, initiator, + session_id_hash: sessionIdHash, timestamp, }); const properties = { $process_person_profile: false, - $insert_id: `${eventName}:` + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), + $insert_id: `${EVENT}:` + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), source: SOURCE, schema_version: SCHEMA_VERSION, skill, agent_harness: agentHarness, - model_config: modelConfig, - ...(args.initiator.trim() ? { initiator: args.initiator.trim() } : {}), + ...(initiator ? { initiator } : {}), ...platformProps(), ...identityProperties, }; if (sessionIdHash) properties.session_id_hash = sessionIdHash; - const context = parseContext(args.context); - if (Object.keys(context).length) properties.context = context; - - return { - api_key: POSTHOG_PROJECT_API_KEY, - event: eventName, - distinct_id: distinctId, - timestamp, - properties, - }; -} - -function activationMarkerPath(skill, sessionIdHash) { - const markerId = crypto.createHash("sha256").update(`${skill}:${sessionIdHash}`).digest("hex").slice(0, 24); - return path.join(os.tmpdir(), `expo-skills-activated-${markerId}`); -} - -// skill_activated should fire once per skill per session. Use an atomic marker -// file so concurrent hook calls don't double-send. -function shouldSendActivation(args, hookInput, dryRun) { - if (resolveEvent(args) !== "skill_activated") return false; - const sessionIdHash = shortHash(hookInput.session_id); - if (!sessionIdHash) return true; - if (dryRun) return true; - - const marker = activationMarkerPath(args.skill.trim(), sessionIdHash); - try { - const fd = fs.openSync(marker, "wx", 0o600); - try { fs.writeFileSync(fd, new Date().toISOString()); } finally { fs.closeSync(fd); } - return true; - } catch (err) { - if (err && err.code === "EEXIST") return false; - return true; - } + return { api_key: POSTHOG_PROJECT_API_KEY, event: EVENT, distinct_id: distinctId, timestamp, properties }; } async function main(argv) { const args = parseArgs(argv); if (telemetryDisabled()) return 0; - if (!telemetryConfigured() && !args.dryRun) return 0; // no key set → fully inert + if (!telemetryConfigured() && !args.dryRun) return 0; // no key set -> fully inert const hookInput = readHookInput(); - let payload; - try { - const eventName = resolveEvent(args); - - if (eventName === "skill_invoked" && args.skill.trim() === "auto") { - const skill = skillFromHook(hookInput); - if (!skill) return 0; - // Only track this plugin's own skills. - if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; - } - - if (eventName === "skill_activated" && !shouldSendActivation(args, hookInput, args.dryRun)) { - if (!args.quiet) console.log(`skill-event: skipped duplicate activation event for ${args.skill}`); - return 0; - } + let skill = args.skill.trim(); + if (skill === "auto") skill = skillFromHook(hookInput); + if (!skill) return 0; // not a skill invocation + if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; // not one of ours - payload = eventPayload(args, hookInput); - } catch (err) { - if (!args.quiet) console.error(`skill-event: ${err.message}`); - return 2; - } + const payload = eventPayload(skill, args, hookInput); if (args.dryRun) { console.log(JSON.stringify({ ...payload, api_key: "phc_..." }, null, 2)); @@ -223,7 +137,7 @@ async function main(argv) { return args.quiet ? 0 : 1; } - if (!args.quiet) console.log(`sent ${payload.event}: ${payload.properties.skill}`); + if (!args.quiet) console.log(`sent ${EVENT}: ${payload.properties.skill} (${payload.properties.initiator || "?"})`); return 0; } diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js index e8c42af..cfb45dd 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -3,7 +3,7 @@ // // Usage: // node skill-feedback.js --skill --rating --text "..." \ -// [--agent-harness ] [--model-config ] [--context k=v] [--dry-run] +// [--agent-harness ] [--dry-run] const crypto = require("crypto"); @@ -17,7 +17,6 @@ const { platformProps, maybeShowFirstRunNotice, stableStringify, - parseContext, telemetryIdentity, sendToPosthog, } = require("./telemetry_common.js"); @@ -27,15 +26,7 @@ const RATINGS = ["useful", "confusing", "bug", "idea", "other"]; const MAX_FEEDBACK_CHARS = 4000; function parseArgs(argv) { - const args = { - skill: "", - rating: "", - text: "", - agentHarness: "", - modelConfig: "unknown", - context: [], - dryRun: false, - }; + const args = { skill: "", rating: "", text: "", agentHarness: "", dryRun: false }; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const next = () => argv[++i] || ""; @@ -44,8 +35,6 @@ function parseArgs(argv) { case "--rating": args.rating = next(); break; case "--text": args.text = next(); break; case "--agent-harness": args.agentHarness = next(); break; - case "--model-config": args.modelConfig = next(); break; - case "--context": args.context.push(next()); break; case "--dry-run": args.dryRun = true; break; default: break; } @@ -57,49 +46,32 @@ function eventPayload(args) { const feedback = args.text.trim().slice(0, MAX_FEEDBACK_CHARS); const skill = args.skill.trim(); const agentHarness = args.agentHarness.trim() || detectHarness(); - const modelConfig = args.modelConfig.trim() || "unknown"; if (!feedback) throw new Error("--text cannot be empty"); if (!skill) throw new Error("--skill cannot be empty"); if (!RATINGS.includes(args.rating)) throw new Error(`--rating must be one of: ${RATINGS.join(", ")}`); const timestamp = new Date().toISOString(); - const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { - createInstallation: !args.dryRun, - }); - - const insertSource = stableStringify({ - agent_harness: agentHarness, - model_config: modelConfig, - skill, - rating: args.rating, - feedback, - timestamp, - }); - - const properties = { - $process_person_profile: false, - $insert_id: "skill-feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), - source: SOURCE, - schema_version: SCHEMA_VERSION, - ...identityProperties, - agent_harness: agentHarness, - model_config: modelConfig, - ...platformProps(), - skill, - rating: args.rating, - feedback_text: feedback, - }; - - const context = parseContext(args.context); - if (Object.keys(context).length) properties.context = context; + const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { createInstallation: !args.dryRun }); + const insertSource = stableStringify({ agent_harness: agentHarness, skill, rating: args.rating, feedback, timestamp }); return { api_key: POSTHOG_PROJECT_API_KEY, event: EVENT_NAME, distinct_id: distinctId, timestamp, - properties, + properties: { + $process_person_profile: false, + $insert_id: "skill-feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), + source: SOURCE, + schema_version: SCHEMA_VERSION, + ...identityProperties, + agent_harness: agentHarness, + ...platformProps(), + skill, + rating: args.rating, + feedback_text: feedback, + }, }; } @@ -110,7 +82,6 @@ async function main(argv) { console.error("skill-feedback: telemetry is disabled (opt-out file or EXPO_SKILLS_TELEMETRY/DO_NOT_TRACK); nothing sent. Re-enable with telemetry.js --on."); return 0; } - if (!telemetryConfigured() && !args.dryRun) { console.error("skill-feedback: no PostHog key configured (placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or the key in telemetry_common.js."); return 0; diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js index 58a48fb..5d36957 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -126,18 +126,6 @@ function stableStringify(value) { return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}"; } -function parseContext(values) { - const context = {}; - for (const value of values) { - const idx = value.indexOf("="); - if (idx === -1) throw new Error("--context must be key=value"); - const key = value.slice(0, idx).trim(); - if (!key) throw new Error("--context key cannot be empty"); - context[key] = value.slice(idx + 1).trim().slice(0, 500); - } - return context; -} - function readInstallationId(create = true) { try { if (fs.existsSync(INSTALLATION_ID_PATH)) { @@ -226,7 +214,6 @@ module.exports = { maybeShowFirstRunNotice, shortHash, stableStringify, - parseContext, readInstallationId, installationIdHash, telemetryIdentity, diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index 0e381b8..2bc3922 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -3,13 +3,6 @@ name: upgrading-expo description: Guidelines for upgrading Expo SDK versions and fixing dependency issues version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill upgrading-expo --event skill_activated --quiet' - timeout: 5 --- ## References diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 4ff6a51..0b060ee 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -3,13 +3,6 @@ name: use-dom description: Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally. version: 1.0.0 license: MIT -hooks: - PostToolUse: - - matcher: "*" - hooks: - - type: command - command: 'sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js" --skill use-dom --event skill_activated --quiet' - timeout: 5 --- ## What are DOM Components? From 8574435bb477d72cb79e4511b76e5f95eba567cb Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Sat, 6 Jun 2026 17:42:45 +0200 Subject: [PATCH 07/23] docs(expo): correct Codex hook status + mark the enablement path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex now loads plugin hooks (openai/codex#17331 is closed) and exposes a PLUGIN_ROOT env var (≈ CLAUDE_PLUGIN_ROOT), but it still has no skill-invocation event: no Skill tool, UserPromptExpansion unimplemented, and skill hooks are on the parity roadmap (openai/codex#21753). So our matchers have nothing to bind to in Codex yet. - Update skill-feedback "Harness support" to this accurate state and document the minimal future enablement (a Codex hooks.json entry using ${PLUGIN_ROOT} + --initiator, plus one skillFromHook field). - Mark the skill-name extraction extension point in skill-event.js. Scripts are already harness-agnostic; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/expo/skills/skill-feedback/SKILL.md | 22 ++++++++++++++----- .../skill-feedback/scripts/skill-event.js | 8 ++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index a7081b5..95cc823 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -60,11 +60,23 @@ keys, or tokens. ## Harness support -- **Claude Code** — `skill_invoked` is automatic via hooks (the AI's `Skill` tool + user `/slash` commands). -- **Codex / Cursor / other agents** — no hook system, so automatic events do not fire. - The skills still work fully; only `skill_feedback` is available (run the script - directly). If a harness later exposes a per-tool hook adapter, the same scripts wire - straight into it. +The scripts are harness-agnostic (node/bun launcher, self-derived plugin root, harness +auto-detect). Only the `hooks/hooks.json` **wiring** is Claude-specific. + +- **Claude Code** — fully wired. `skill_invoked` fires via `hooks/hooks.json`: the AI's + `Skill` tool (`PostToolUse` → `initiator: ai`) and user `/slash` commands + (`UserPromptExpansion` → `initiator: user`). +- **Codex** — Codex now loads plugin `hooks/hooks.json` (openai/codex#17331, closed) and + exposes a `PLUGIN_ROOT` env var ≈ `${CLAUDE_PLUGIN_ROOT}`. But it has **no skill-invocation + event yet**: there is no `Skill` tool, `UserPromptExpansion` is unimplemented, and + dedicated skill hooks are still on the roadmap (openai/codex#21753) — so our matchers have + nothing to bind to in Codex today. **To enable when a skill event lands:** add a Codex entry + to `hooks/hooks.json` calling `run.sh`/`skill-event.js` with `${PLUGIN_ROOT}` and + `--initiator`, and add Codex's skill-name field to `skillFromHook()`. No other script change. +- **Cursor / others** — no plugin-hook system; `skill_feedback` (run the script directly) is + the only signal. + +Manual `skill_feedback` works in every agent. ## Turning it off diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index a7c0ec3..b637cef 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -57,9 +57,11 @@ function readHookInput() { } } -// The invoked skill name comes straight from Claude Code's hook payload: -// - AI: the Skill tool's `tool_input.skill` -// - user: UserPromptExpansion's `command_name` (slash command) +// Resolve the invoked skill name from the hook payload. Per-harness shapes: +// - Claude Code Skill tool: tool_input.skill (e.g. "expo:expo-observe") +// - Claude Code /slash: command_name (UserPromptExpansion) +// - Codex: add its skill-invocation field here once Codex ships a skill hook +// (openai/codex#21753). The rest of the pipeline is already harness-agnostic. // Plugin skills are namespaced (e.g. "expo:expo-observe") — keep the final segment. function skillFromHook(hookInput) { const toolInput = hookInput.tool_input; From 0dd91dbb1ed8bd82ea80a4684c9c6016f9a98ffb Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Sat, 6 Jun 2026 18:04:15 +0200 Subject: [PATCH 08/23] =?UTF-8?q?docs(expo):=20record=20in-depth=20Codex?= =?UTF-8?q?=20finding=20=E2=80=94=20skill=20telemetry=20not=20hook-observa?= =?UTF-8?q?ble?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against codex-cli 0.130.0 + openai/codex main + a real `codex exec` run with live hooks: none of Codex's 10 hook events are skill-aware and no payload carries the skill name (skills are injected as context, not a tool; the SkillInvoked fact is internal analytics, never exposed to the hooks crate). Plugin-bundled hooks are also off by default (plugin_hooks, under development). Codex does alias CLAUDE_PLUGIN_ROOT, so our paths already resolve — enabling Codex is a tiny add once it ships a skill hook event + stable plugin_hooks. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/expo/skills/skill-feedback/SKILL.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 95cc823..17b4049 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -66,13 +66,15 @@ auto-detect). Only the `hooks/hooks.json` **wiring** is Claude-specific. - **Claude Code** — fully wired. `skill_invoked` fires via `hooks/hooks.json`: the AI's `Skill` tool (`PostToolUse` → `initiator: ai`) and user `/slash` commands (`UserPromptExpansion` → `initiator: user`). -- **Codex** — Codex now loads plugin `hooks/hooks.json` (openai/codex#17331, closed) and - exposes a `PLUGIN_ROOT` env var ≈ `${CLAUDE_PLUGIN_ROOT}`. But it has **no skill-invocation - event yet**: there is no `Skill` tool, `UserPromptExpansion` is unimplemented, and - dedicated skill hooks are still on the roadmap (openai/codex#21753) — so our matchers have - nothing to bind to in Codex today. **To enable when a skill event lands:** add a Codex entry - to `hooks/hooks.json` calling `run.sh`/`skill-event.js` with `${PLUGIN_ROOT}` and - `--initiator`, and add Codex's skill-name field to `skillFromHook()`. No other script change. +- **Codex** — not wired, and blocked on Codex itself (verified against codex-cli 0.130.0 + + `openai/codex` main): its 10 hook events (`PostToolUse`, `UserPromptSubmit`, `SessionStart`, + …) include **none for skills**, and no payload carries the skill name — skills are injected + as `` context (not invoked as a tool), and Codex's `SkillInvoked` is internal + analytics never exposed to hooks. Plugin-bundled hooks are also **off by default** + (`plugin_hooks`, under development), and user/project hooks need a trust approval. Codex + *does* alias `CLAUDE_PLUGIN_ROOT`, so our paths already resolve — so when Codex ships a skill + hook event/field and stabilizes `plugin_hooks`, enabling us is a tiny add (one hooks entry + + one `skillFromHook` field). Until then, `skill_feedback` (run the script) is the Codex signal. - **Cursor / others** — no plugin-hook system; `skill_feedback` (run the script directly) is the only signal. From 5a9cf78ead6b737aeea7128255766457b3df4f60 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Sat, 6 Jun 2026 18:09:21 +0200 Subject: [PATCH 09/23] feat(expo): make skill_invoked resolver payload-tolerant (Codex-ready) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build it so Codex lights up automatically once it reaches hook parity (openai/codex#21753) + enables plugin_hooks — with no rework from us: - skillFromHook() now reads the skill name from any plausible payload field (tool_input.skill / tool_input.skill_name / command_name / top-level skill / skill_name). Strict skillBelongsToPlugin() scoping keeps this safe even though it's permissive (non-skills are dropped). - We already use Claude's hook event names (Skill, UserPromptExpansion) and ${CLAUDE_PLUGIN_ROOT}, which Codex mirrors/aliases — so the existing hooks.json fires in a parity Codex unchanged. Claude paths verified unchanged; non-skill payloads still skipped; plugin validates. Codex still can't fire yet (no skill-aware hook event; plugin_hooks off by default) — but our side needs zero further change for the parity path. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/expo/skills/skill-feedback/SKILL.md | 19 ++++++++------- .../skill-feedback/scripts/skill-event.js | 24 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 17b4049..e906949 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -66,15 +66,16 @@ auto-detect). Only the `hooks/hooks.json` **wiring** is Claude-specific. - **Claude Code** — fully wired. `skill_invoked` fires via `hooks/hooks.json`: the AI's `Skill` tool (`PostToolUse` → `initiator: ai`) and user `/slash` commands (`UserPromptExpansion` → `initiator: user`). -- **Codex** — not wired, and blocked on Codex itself (verified against codex-cli 0.130.0 + - `openai/codex` main): its 10 hook events (`PostToolUse`, `UserPromptSubmit`, `SessionStart`, - …) include **none for skills**, and no payload carries the skill name — skills are injected - as `` context (not invoked as a tool), and Codex's `SkillInvoked` is internal - analytics never exposed to hooks. Plugin-bundled hooks are also **off by default** - (`plugin_hooks`, under development), and user/project hooks need a trust approval. Codex - *does* alias `CLAUDE_PLUGIN_ROOT`, so our paths already resolve — so when Codex ships a skill - hook event/field and stabilizes `plugin_hooks`, enabling us is a tiny add (one hooks entry + - one `skillFromHook` field). Until then, `skill_feedback` (run the script) is the Codex signal. +- **Codex** — built to light up **automatically** when Codex catches up, no rework from us. + We use Claude's hook event names (`Skill`, `UserPromptExpansion`), Codex **aliases + `CLAUDE_PLUGIN_ROOT`**, and `skillFromHook()` tolerates any payload field for the skill name + — so the existing `hooks/hooks.json` fires in a parity Codex unchanged. It just can't fire + *yet*: verified against codex-cli 0.130.0 + `openai/codex` main, none of Codex's 10 hook + events are skill-aware (the `SkillInvoked` fact is internal analytics, not hook-exposed), + `UserPromptExpansion` isn't implemented, and plugin-bundled hooks are **off by default** + (`plugin_hooks`, under development). So Codex is gated on OpenAI reaching skill-hook parity + (openai/codex#21753) + enabling `plugin_hooks`; if it instead ships a *native* skill event, + the only addition is one matcher. Until then, `skill_feedback` is the Codex signal. - **Cursor / others** — no plugin-hook system; `skill_feedback` (run the script directly) is the only signal. diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index b637cef..e3e5cb8 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -57,18 +57,22 @@ function readHookInput() { } } -// Resolve the invoked skill name from the hook payload. Per-harness shapes: -// - Claude Code Skill tool: tool_input.skill (e.g. "expo:expo-observe") -// - Claude Code /slash: command_name (UserPromptExpansion) -// - Codex: add its skill-invocation field here once Codex ships a skill hook -// (openai/codex#21753). The rest of the pipeline is already harness-agnostic. +// Resolve the invoked skill name from the hook payload. The name arrives in different +// fields across harnesses and payload shapes, so check every plausible location — the +// strict skillBelongsToPlugin() scoping downstream keeps this safe even when permissive +// (anything that isn't really one of our skills is dropped): +// - Claude Code Skill tool: tool_input.skill (e.g. "expo:expo-observe") +// - Claude Code /slash command: command_name (UserPromptExpansion) +// - Codex / future skill hooks: tool_input.skill_name, top-level skill / skill_name +// Codex mirrors Claude's hook event names (parity tracker openai/codex#21753) and aliases +// CLAUDE_PLUGIN_ROOT, so the existing hooks fire there unchanged once Codex enables plugin +// hooks — this resolver just has to tolerate whatever field the payload uses for the name. // Plugin skills are namespaced (e.g. "expo:expo-observe") — keep the final segment. function skillFromHook(hookInput) { - const toolInput = hookInput.tool_input; - let raw = toolInput && typeof toolInput === "object" ? String(toolInput.skill || "").trim() : ""; - if (!raw && hookInput.expansion_type === "slash_command") { - raw = String(hookInput.command_name || "").trim(); - } + const ti = hookInput && typeof hookInput.tool_input === "object" && hookInput.tool_input ? hookInput.tool_input : {}; + const raw = String( + ti.skill || ti.skill_name || hookInput.command_name || hookInput.skill || hookInput.skill_name || "" + ).trim(); return raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw; } From 0949797ebc2af012bcebc7ca67bfdce53524bdf4 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Mon, 8 Jun 2026 16:51:55 +0200 Subject: [PATCH 10/23] chore(expo): apply code-review polish to skill telemetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-functional hardening from the multi-lens code review — no behavior change to the happy path: - skill-event.js: strip a leading "/" from slash-command skill payloads before namespacing; trim the AI-path hook timeout 5s -> 3s. - skill-feedback.js: fix $insert_id prefix "skill-feedback:" -> "skill_feedback:" to match the event name; drop the unused maybeShowFirstRunNotice import/call (feedback is explicit consent, so the first-run notice was misleading here). - hooks.json: pass --plugin-root "${CLAUDE_PLUGIN_ROOT}" explicitly on both hooks so plugin-scoping never depends on path inference. - docs/comments: correct run.sh usage line, skill-feedback SKILL.md example paths (${CLAUDE_SKILL_DIR}/scripts/...) and Codex example (use-dom), telemetry_common stableStringify comment, and AGENTS.md opt-out guidance. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/hooks/hooks.json | 4 ++-- plugins/expo/skills/skill-feedback/SKILL.md | 6 +++--- plugins/expo/skills/skill-feedback/scripts/run.sh | 2 +- plugins/expo/skills/skill-feedback/scripts/skill-event.js | 4 ++-- .../expo/skills/skill-feedback/scripts/skill-feedback.js | 4 +--- .../expo/skills/skill-feedback/scripts/telemetry_common.js | 3 ++- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f20d364..0f47972 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,4 +257,4 @@ codex plugin marketplace add expo/skills --ref main The `expo` plugin sends **anonymous** usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Usage tracking is automatic via the plugin's `hooks/hooks.json` (no per-skill hooks). When adding a skill, just copy the `## Expo Skill Feedback` footer from any existing skill. -**Turn it off:** run `telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. +**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index af8c285..d6cf59e 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator ai --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator user --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index e906949..60089d5 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -37,8 +37,8 @@ Keep it to 1-3 short sentences, name the skill, and prefer concrete observations **Claude Code** (`${CLAUDE_SKILL_DIR}` resolves to the active skill's folder): ```bash -sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" \ - "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" \ +sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" \ + "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" \ --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` @@ -49,7 +49,7 @@ explicit override): ```bash node skill-feedback/scripts/skill-feedback.js \ - --skill deslop --rating confusing --agent-harness codex \ + --skill use-dom --rating confusing --agent-harness codex \ --text "The skill should say which files it inspected before changing code." ``` diff --git a/plugins/expo/skills/skill-feedback/scripts/run.sh b/plugins/expo/skills/skill-feedback/scripts/run.sh index 6ca9d44..98f4a08 100644 --- a/plugins/expo/skills/skill-feedback/scripts/run.sh +++ b/plugins/expo/skills/skill-feedback/scripts/run.sh @@ -6,7 +6,7 @@ # script with it. If neither is installed, it exits 0 and does nothing, so a # missing runtime never blocks a tool call. # -# Usage: sh run.js.sh [args...] +# Usage: sh run.sh [args...] script="$1" [ -n "$script" ] || exit 0 diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index e3e5cb8..db63fb1 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -72,7 +72,7 @@ function skillFromHook(hookInput) { const ti = hookInput && typeof hookInput.tool_input === "object" && hookInput.tool_input ? hookInput.tool_input : {}; const raw = String( ti.skill || ti.skill_name || hookInput.command_name || hookInput.skill || hookInput.skill_name || "" - ).trim(); + ).trim().replace(/^\//, ""); // tolerate a leading "/" from slash-command payloads return raw.includes(":") ? raw.slice(raw.lastIndexOf(":") + 1) : raw; } @@ -137,7 +137,7 @@ async function main(argv) { maybeShowFirstRunNotice(); try { - await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 5000 }); + await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 3000 }); } catch (err) { if (!args.quiet) console.error(`skill-event: ${err.message}`); return args.quiet ? 0 : 1; diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js index cfb45dd..db0351f 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -15,7 +15,6 @@ const { telemetryConfigured, detectHarness, platformProps, - maybeShowFirstRunNotice, stableStringify, telemetryIdentity, sendToPosthog, @@ -62,7 +61,7 @@ function eventPayload(args) { timestamp, properties: { $process_person_profile: false, - $insert_id: "skill-feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), + $insert_id: "skill_feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), source: SOURCE, schema_version: SCHEMA_VERSION, ...identityProperties, @@ -100,7 +99,6 @@ async function main(argv) { return 0; } - maybeShowFirstRunNotice(); try { await sendToPosthog(payload, { userAgent: "expo-skills/skill-feedback", timeoutMs: 10000 }); } catch (err) { diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js index 5d36957..490c076 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -118,7 +118,8 @@ function shortHash(value, length = 16) { return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length); } -// Deterministic JSON with sorted keys, so $insert_id is stable across runs. +// Deterministic JSON with sorted keys for a consistent $insert_id hash. (Events are +// still unique per invocation — callers include `timestamp` in the hashed input.) function stableStringify(value) { if (value === null || typeof value !== "object") return JSON.stringify(value); if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"; From 5c690e320436714be6b4c563cb96491fcc36a74a Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Tue, 16 Jun 2026 23:04:15 +0200 Subject: [PATCH 11/23] fix(expo): address skill-telemetry PR review (Codex, latency, consent, UX) Responds to review feedback from @davidmokos and @Kudo on #71. Auto-path latency (@Kudo): the PostToolUse hook no longer blocks the turn on the network. New scripts/skill-event.sh detaches the send into its own session (setsid -> perl POSIX::setsid -> nohup) and returns in ~20ms. Because a backgrounded process's stdin is /dev/null (POSIX), the wrapper reads the hook payload synchronously into an mktemp file and passes it via --hook-input-file; skill-event.js reads then unlinks it (before the telemetry-off guards, so it's cleaned up even when nothing is sent). If mktemp is unavailable it bails rather than write the payload to a predictable, world-readable /tmp path. Footer UX (@davidmokos): new scripts/skill-feedback.sh wrapper bundles run.sh + the script path, so all 16 SKILL.md footers shrink to one line. Codex (@davidmokos): corrected the stale "will light up when Codex catches up" narrative. Verified on codex-cli 0.138: plugin_hooks is a *removed* feature, and Codex runs skills by reading SKILL.md directly (no Skill tool to hook), so a plugin can't ship skill-usage hooks today. Manual skill_feedback is the Codex signal. Detail in references/telemetry.md. Honesty / consent (@Kudo): removed the false "fully inert until a key is configured" claims (a real public phc_ key ships, so telemetry is ON by default) from the scripts and AGENTS.md; the guard now only inerts a key-stripped fork. The one-time notice now fires only on visible (non-quiet) runs, so the silenced detached hook can't burn its marker unseen. Docs (@davidmokos, @Kudo): fixed the misleading "harness is auto-detected" wording (only claude-code + codex are detected); split automatic skill_invoked vs explicit skill_feedback and documented that opt-out silences both; relocated maintainer-facing config + event-shape detail to references/telemetry.md to keep SKILL.md lean; documented the prompt/chat-history non-capture stance and the Windows sh limitation. Hardening: skill name must be a single kebab segment (blocks path traversal from a malformed payload reaching path.join or the event property). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/hooks/hooks.json | 4 +- plugins/expo/skills/add-app-clip/SKILL.md | 4 +- .../expo/skills/building-native-ui/SKILL.md | 4 +- .../expo/skills/eas-update-insights/SKILL.md | 4 +- plugins/expo/skills/expo-api-routes/SKILL.md | 4 +- plugins/expo/skills/expo-brownfield/SKILL.md | 4 +- .../expo/skills/expo-cicd-workflows/SKILL.md | 4 +- plugins/expo/skills/expo-deployment/SKILL.md | 4 +- plugins/expo/skills/expo-dev-client/SKILL.md | 4 +- plugins/expo/skills/expo-module/SKILL.md | 4 +- plugins/expo/skills/expo-observe/SKILL.md | 4 +- .../expo/skills/expo-tailwind-setup/SKILL.md | 4 +- plugins/expo/skills/expo-ui/SKILL.md | 4 +- .../expo/skills/native-data-fetching/SKILL.md | 4 +- plugins/expo/skills/skill-feedback/SKILL.md | 125 +++++++----------- .../skill-feedback/references/telemetry.md | 108 +++++++++++++++ .../skill-feedback/scripts/skill-event.js | 42 ++++-- .../skill-feedback/scripts/skill-event.sh | 38 ++++++ .../skill-feedback/scripts/skill-feedback.js | 2 +- .../skill-feedback/scripts/skill-feedback.sh | 11 ++ .../skill-feedback/scripts/telemetry.js | 2 +- .../scripts/telemetry_common.js | 6 +- plugins/expo/skills/upgrading-expo/SKILL.md | 4 +- plugins/expo/skills/use-dom/SKILL.md | 4 +- 25 files changed, 274 insertions(+), 126 deletions(-) create mode 100644 plugins/expo/skills/skill-feedback/references/telemetry.md create mode 100644 plugins/expo/skills/skill-feedback/scripts/skill-event.sh create mode 100644 plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh diff --git a/AGENTS.md b/AGENTS.md index 0f47972..2bdf05c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous** usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Usage tracking is automatic via the plugin's `hooks/hooks.json` (no per-skill hooks). When adding a skill, just copy the `## Expo Skill Feedback` footer from any existing skill. +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, just copy the `## Expo Skill Feedback` footer from any existing skill. **Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index d6cf59e..a6d534f 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.sh\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh\" \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.js\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.sh\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index 335120e..f084d86 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -285,7 +285,7 @@ See [./references/native-module.md](./references/native-module.md) for the Swift If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill add-app-clip --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 8dc2b66..b9332d0 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -326,7 +326,7 @@ export default function Layout({ segment }) { If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill building-native-ui --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 8d13755..b65835e 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -233,7 +233,7 @@ Human-readable group details plus 30 days of launches/failures per platform — If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill eas-update-insights --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 2673500..bdff58c 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -373,7 +373,7 @@ export async function GET(request: Request) { If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-api-routes --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 878354c..849df30 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -59,7 +59,7 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-brownfield --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 148dee4..2be454b 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -97,7 +97,7 @@ When users ask about available options (job types, triggers, runner types, etc.) If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-cicd-workflows --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 71a5a43..471a9ef 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -195,7 +195,7 @@ eas submit:list If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-deployment --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index b176bb4..202aabd 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -169,7 +169,7 @@ eas update If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-dev-client --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index be50f29..333c41c 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -146,7 +146,7 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-module --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index ac6fcc6..fac326a 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -34,7 +34,7 @@ The three reference files in `./references/` cover the three things people typic If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-observe --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 52846f0..11ec8aa 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -485,7 +485,7 @@ type Props = React.ComponentProps & { className?: string }; If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-tailwind-setup --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index abd63e4..bca48e4 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -49,7 +49,7 @@ references/ If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill expo-ui --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index c8ce2f0..af3e508 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -512,7 +512,7 @@ User: "How do I load data for a page in Expo Router?" If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill native-data-fetching --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 60089d5..4c97eb4 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -5,30 +5,34 @@ description: Submit feedback about Expo skills, and how the bundled usage teleme # Skill Feedback -The shared feedback + usage-telemetry helper for the Expo skills. Every skill ends -with an `Expo Skill Feedback` footer that points here. It sends anonymous events to -PostHog so the Expo team can see how skills are used and where they need work. +The shared telemetry + feedback helper for the Expo skills. It powers two separate +signals, both **anonymous** and sent to PostHog so the Expo team can see how skills are +used and where they fall short: -No setup, no env vars, and no user token are required. On first send it creates one -random local installation ID at `~/.expo-skills/installation-id` and sends only a -short **hash** of it. Hostnames, usernames, emails, file paths, source code, prompts, -tool inputs, and machine identifiers are **never** sent. Telemetry is **skipped -automatically in CI**, and the first real send prints a one-time notice to stderr. +- **Automatic usage** (`skill_invoked`) — emitted by plugin hooks when a skill runs. No + manual step. **On by default.** +- **Explicit feedback** (`skill_feedback`) — a rating + short note you (or the agent, with + the user's OK) send deliberately by running the command below. + +Both send only a short **hash** of a random local installation ID +(`~/.expo-skills/installation-id`, created on first send). Hostnames, usernames, emails, +file paths, source code, **prompts, and chat history** are **never** sent. Telemetry is +**skipped automatically in CI**, and a direct (non-quiet) run prints a one-time stderr +notice on first send. To turn everything off, see *Turning it off* below. ## Runtime -Scripts are plain JavaScript and run under **Node or Bun** — both ship the built-ins -they use (`crypto`, `https`, `fs`). `scripts/run.sh` picks whichever is installed -(`node`, then `bun`) and does nothing if neither is found, so a missing runtime never -blocks a tool call. Node/Bun is a hard requirement for Expo development, so it is -effectively always present where these skills run. +Scripts are plain JavaScript and run under **Node or Bun** — both ship the built-ins they +use (`crypto`, `https`, `fs`). `scripts/run.sh` picks whichever is installed (`node`, then +`bun`) and does nothing if neither is found, so a missing runtime never blocks a tool call. +Node/Bun is a hard requirement for Expo development, so it's effectively always present. -## Events +## The two signals -| Event | When | How | +| Event | Trigger | Mechanism | | --- | --- | --- | -| `skill_invoked` | A skill is invoked — by the **AI** (`Skill` tool) or a **user** `/slash` command | Plugin `Skill` + `UserPromptExpansion` hooks; tagged `initiator: ai`\|`user`, scoped to this plugin | -| `skill_feedback` | You submit feedback | Manual — the command below | +| `skill_invoked` | A skill runs — by the **AI** (`Skill` tool) or a **user** `/slash` command | Automatic, via the plugin's `hooks/hooks.json`; tagged `initiator: ai`\|`user`, scoped to this plugin. **Claude Code only** — see *Harness support* | +| `skill_feedback` | You deliberately send a rating + note | Manual — the command below | ## Submitting feedback @@ -37,19 +41,18 @@ Keep it to 1-3 short sentences, name the skill, and prefer concrete observations **Claude Code** (`${CLAUDE_SKILL_DIR}` resolves to the active skill's folder): ```bash -sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" \ - "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" \ +sh "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.sh" \ --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` -**Other agents (Codex, etc.)** — run the same bundled script with `node` or `bun`, -using its path on disk (the harness auto-detects; `--agent-harness` below is an -explicit override): +**Other agents (Codex, Cursor, …)** — run the bundled script directly with `node` or +`bun`. The harness is auto-detected only for Claude Code and Codex; on any other agent +pass `--agent-harness` so the event is labelled correctly: ```bash node skill-feedback/scripts/skill-feedback.js \ - --skill use-dom --rating confusing --agent-harness codex \ + --skill use-dom --rating confusing --agent-harness cursor \ --text "The skill should say which files it inspected before changing code." ``` @@ -58,35 +61,12 @@ Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Add `--dry-run` to print **Never include** secrets, private data, source code, long prompts, stack traces, API keys, or tokens. -## Harness support - -The scripts are harness-agnostic (node/bun launcher, self-derived plugin root, harness -auto-detect). Only the `hooks/hooks.json` **wiring** is Claude-specific. - -- **Claude Code** — fully wired. `skill_invoked` fires via `hooks/hooks.json`: the AI's - `Skill` tool (`PostToolUse` → `initiator: ai`) and user `/slash` commands - (`UserPromptExpansion` → `initiator: user`). -- **Codex** — built to light up **automatically** when Codex catches up, no rework from us. - We use Claude's hook event names (`Skill`, `UserPromptExpansion`), Codex **aliases - `CLAUDE_PLUGIN_ROOT`**, and `skillFromHook()` tolerates any payload field for the skill name - — so the existing `hooks/hooks.json` fires in a parity Codex unchanged. It just can't fire - *yet*: verified against codex-cli 0.130.0 + `openai/codex` main, none of Codex's 10 hook - events are skill-aware (the `SkillInvoked` fact is internal analytics, not hook-exposed), - `UserPromptExpansion` isn't implemented, and plugin-bundled hooks are **off by default** - (`plugin_hooks`, under development). So Codex is gated on OpenAI reaching skill-hook parity - (openai/codex#21753) + enabling `plugin_hooks`; if it instead ships a *native* skill event, - the only addition is one matcher. Until then, `skill_feedback` is the Codex signal. -- **Cursor / others** — no plugin-hook system; `skill_feedback` (run the script directly) is - the only signal. - -Manual `skill_feedback` works in every agent. - ## Turning it off -Telemetry is anonymous and on by default. The reliable, launch-independent way to opt -out is the bundled toggle — it writes `~/.expo-skills/opt-out`, which every script checks -before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run -it directly: +Telemetry is anonymous and **on by default**. Opting out silences **both** signals — +automatic `skill_invoked` and explicit `skill_feedback`. The reliable, launch-independent +switch is the bundled toggle; it writes `~/.expo-skills/opt-out`, which every script checks +before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run it: ```bash # Claude Code: @@ -95,9 +75,9 @@ sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/telemetry.j node skill-feedback/scripts/telemetry.js --off # --on to re-enable, --status to check ``` -For CI or a global opt-out, set an env var instead. Env vars are convenient but only -apply where they're exported and don't always reach hook subprocesses, so prefer the -toggle for a persistent opt-out: +For CI or a global opt-out, set an env var instead (env vars are convenient but only apply +where exported and don't always reach hook subprocesses, so prefer the toggle for a +persistent opt-out): ```bash export EXPO_SKILLS_TELEMETRY=0 # or: false / off / no @@ -107,32 +87,21 @@ export DO_NOT_TRACK=1 # cross-tool standard, https://consoledonottrac When disabled, no installation ID is created and nothing is sent. Uninstalling the plugin removes everything. -## Configuring the PostHog project - -The project key lives in `scripts/telemetry_common.js`. It is a **write-only, public** -ingestion key (`phc_...`) — the same kind embedded in browser snippets — so it is safe -to commit. Override it per environment (e.g. a staging project) without editing the file: - -```bash -export EXPO_SKILLS_POSTHOG_KEY="phc_your_project_key" -``` +## Harness support -Only ever use a *project* key (`phc_...`) here. Never a *personal* API key (`phx_...`) — -those are secret and used only for reading/querying (e.g. the PostHog MCP). +Only the automatic `skill_invoked` path depends on the harness; manual `skill_feedback` +works everywhere. -## PostHog event shape +- **Claude Code** — fully wired. `skill_invoked` fires from `hooks/hooks.json` (the AI's + `Skill` tool → `initiator: ai`; user `/slash` commands → `initiator: user`). +- **Codex** — automatic tracking can't be shipped from the plugin today: in codex-cli + 0.138 `plugin_hooks` is a *removed* feature, and Codex runs a skill by reading its + `SKILL.md` directly (there is no `Skill` tool to hook). Manual `skill_feedback` is the + Codex signal. +- **Cursor / others** — no plugin-hook system; manual `skill_feedback` only. -- `event`: `skill_invoked` | `skill_feedback` -- `distinct_id`: `expo-skills-installation:` -- `properties.source`: `expo-skills` -- `properties.$process_person_profile`: `false` -- `properties.skill`: skill folder name (e.g. `expo-deployment`) -- `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` -- `properties.initiator` (on `skill_invoked`): `ai` (Claude invoked via the `Skill` tool) or `user` (a `/slash` command) -- `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) -- `properties.installation_id_hash`: anonymous hash of the local random installation ID -- `properties.session_id_hash`: short hash only; raw session IDs are never sent -- `skill_feedback` adds `properties.rating` and `properties.feedback_text` +See `./references/telemetry.md` for the event shape, PostHog key configuration, the Codex +details, and the prompt-capture and Windows notes. --- @@ -140,7 +109,7 @@ those are secret and used only for reading/querying (e.g. the PostHog MCP). If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.js" --skill skill-feedback --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/references/telemetry.md b/plugins/expo/skills/skill-feedback/references/telemetry.md new file mode 100644 index 0000000..8827708 --- /dev/null +++ b/plugins/expo/skills/skill-feedback/references/telemetry.md @@ -0,0 +1,108 @@ +# Telemetry internals (maintainer reference) + +Background detail for the `skill-feedback` scripts. The day-to-day surface — what's +sent, how to submit feedback, and how to turn it off — lives in `../SKILL.md`. This +file is for people changing or auditing the telemetry, not for the agent at runtime. + +## Configuring the PostHog project + +The project key lives in `scripts/telemetry_common.js`. It is a **write-only, public** +ingestion key (`phc_...`) — the same kind embedded in browser snippets — so it is safe +to commit, and it is what makes telemetry **on by default**. Override it per environment +(e.g. a staging project) without editing the file: + +```bash +export EXPO_SKILLS_POSTHOG_KEY="phc_your_project_key" +``` + +Only ever use a *project* key (`phc_...`) here. Never a *personal* API key (`phx_...`) — +those are secret and used only for reading/querying (e.g. the PostHog MCP). If the key is +stripped to empty or `phc_REPLACE_ME` (e.g. a fork), `telemetryConfigured()` returns false +and the scripts go inert — no installation ID, no network. + +## Event shape + +- `event`: `skill_invoked` | `skill_feedback` +- `distinct_id`: `expo-skills-installation:` +- `properties.source`: `expo-skills` +- `properties.$process_person_profile`: `false` +- `properties.skill`: skill folder name (e.g. `expo-deployment`) +- `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` +- `properties.initiator` (on `skill_invoked`): `ai` (invoked via the `Skill` tool) or `user` (a `/slash` command) +- `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) +- `properties.installation_id_hash`: anonymous hash of the local random installation ID +- `properties.session_id_hash`: short hash only; raw session IDs are never sent +- `skill_feedback` adds `properties.rating` and `properties.feedback_text` + +## Why prompts / chat history are never captured + +Richer context (the prompt that triggered a skill, surrounding chat) would help study how +to improve a skill, but capturing it would break the anonymity guarantee that justifies +on-by-default collection. So the automatic `skill_invoked` event carries **only the skill +name** and the anonymous properties above — never prompt text, code, file paths, or tool +inputs. If a maintainer wants qualitative context, the path is **explicit and +user-approved**: a human (or an agent with the user's consent) puts a short, scrubbed note +in the `--text` field of `skill_feedback`. Automatic prompt/history capture is intentionally +off the table. + +## How the auto path stays off the critical path + +`skill-event.sh` is launched from a Claude Code hook, which waits for the command to exit. +To avoid adding the POST's latency to every turn, the wrapper detaches the send into its +own session (`setsid`, falling back to `perl POSIX::setsid`, then `nohup`) and returns +immediately; the child completes the POST in the background. `skill-event.js` still enforces +a hard 3s request timeout so a hung network call can't linger. + +The hook payload (which skill was invoked) arrives on the wrapper's **stdin** — but a +backgrounded process's stdin is `/dev/null` (POSIX), so the detached child can't read it +directly. So the wrapper reads stdin **synchronously** (a fast local pipe read, not the +network) into a `mktemp` temp file and passes its path via `--hook-input-file`; +`skill-event.js` reads that file and then unlinks it. It reads the file **before** the +telemetry-off guards, so the temp file is cleaned up even when nothing is sent. The +detached child has all three of stdin/stdout/stderr redirected to `/dev/null` (stdout and +stderr to keep a stray write from raising SIGPIPE and killing the POST mid-flight). If +`mktemp` is unavailable the wrapper exits without sending, rather than writing the payload +to a predictable path. + +The explicit `skill-feedback.sh` path does **not** detach — it runs in the foreground and +awaits the send, because it reports success/failure back to the user. + +## Harness support, in depth + +- **Claude Code** — fully wired via `hooks/hooks.json`: `PostToolUse` matcher `Skill` + (the AI's `Skill` tool → `initiator: ai`) and `UserPromptExpansion` (user `/slash` + commands → `initiator: user`). + +- **Codex** — `skill_invoked` cannot fire from the plugin today (verified against + codex-cli 0.138.0): + 1. **Plugin hooks are removed.** `codex features list` shows `plugin_hooks` = stage + `removed`, state `false`. The general hook engine (`hooks`) is stable, but it only + loads hooks from the user / project / managed layers (a `hooks.json`/`hooks.toml` in + `CODEX_HOME` or `config.toml [hooks]`), not from a plugin. A `hooks` key in + `.codex-plugin/plugin.json` is accepted by the installer but never registers a hook — + and Codex's own plugin scaffolder explicitly says to omit unsupported manifest fields + including `hooks`. So we do **not** ship a Codex plugin hooks file. + 2. **Skills aren't tool-invoked.** Codex injects the skill catalog into context and the + model reads a skill's `SKILL.md` with its normal file/shell tools — there is no + `Skill` tool. So a `PostToolUse` matcher on `tool_name == "Skill"` would never fire, + and matching the generic `shell`/read tool would be noisy and brittle. + 3. **No `UserPromptExpansion`.** Codex has `UserPromptSubmit` (fires on every prompt, + not skill-scoped), so the Claude user-path event key is ignored. + + Good news for the future: Codex **aliases** `CLAUDE_PLUGIN_ROOT` / `CLAUDE_PLUGIN_DATA` + (confirmed in the hook engine's env table), so if both gaps close — `plugin_hooks` + returns *and* skill use becomes observable via a hook event — the wiring is small. Until + then, manual `skill_feedback` is the Codex signal. + +- **Cursor / others** — no plugin-hook system at all; manual `skill_feedback` (run the + bundled script directly) is the only signal. + +## Windows + +The hook command and footer commands start with `sh`, which stock Windows lacks (it exists +under Git Bash / WSL, where many Expo devs already work). The design is fail-open: if `sh` +isn't found the hook simply no-ops, so nothing breaks — but automatic `skill_invoked` +tracking won't fire on a bare Windows shell, so Windows usage is under-counted there. +Manual `skill_feedback` still works by running the JS directly with `node`/`bun`. A future +cross-platform fix is to do the detach inside Node (`spawn(..., { detached: true, +stdio: "ignore" }).unref()`) instead of in `sh`. diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/skill-feedback/scripts/skill-event.js index db63fb1..457629a 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.js @@ -28,7 +28,7 @@ const { const EVENT = "skill_invoked"; function parseArgs(argv) { - const args = { skill: "", agentHarness: "", initiator: "", pluginRoot: "", dryRun: false, quiet: false }; + const args = { skill: "", agentHarness: "", initiator: "", pluginRoot: "", hookInputFile: "", dryRun: false, quiet: false }; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const next = () => argv[++i] || ""; @@ -37,6 +37,7 @@ function parseArgs(argv) { case "--agent-harness": args.agentHarness = next(); break; case "--initiator": args.initiator = next(); break; case "--plugin-root": args.pluginRoot = next(); break; + case "--hook-input-file": args.hookInputFile = next(); break; case "--dry-run": args.dryRun = true; break; case "--quiet": args.quiet = true; break; default: break; // ignore unknown flags @@ -45,10 +46,21 @@ function parseArgs(argv) { return args; } -function readHookInput() { +// Read the hook payload. The detaching wrapper (skill-event.sh) stashes stdin in a temp +// file and passes --hook-input-file, because a backgrounded process's stdin is /dev/null +// (POSIX), so the detached send can't read the pipe directly. We read that file then +// unlink it. With no file (foreground / direct invocation), fall back to stdin (fd 0). +function readHookInput(file) { try { - if (process.stdin.isTTY) return {}; - const raw = fs.readFileSync(0, "utf8").trim(); // fd 0 = stdin + let raw; + if (file) { + raw = fs.readFileSync(file, "utf8"); + try { fs.unlinkSync(file); } catch {} + } else { + if (process.stdin.isTTY) return {}; + raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin + } + raw = (raw || "").trim(); if (!raw) return {}; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; @@ -63,10 +75,9 @@ function readHookInput() { // (anything that isn't really one of our skills is dropped): // - Claude Code Skill tool: tool_input.skill (e.g. "expo:expo-observe") // - Claude Code /slash command: command_name (UserPromptExpansion) -// - Codex / future skill hooks: tool_input.skill_name, top-level skill / skill_name -// Codex mirrors Claude's hook event names (parity tracker openai/codex#21753) and aliases -// CLAUDE_PLUGIN_ROOT, so the existing hooks fire there unchanged once Codex enables plugin -// hooks — this resolver just has to tolerate whatever field the payload uses for the name. +// - other payload shapes: tool_input.skill_name, top-level skill / skill_name +// We check every plausible field so the resolver stays robust across payload shapes; the +// strict skillBelongsToPlugin() scoping below keeps it safe even when permissive. // Plugin skills are namespaced (e.g. "expo:expo-observe") — keep the final segment. function skillFromHook(hookInput) { const ti = hookInput && typeof hookInput.tool_input === "object" && hookInput.tool_input ? hookInput.tool_input : {}; @@ -83,8 +94,11 @@ function pluginRootFor(args) { // Only emit for skills that belong to THIS plugin (so we never track other plugins' // or the user's own skills). Confirms /skills//SKILL.md exists. +// The skill name must be a single kebab-case segment — this also blocks path traversal +// (e.g. "../../x") from a malformed payload reaching path.join or the event property. function skillBelongsToPlugin(skill, pluginRoot) { if (!skill || !pluginRoot) return false; + if (!/^[a-z0-9][a-z0-9-]*$/.test(skill)) return false; try { return fs.existsSync(path.join(pluginRoot, "skills", skill, "SKILL.md")); } catch { return false; } } @@ -119,9 +133,12 @@ function eventPayload(skill, args, hookInput) { async function main(argv) { const args = parseArgs(argv); + // Read (and unlink) the hook payload FIRST so the temp file from skill-event.sh is + // cleaned up on every path below, including the telemetry-off early returns. (It can + // still be orphaned if no JS runtime starts at all; those are mode 0600 and OS-reaped.) + const hookInput = readHookInput(args.hookInputFile); if (telemetryDisabled()) return 0; - if (!telemetryConfigured() && !args.dryRun) return 0; // no key set -> fully inert - const hookInput = readHookInput(); + if (!telemetryConfigured() && !args.dryRun) return 0; // no key in this build (e.g. a fork) -> stay inert let skill = args.skill.trim(); if (skill === "auto") skill = skillFromHook(hookInput); @@ -135,7 +152,10 @@ async function main(argv) { return 0; } - maybeShowFirstRunNotice(); + // Only show the one-time notice on a visible (non-quiet) invocation. The quiet hook path + // runs detached with stderr -> /dev/null, so printing there would burn the once-per-machine + // marker without the user ever seeing it. On-by-default is disclosed in the README + skill. + if (!args.quiet) maybeShowFirstRunNotice(); try { await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 3000 }); } catch (err) { diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.sh b/plugins/expo/skills/skill-feedback/scripts/skill-event.sh new file mode 100644 index 0000000..595d0ee --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/skill-event.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Emit an anonymous `skill_invoked` usage event — FIRE AND FORGET. +# +# Launched from a Claude Code hook (PostToolUse / UserPromptExpansion), which waits for +# the command to exit. We must NOT block the agent turn on a network round-trip, so the +# actual send is detached into its own session and this wrapper returns immediately; the +# child finishes the POST off the critical path even if the hook's process group is torn +# down (`setsid`/POSIX::setsid put it in a fresh group; `nohup` is the last resort). +# +# The hook payload (which skill was invoked) arrives on stdin. We canNOT let the detached +# child read stdin directly: POSIX assigns a backgrounded command's stdin to /dev/null +# unless redirected, so the child would see no payload and emit nothing. Instead we read +# stdin SYNCHRONOUSLY here (a fast local pipe read, not the network) into a temp file and +# hand the path to skill-event.js via --hook-input-file; it reads then unlinks the file. +# +# stdout/stderr -> /dev/null on the child so a stray write after the harness pipe closes +# can't raise SIGPIPE and kill the POST mid-flight. +# +# Usage: sh skill-event.sh --skill --initiator [--plugin-root ] [--quiet] + +dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +# mktemp gives a fresh, unpredictable, 0600 file. If it's somehow unavailable, bail rather +# than fall back to a predictable /tmp name (world-readable + symlink-followable, and the +# payload can hold tool I/O). Telemetry is best-effort, so skipping a send is fine. +payload=$(mktemp "${TMPDIR:-/tmp}/expo-skills-hook.XXXXXX" 2>/dev/null) || exit 0 +cat > "$payload" + +if command -v setsid >/dev/null 2>&1; then + setsid sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & +elif command -v perl >/dev/null 2>&1; then + perl -e 'use POSIX qw(setsid); setsid(); exec @ARGV or exit 127;' \ + sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & +else + nohup sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & +fi + +exit 0 diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js index db0351f..5450f2c 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js @@ -82,7 +82,7 @@ async function main(argv) { return 0; } if (!telemetryConfigured() && !args.dryRun) { - console.error("skill-feedback: no PostHog key configured (placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or the key in telemetry_common.js."); + console.error("skill-feedback: no PostHog key in this build (key stripped to placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or restore the key in telemetry_common.js."); return 0; } diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh new file mode 100644 index 0000000..3731fa7 --- /dev/null +++ b/plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Submit explicit feedback about an Expo skill. +# +# Thin wrapper so skill footers can call ONE short path instead of repeating +# `run.sh` + the script path. Runs in the FOREGROUND and awaits the send, since +# this path reports success/failure back to the user. +# +# Usage: sh skill-feedback.sh --skill --rating --text "..." [--dry-run] + +dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +exec sh "$dir/run.sh" "$dir/skill-feedback.js" "$@" diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry.js b/plugins/expo/skills/skill-feedback/scripts/telemetry.js index 69389a9..64f8770 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry.js @@ -33,7 +33,7 @@ function printStatus() { if (byEnv) reasons.push(`env var ${byEnv}`); console.log(`Expo skills telemetry: DISABLED — via ${reasons.join(" and ")}.`); } else if (!telemetryConfigured()) { - console.log("Expo skills telemetry: ENABLED but NOT CONFIGURED — no PostHog key set (placeholder), so nothing is created or sent. Disable permanently with: telemetry.js --off"); + console.log("Expo skills telemetry: ENABLED, but no PostHog key in this build (key stripped to placeholder) — nothing is created or sent. Disable permanently with: telemetry.js --off"); } else { console.log("Expo skills telemetry: ENABLED (anonymous). Disable with: telemetry.js --off"); } diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js index 490c076..9ebad0a 100644 --- a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js @@ -69,8 +69,10 @@ function telemetryDisabled() { return false; } -// True only when a real PostHog project key is set. Until then the plugin is -// fully inert: no installation ID is created and no network request is made. +// Whether a usable PostHog project key is present. A real key ships in this +// file by default, so telemetry is ON by default (anonymous; opt out via +// telemetryDisabled()). This guard only makes the scripts inert if someone +// strips the key to empty / "phc_REPLACE_ME" (e.g. a fork or a private build). function telemetryConfigured() { const key = String(POSTHOG_PROJECT_API_KEY || "").trim(); return key.length > 0 && key !== "phc_REPLACE_ME"; diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index 2bc3922..fffb9d3 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -139,7 +139,7 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill upgrading-expo --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 0b060ee..806c190 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -422,7 +422,7 @@ On web, the `dom` prop is ignored since no webview is needed. If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/run.sh" "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.js" --skill use-dom --rating idea --text "..."` -- **Other agents (Codex, etc.):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags (the harness is auto-detected). +- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` +- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. From 4143946aabdcbb2c690a2c47c87438aa9a39b332 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 17 Jun 2026 18:52:26 +0200 Subject: [PATCH 12/23] docs(expo): broaden skill-feedback triggers + de-dup harness story MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the skill-writing-framework review. - description: add opt-out / disable / turn-off / telemetry triggers. The skill is the only place an agent in a fresh user project learns the opt-out command, but the description only carried feedback triggers — so "turn off Expo skills telemetry" wouldn't reliably invoke it. (P0: one trigger per branch.) - SKILL.md "Harness support": collapse the medium tier to a 1-line glance + pointer; the per-harness detail (volatile, pinned to codex 0.138) now lives only in references/telemetry.md, so a Codex change is a one-place edit. (P1.) - AGENTS.md: correct the "just copy the footer" note — you must swap --skill , and skill-feedback's own footer uses a different relative path. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/skills/skill-feedback/SKILL.md | 19 +++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2bdf05c..9c6f3c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, just copy the `## Expo Skill Feedback` footer from any existing skill. +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, copy the `## Expo Skill Feedback` footer from another product skill and swap in your skill's `--skill ` (note `skill-feedback`'s own footer uses a different relative script path). **Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 4c97eb4..8748b01 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -1,6 +1,6 @@ --- name: skill-feedback -description: Submit feedback about Expo skills, and how the bundled usage telemetry works. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving. +description: Submit feedback on an Expo skill, or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving — or when the user wants to disable, opt out of, turn off, stop, or understand the anonymous usage tracking these skills send. --- # Skill Feedback @@ -89,19 +89,10 @@ removes everything. ## Harness support -Only the automatic `skill_invoked` path depends on the harness; manual `skill_feedback` -works everywhere. - -- **Claude Code** — fully wired. `skill_invoked` fires from `hooks/hooks.json` (the AI's - `Skill` tool → `initiator: ai`; user `/slash` commands → `initiator: user`). -- **Codex** — automatic tracking can't be shipped from the plugin today: in codex-cli - 0.138 `plugin_hooks` is a *removed* feature, and Codex runs a skill by reading its - `SKILL.md` directly (there is no `Skill` tool to hook). Manual `skill_feedback` is the - Codex signal. -- **Cursor / others** — no plugin-hook system; manual `skill_feedback` only. - -See `./references/telemetry.md` for the event shape, PostHog key configuration, the Codex -details, and the prompt-capture and Windows notes. +Automatic `skill_invoked` is **Claude Code only** (it fires from `hooks/hooks.json`). +**Codex** and **Cursor** can't host the plugin hook today, so they rely on manual +`skill_feedback` — which works everywhere. The Codex specifics (why), the event shape, key +configuration, and the prompt-capture and Windows notes are in `./references/telemetry.md`. --- From 4f95bda895e3225be49baddaef5b30113d602bc5 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 17 Jun 2026 19:59:06 +0200 Subject: [PATCH 13/23] fix(expo): reference feedback script via ${CLAUDE_PLUGIN_ROOT}, not a non-existent var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill footers and skill-feedback SKILL.md referenced the bundled scripts as ${CLAUDE_SKILL_DIR}/... — but that variable does not exist. The Claude Code plugins reference defines exactly three path variables substituted inline in skill content: ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${CLAUDE_PROJECT_DIR}. ${CLAUDE_SKILL_DIR} was used by no other plugin and is undocumented, so it expanded to empty and the explicit-feedback command was broken on Claude Code (`sh "/../skill-feedback/scripts/skill-feedback.sh"`). The skill_invoked hook path was unaffected — it already (correctly) used ${CLAUDE_PLUGIN_ROOT}. Rewrite every reference to the official ${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/ scripts/... form (substituted inline in skill content per the docs). This also makes all 16 footers byte-identical (no more special path for skill-feedback's own footer), so the AGENTS.md "just copy the footer" note is now literally true. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/skills/add-app-clip/SKILL.md | 2 +- plugins/expo/skills/building-native-ui/SKILL.md | 2 +- plugins/expo/skills/eas-update-insights/SKILL.md | 2 +- plugins/expo/skills/expo-api-routes/SKILL.md | 2 +- plugins/expo/skills/expo-brownfield/SKILL.md | 2 +- plugins/expo/skills/expo-cicd-workflows/SKILL.md | 2 +- plugins/expo/skills/expo-deployment/SKILL.md | 2 +- plugins/expo/skills/expo-dev-client/SKILL.md | 2 +- plugins/expo/skills/expo-module/SKILL.md | 2 +- plugins/expo/skills/expo-observe/SKILL.md | 2 +- plugins/expo/skills/expo-tailwind-setup/SKILL.md | 2 +- plugins/expo/skills/expo-ui/SKILL.md | 2 +- plugins/expo/skills/native-data-fetching/SKILL.md | 2 +- plugins/expo/skills/skill-feedback/SKILL.md | 8 ++++---- plugins/expo/skills/upgrading-expo/SKILL.md | 2 +- plugins/expo/skills/use-dom/SKILL.md | 2 +- 17 files changed, 20 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9c6f3c5..a6b263d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, copy the `## Expo Skill Feedback` footer from another product skill and swap in your skill's `--skill ` (note `skill-feedback`'s own footer uses a different relative script path). +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/...`, so no per-skill path edits). **Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index f084d86..3163e3b 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -285,7 +285,7 @@ See [./references/native-module.md](./references/native-module.md) for the Swift If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index b9332d0..7780c6e 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -326,7 +326,7 @@ export default function Layout({ segment }) { If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index b65835e..52b2c79 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -233,7 +233,7 @@ Human-readable group details plus 30 days of launches/failures per platform — If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index bdff58c..4f2bf98 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -373,7 +373,7 @@ export async function GET(request: Request) { If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 849df30..4972b62 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -59,7 +59,7 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 2be454b..c9481e4 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -97,7 +97,7 @@ When users ask about available options (job types, triggers, runner types, etc.) If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 471a9ef..7ee4922 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -195,7 +195,7 @@ eas submit:list If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index 202aabd..4abf415 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -169,7 +169,7 @@ eas update If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index 333c41c..a8b3bc2 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -146,7 +146,7 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index fac326a..ac562fb 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -34,7 +34,7 @@ The three reference files in `./references/` cover the three things people typic If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 11ec8aa..0c97cdc 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -485,7 +485,7 @@ type Props = React.ComponentProps & { className?: string }; If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index bca48e4..7cc49e6 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -49,7 +49,7 @@ references/ If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index af3e508..a0a4470 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -512,7 +512,7 @@ User: "How do I load data for a page in Expo Router?" If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 8748b01..27c17fd 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -38,10 +38,10 @@ Node/Bun is a hard requirement for Expo development, so it's effectively always Keep it to 1-3 short sentences, name the skill, and prefer concrete observations. -**Claude Code** (`${CLAUDE_SKILL_DIR}` resolves to the active skill's folder): +**Claude Code** (`${CLAUDE_PLUGIN_ROOT}` is substituted with the plugin's install path): ```bash -sh "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.sh" \ +sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" \ --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` @@ -70,7 +70,7 @@ before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** ```bash # Claude Code: -sh "${CLAUDE_SKILL_DIR}/scripts/run.sh" "${CLAUDE_SKILL_DIR}/scripts/telemetry.js" --off +sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/telemetry.js" --off # Other agents: node skill-feedback/scripts/telemetry.js --off # --on to re-enable, --status to check ``` @@ -100,7 +100,7 @@ configuration, and the prompt-capture and Windows notes are in `./references/tel If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index fffb9d3..29e25e5 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -139,7 +139,7 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 806c190..0c3bca0 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -422,7 +422,7 @@ On web, the `dom` prop is ignored since no webview is needed. If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: -- **Claude Code:** `sh "${CLAUDE_SKILL_DIR}/../skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` +- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` - **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. Never include secrets, private data, source code, long prompts, or stack traces. From db9eca4eee2e3eae3c39a50b802630d64811916f Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 15:12:40 +0200 Subject: [PATCH 14/23] refactor(expo): focus skill telemetry on Claude Code; drop other-agent messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both events stay — automatic skill_invoked (plugin hooks, verified firing on both the AI Skill-tool and user /slash paths) and explicit skill_feedback (the agent runs the bundled script, which POSTs to PostHog). Automatic tracking only works on Claude Code, so the docs now say so plainly instead of advertising a half-working "other agents" story: - 15 product footers + skill-feedback's own: trimmed to a single Claude Code line (dropped the "Other agents (Codex, Cursor)" bullet and the wrapper-vs-js duplication). ~8-line footer -> ~5. - skill-feedback/SKILL.md: framed as Claude Code; removed the "other agents" submit/opt-out blocks; collapsed "Harness support" into a one-line pointer. - README + plugin README: telemetry sections say Claude Code only; left all the plugin-DISTRIBUTION mentions (Codex install, Run button, skills CLI) intact — those are unrelated to telemetry. - references/telemetry.md: condensed the harness deep-dive to a short "why Claude Code only" note (now citing the openai/codex source: plugin_hooks is a removed feature + no skill event in HookEventName). Kept the generic detectHarness()/--agent-harness plumbing (5 lines, harmless, labels any direct manual send correctly). No Codex hooks file is shipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- README.md | 7 ++- plugins/expo/README.md | 2 +- plugins/expo/skills/add-app-clip/SKILL.md | 7 +-- .../expo/skills/building-native-ui/SKILL.md | 7 +-- .../expo/skills/eas-update-insights/SKILL.md | 7 +-- plugins/expo/skills/expo-api-routes/SKILL.md | 7 +-- plugins/expo/skills/expo-brownfield/SKILL.md | 7 +-- .../expo/skills/expo-cicd-workflows/SKILL.md | 7 +-- plugins/expo/skills/expo-deployment/SKILL.md | 7 +-- plugins/expo/skills/expo-dev-client/SKILL.md | 7 +-- plugins/expo/skills/expo-module/SKILL.md | 7 +-- plugins/expo/skills/expo-observe/SKILL.md | 7 +-- .../expo/skills/expo-tailwind-setup/SKILL.md | 7 +-- plugins/expo/skills/expo-ui/SKILL.md | 7 +-- .../expo/skills/native-data-fetching/SKILL.md | 7 +-- plugins/expo/skills/skill-feedback/SKILL.md | 61 +++++++------------ .../skill-feedback/references/telemetry.md | 45 +++++--------- plugins/expo/skills/upgrading-expo/SKILL.md | 7 +-- plugins/expo/skills/use-dom/SKILL.md | 7 +-- 20 files changed, 88 insertions(+), 134 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a6b263d..272b818 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks — **Claude Code only**, since Codex can't host plugin hooks today, so other harnesses fall back to manual feedback) and explicit `skill_feedback`. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/...`, so no per-skill path edits). +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks; see the `skill-feedback` skill's `references/telemetry.md` for why. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/...`, so no per-skill path edits). **Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/README.md b/README.md index 802c81d..598608c 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,12 @@ Expo documentation, Expo CLI, and EAS CLI are the source of truth. These skills ## Usage telemetry & feedback -These skills send **anonymous** usage events so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. +These skills send **anonymous** usage events on **Claude Code** so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. -- **Tracked in Claude Code:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator` = `ai` or `user`). +- **Usage:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator` = `ai` or `user`). - **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. -- **Other agents (Codex, Cursor, …):** automatic tracking needs Claude Code hooks, so it's off there; feedback still works by running the bundled script. + +The wiring is Claude Code-specific (plugin hooks + footer), so other agents send nothing. Turn it off any time — the simplest way is to ask your agent: **"turn off Expo skills telemetry"** (it runs the bundled toggle, which writes `~/.expo-skills/opt-out` — a persistent switch that works regardless of how the agent was launched). For a global or CI opt-out, set an env var instead: diff --git a/plugins/expo/README.md b/plugins/expo/README.md index a8bae66..47979cc 100644 --- a/plugins/expo/README.md +++ b/plugins/expo/README.md @@ -86,7 +86,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, ## Usage telemetry & feedback -These skills send **anonymous** usage events (which skill ran, and whether the AI or a user `/slash` command invoked it) so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent — never source code, prompts, file paths, or personal data. Every skill also ends with an *Expo Skill Feedback* footer for a quick rating + note. Tracking is automatic in Claude Code; in other agents (Codex, Cursor) it's off, but feedback still works. +These skills send **anonymous** usage events (which skill ran, and whether the AI or a user `/slash` command invoked it) so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent — never source code, prompts, file paths, or personal data. Every skill also ends with an *Expo Skill Feedback* footer for a quick rating + note. Both run on **Claude Code** (via the plugin's hooks and footer); other agents send nothing. Turn it off any time — ask your agent to **"turn off Expo skills telemetry"**, or set `DO_NOT_TRACK=1` (or `EXPO_SKILLS_TELEMETRY=0`). See the **skill-feedback** skill for full details, the event shape, and how to configure the PostHog project key. diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index 3163e3b..9ae302a 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -283,9 +283,8 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 7780c6e..918319a 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -324,9 +324,8 @@ export default function Layout({ segment }) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 52b2c79..314a758 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -231,9 +231,8 @@ Human-readable group details plus 30 days of launches/failures per platform — ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 4f2bf98..118806c 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -371,9 +371,8 @@ export async function GET(request: Request) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 4972b62..28fd1d9 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -57,9 +57,8 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index c9481e4..2d6773d 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -95,9 +95,8 @@ When users ask about available options (job types, triggers, runner types, etc.) ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 7ee4922..c3d3185 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -193,9 +193,8 @@ eas submit:list ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index 4abf415..ff5133d 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -167,9 +167,8 @@ eas update ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index a8b3bc2..061d52f 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -144,9 +144,8 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index ac562fb..a9a4d88 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -32,9 +32,8 @@ The three reference files in `./references/` cover the three things people typic ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 0c97cdc..90c83b6 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -483,9 +483,8 @@ type Props = React.ComponentProps & { className?: string }; ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index 7cc49e6..f7c5546 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -47,9 +47,8 @@ references/ ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index a0a4470..84b48d5 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -510,9 +510,8 @@ User: "How do I load data for a page in Expo Router?" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index 27c17fd..b8ab39f 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -5,20 +5,19 @@ description: Submit feedback on an Expo skill, or turn the bundled anonymous usa # Skill Feedback -The shared telemetry + feedback helper for the Expo skills. It powers two separate -signals, both **anonymous** and sent to PostHog so the Expo team can see how skills are -used and where they fall short: +The shared telemetry + feedback helper for the Expo skills on **Claude Code**. It powers +two **anonymous** signals, sent to PostHog so the Expo team can see how skills are used and +where they fall short: -- **Automatic usage** (`skill_invoked`) — emitted by plugin hooks when a skill runs. No - manual step. **On by default.** +- **Automatic usage** (`skill_invoked`) — emitted by the plugin's hooks when a skill runs. + No manual step. **On by default.** - **Explicit feedback** (`skill_feedback`) — a rating + short note you (or the agent, with the user's OK) send deliberately by running the command below. Both send only a short **hash** of a random local installation ID (`~/.expo-skills/installation-id`, created on first send). Hostnames, usernames, emails, file paths, source code, **prompts, and chat history** are **never** sent. Telemetry is -**skipped automatically in CI**, and a direct (non-quiet) run prints a one-time stderr -notice on first send. To turn everything off, see *Turning it off* below. +**skipped automatically in CI**. To turn everything off, see *Turning it off* below. ## Runtime @@ -31,14 +30,13 @@ Node/Bun is a hard requirement for Expo development, so it's effectively always | Event | Trigger | Mechanism | | --- | --- | --- | -| `skill_invoked` | A skill runs — by the **AI** (`Skill` tool) or a **user** `/slash` command | Automatic, via the plugin's `hooks/hooks.json`; tagged `initiator: ai`\|`user`, scoped to this plugin. **Claude Code only** — see *Harness support* | +| `skill_invoked` | A skill runs — by the **AI** (`Skill` tool) or a **user** `/slash` command | Automatic, via the plugin's `hooks/hooks.json`; tagged `initiator: ai`\|`user`, scoped to this plugin | | `skill_feedback` | You deliberately send a rating + note | Manual — the command below | ## Submitting feedback -Keep it to 1-3 short sentences, name the skill, and prefer concrete observations. - -**Claude Code** (`${CLAUDE_PLUGIN_ROOT}` is substituted with the plugin's install path): +Keep it to 1-3 short sentences, name the skill, and prefer concrete observations +(`${CLAUDE_PLUGIN_ROOT}` is substituted with the plugin's install path): ```bash sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" \ @@ -46,16 +44,6 @@ sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" \ --text "TestFlight steps were clear and worked first try." ``` -**Other agents (Codex, Cursor, …)** — run the bundled script directly with `node` or -`bun`. The harness is auto-detected only for Claude Code and Codex; on any other agent -pass `--agent-harness` so the event is labelled correctly: - -```bash -node skill-feedback/scripts/skill-feedback.js \ - --skill use-dom --rating confusing --agent-harness cursor \ - --text "The skill should say which files it inspected before changing code." -``` - Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Add `--dry-run` to print the payload without sending. **Never include** secrets, private data, source code, long prompts, stack traces, API @@ -63,21 +51,16 @@ keys, or tokens. ## Turning it off -Telemetry is anonymous and **on by default**. Opting out silences **both** signals — -automatic `skill_invoked` and explicit `skill_feedback`. The reliable, launch-independent -switch is the bundled toggle; it writes `~/.expo-skills/opt-out`, which every script checks -before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run it: +Telemetry is anonymous and **on by default**. Opting out silences **both** signals. The +reliable switch is the bundled toggle; it writes `~/.expo-skills/opt-out`, which every +script checks before sending. Easiest: ask your agent to **"turn off Expo skills +telemetry."** Or run it: ```bash -# Claude Code: sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/telemetry.js" --off -# Other agents: -node skill-feedback/scripts/telemetry.js --off # --on to re-enable, --status to check ``` -For CI or a global opt-out, set an env var instead (env vars are convenient but only apply -where exported and don't always reach hook subprocesses, so prefer the toggle for a -persistent opt-out): +For CI or a global opt-out, set an env var instead (`--on` re-enables, `--status` checks): ```bash export EXPO_SKILLS_TELEMETRY=0 # or: false / off / no @@ -87,20 +70,18 @@ export DO_NOT_TRACK=1 # cross-tool standard, https://consoledonottrac When disabled, no installation ID is created and nothing is sent. Uninstalling the plugin removes everything. -## Harness support +## Notes for maintainers -Automatic `skill_invoked` is **Claude Code only** (it fires from `hooks/hooks.json`). -**Codex** and **Cursor** can't host the plugin hook today, so they rely on manual -`skill_feedback` — which works everywhere. The Codex specifics (why), the event shape, key -configuration, and the prompt-capture and Windows notes are in `./references/telemetry.md`. +`skill_invoked` is wired for **Claude Code** via the plugin's `hooks/hooks.json`. See +`./references/telemetry.md` for the event shape, PostHog key configuration, why other +harnesses have no automatic signal, and the prompt-capture and Windows notes. --- ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/skill-feedback/references/telemetry.md b/plugins/expo/skills/skill-feedback/references/telemetry.md index 8827708..a9bcb64 100644 --- a/plugins/expo/skills/skill-feedback/references/telemetry.md +++ b/plugins/expo/skills/skill-feedback/references/telemetry.md @@ -67,35 +67,22 @@ to a predictable path. The explicit `skill-feedback.sh` path does **not** detach — it runs in the foreground and awaits the send, because it reports success/failure back to the user. -## Harness support, in depth - -- **Claude Code** — fully wired via `hooks/hooks.json`: `PostToolUse` matcher `Skill` - (the AI's `Skill` tool → `initiator: ai`) and `UserPromptExpansion` (user `/slash` - commands → `initiator: user`). - -- **Codex** — `skill_invoked` cannot fire from the plugin today (verified against - codex-cli 0.138.0): - 1. **Plugin hooks are removed.** `codex features list` shows `plugin_hooks` = stage - `removed`, state `false`. The general hook engine (`hooks`) is stable, but it only - loads hooks from the user / project / managed layers (a `hooks.json`/`hooks.toml` in - `CODEX_HOME` or `config.toml [hooks]`), not from a plugin. A `hooks` key in - `.codex-plugin/plugin.json` is accepted by the installer but never registers a hook — - and Codex's own plugin scaffolder explicitly says to omit unsupported manifest fields - including `hooks`. So we do **not** ship a Codex plugin hooks file. - 2. **Skills aren't tool-invoked.** Codex injects the skill catalog into context and the - model reads a skill's `SKILL.md` with its normal file/shell tools — there is no - `Skill` tool. So a `PostToolUse` matcher on `tool_name == "Skill"` would never fire, - and matching the generic `shell`/read tool would be noisy and brittle. - 3. **No `UserPromptExpansion`.** Codex has `UserPromptSubmit` (fires on every prompt, - not skill-scoped), so the Claude user-path event key is ignored. - - Good news for the future: Codex **aliases** `CLAUDE_PLUGIN_ROOT` / `CLAUDE_PLUGIN_DATA` - (confirmed in the hook engine's env table), so if both gaps close — `plugin_hooks` - returns *and* skill use becomes observable via a hook event — the wiring is small. Until - then, manual `skill_feedback` is the Codex signal. - -- **Cursor / others** — no plugin-hook system at all; manual `skill_feedback` (run the - bundled script directly) is the only signal. +## Why Claude Code only + +`skill_invoked` is wired for Claude Code via `hooks/hooks.json`: `PostToolUse` matcher +`Skill` (the AI's `Skill` tool → `initiator: ai`) and `UserPromptExpansion` (user `/slash` +→ `initiator: user`). Both are verified firing end-to-end. + +Other harnesses have no automatic signal, and we deliberately do **not** ship a +Codex/Cursor hooks file. On **Codex** (verified against codex-cli 0.138 + the `openai/codex` +source): plugin-bundled hooks are a *removed* feature (`Feature::PluginHooks` = +`Stage::Removed`), and Codex runs a skill by reading its `SKILL.md` directly — there is no +`Skill` tool and no skill-invocation event in the `HookEventName` enum, so nothing can +observe skill use. **Cursor** and others have no plugin-hook system. The bundled +`skill_feedback` script still runs anywhere if invoked directly. + +If OpenAI re-introduces `plugin_hooks` *and* adds a skill event, the wiring is small — +Codex already aliases `${CLAUDE_PLUGIN_ROOT}`. ## Windows diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index 29e25e5..d15d43b 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -137,9 +137,8 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 0c3bca0..7cd1625 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -420,9 +420,8 @@ On web, the `dom` prop is ignored since no webview is needed. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences with the bundled feedback script: +If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: -- **Claude Code:** `sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` -- **Other agents (Codex, Cursor, …):** run `skill-feedback/scripts/skill-feedback.js` (bundled in this plugin) with `node` or `bun`, same flags; pass `--agent-harness` on agents other than Claude Code / Codex. +`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` -Never include secrets, private data, source code, long prompts, or stack traces. +Never include secrets, source code, or long prompts. From 5211d9063e433deba77d875fffaabae36f362d4a Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 15:21:51 +0200 Subject: [PATCH 15/23] feat(expo): make the skill footer a proactive feedback nudge + pointer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes the per-skill footer from a copy-pasted command into a trigger that (a) makes the agent aware it can give feedback, (b) actively nudges it to be proactive — especially when a skill was wrong/confusing/outdated/missing — and (c) points to the `skill-feedback` skill for the actual how. The command now lives in one place (skill-feedback's "Submitting feedback"), so all 15 product footers are byte-identical with nothing per-skill to get wrong. - 15 product footers: nudge + "use the `skill-feedback` skill" pointer (no inline command). - skill-feedback/SKILL.md: "Submitting feedback" gains a proactive lead-in and a note to pass *the skill being rated* to `--skill` (the agent arrives here from another skill's footer, not to rate skill-feedback itself). Its own footer matches the nudge tone and points to that section. Tradeoff: the agent loads `skill-feedback` to get the command (one extra hop) instead of running it inline — accepted for the stronger nudge + single source. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/expo/skills/add-app-clip/SKILL.md | 6 ++---- plugins/expo/skills/building-native-ui/SKILL.md | 6 ++---- plugins/expo/skills/eas-update-insights/SKILL.md | 6 ++---- plugins/expo/skills/expo-api-routes/SKILL.md | 6 ++---- plugins/expo/skills/expo-brownfield/SKILL.md | 6 ++---- plugins/expo/skills/expo-cicd-workflows/SKILL.md | 6 ++---- plugins/expo/skills/expo-deployment/SKILL.md | 6 ++---- plugins/expo/skills/expo-dev-client/SKILL.md | 6 ++---- plugins/expo/skills/expo-module/SKILL.md | 6 ++---- plugins/expo/skills/expo-observe/SKILL.md | 6 ++---- plugins/expo/skills/expo-tailwind-setup/SKILL.md | 6 ++---- plugins/expo/skills/expo-ui/SKILL.md | 6 ++---- plugins/expo/skills/native-data-fetching/SKILL.md | 6 ++---- plugins/expo/skills/skill-feedback/SKILL.md | 13 ++++++------- plugins/expo/skills/upgrading-expo/SKILL.md | 6 ++---- plugins/expo/skills/use-dom/SKILL.md | 6 ++---- 16 files changed, 36 insertions(+), 67 deletions(-) diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index 9ae302a..3fd99d3 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -283,8 +283,6 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill add-app-clip --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 918319a..8dd46ed 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -324,8 +324,6 @@ export default function Layout({ segment }) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill building-native-ui --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 314a758..5a9ff24 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -231,8 +231,6 @@ Human-readable group details plus 30 days of launches/failures per platform — ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill eas-update-insights --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 118806c..25b1aa5 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -371,8 +371,6 @@ export async function GET(request: Request) { ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-api-routes --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 28fd1d9..7a30dea 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -57,8 +57,6 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-brownfield --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 2d6773d..1ae999c 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -95,8 +95,6 @@ When users ask about available options (job types, triggers, runner types, etc.) ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-cicd-workflows --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index c3d3185..8792264 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -193,8 +193,6 @@ eas submit:list ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-deployment --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index ff5133d..24cbe59 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -167,8 +167,6 @@ eas update ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-dev-client --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index 061d52f..34c6c8d 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -144,8 +144,6 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-module --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index a9a4d88..571048f 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -32,8 +32,6 @@ The three reference files in `./references/` cover the three things people typic ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-observe --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 90c83b6..27d85ef 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -483,8 +483,6 @@ type Props = React.ComponentProps & { className?: string }; ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-tailwind-setup --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index f7c5546..e814fcd 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -47,8 +47,6 @@ references/ ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill expo-ui --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index 84b48d5..9adb4b9 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -510,8 +510,6 @@ User: "How do I load data for a page in Expo Router?" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill native-data-fetching --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/skill-feedback/SKILL.md index b8ab39f..40dbf93 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/skill-feedback/SKILL.md @@ -35,8 +35,11 @@ Node/Bun is a hard requirement for Expo development, so it's effectively always ## Submitting feedback -Keep it to 1-3 short sentences, name the skill, and prefer concrete observations -(`${CLAUDE_PLUGIN_ROOT}` is substituted with the plugin's install path): +**We want this — please be proactive.** When an Expo skill was wrong, confusing, outdated, +or missing something, send a quick note so the team can fix it (a note when it worked well +helps too). Keep it to 1-3 short sentences, prefer concrete observations, and pass **the +skill you're giving feedback on** to `--skill` — usually the skill whose footer sent you +here, not `skill-feedback` itself (`${CLAUDE_PLUGIN_ROOT}` resolves to the plugin's install path): ```bash sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" \ @@ -80,8 +83,4 @@ harnesses have no automatic signal, and the prompt-capture and Windows notes. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: - -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill skill-feedback --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +Help make these skills better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: flag anything wrong, confusing, outdated, or missing (a quick note when something worked well helps too). See *Submitting feedback* above for the command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index d15d43b..84034cb 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -137,8 +137,6 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill upgrading-expo --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 7cd1625..cbe47a8 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -420,8 +420,6 @@ On web, the `dom` prop is ignored since no webview is needed. ## Expo Skill Feedback -If this skill was useful, confusing, broken, or missing context, submit 1-3 safe sentences: +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -`sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" --skill use-dom --rating idea --text "..."` - -Never include secrets, source code, or long prompts. +To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. From 287960a89a216e4219951c6a8e78f40aa80ecace Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 15:35:31 +0200 Subject: [PATCH 16/23] feat(expo): rename to expo-skill-feedback, add Expo-itself feedback, simplify, proactive footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename the skill `skill-feedback` → `expo-skill-feedback` (dir, frontmatter name, hooks.json path, all 15 footer pointers, README/AGENTS refs). Script filenames (skill-feedback.js/.sh) and the userAgent label stay. Clearer leading word that matches the "Expo Skill Feedback" footer heading. - Feedback can now target **Expo itself**, not just the skill: skill-feedback.js gains `--about skill|expo` (default skill) -> `properties.about`, so the team can separate framework issues from skill-guidance issues. Surfaced in the footer nudge, the skill's Submitting-feedback section, and the description. - Simplify references/telemetry.md (95 -> 50 lines): keep the event shape (the privacy receipt) + add `about`, condense the rest, and point to the scripts' own comments for the detach/Windows how (single source of truth). - Bump plugin manifests 1.3.0 -> 1.4.0 (main reached 1.3.0 via #84). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 4 +- README.md | 2 +- plugins/expo/.claude-plugin/plugin.json | 2 +- plugins/expo/.codex-plugin/plugin.json | 2 +- plugins/expo/.cursor-plugin/plugin.json | 2 +- plugins/expo/README.md | 2 +- plugins/expo/hooks/hooks.json | 4 +- plugins/expo/skills/add-app-clip/SKILL.md | 4 +- .../expo/skills/building-native-ui/SKILL.md | 4 +- .../expo/skills/eas-update-insights/SKILL.md | 4 +- plugins/expo/skills/expo-api-routes/SKILL.md | 4 +- plugins/expo/skills/expo-brownfield/SKILL.md | 4 +- .../expo/skills/expo-cicd-workflows/SKILL.md | 4 +- plugins/expo/skills/expo-deployment/SKILL.md | 4 +- plugins/expo/skills/expo-dev-client/SKILL.md | 4 +- plugins/expo/skills/expo-module/SKILL.md | 4 +- plugins/expo/skills/expo-observe/SKILL.md | 4 +- .../SKILL.md | 16 ++-- .../references/telemetry.md | 50 ++++++++++ .../scripts/package.json | 0 .../scripts/run.sh | 0 .../scripts/skill-event.js | 0 .../scripts/skill-event.sh | 0 .../scripts/skill-feedback.js | 13 ++- .../scripts/skill-feedback.sh | 0 .../scripts/telemetry.js | 0 .../scripts/telemetry_common.js | 0 .../expo/skills/expo-tailwind-setup/SKILL.md | 4 +- plugins/expo/skills/expo-ui/SKILL.md | 4 +- .../expo/skills/native-data-fetching/SKILL.md | 4 +- .../skill-feedback/references/telemetry.md | 95 ------------------- plugins/expo/skills/upgrading-expo/SKILL.md | 4 +- plugins/expo/skills/use-dom/SKILL.md | 4 +- 33 files changed, 109 insertions(+), 143 deletions(-) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/SKILL.md (72%) create mode 100644 plugins/expo/skills/expo-skill-feedback/references/telemetry.md rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/package.json (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/run.sh (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/skill-event.js (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/skill-event.sh (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/skill-feedback.js (83%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/skill-feedback.sh (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/telemetry.js (100%) rename plugins/expo/skills/{skill-feedback => expo-skill-feedback}/scripts/telemetry_common.js (100%) delete mode 100644 plugins/expo/skills/skill-feedback/references/telemetry.md diff --git a/AGENTS.md b/AGENTS.md index 272b818..c8e41c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks; see the `skill-feedback` skill's `references/telemetry.md` for why. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/...`, so no per-skill path edits). +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks; see the `expo-skill-feedback` skill's `references/telemetry.md` for why. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). -**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. +**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/README.md b/README.md index 598608c..5ed0931 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ Turn it off any time — the simplest way is to ask your agent: **"turn off Expo export DO_NOT_TRACK=1 # or: export EXPO_SKILLS_TELEMETRY=0 ``` -See the `skill-feedback` skill for full details, the event shape, and how to configure the PostHog project key. +See the `expo-skill-feedback` skill for full details, the event shape, and how to configure the PostHog project key. ## License diff --git a/plugins/expo/.claude-plugin/plugin.json b/plugins/expo/.claude-plugin/plugin.json index cbc1e4f..4dfefed 100644 --- a/plugins/expo/.claude-plugin/plugin.json +++ b/plugins/expo/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "expo", - "version": "1.3.0", + "version": "1.4.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo apps", "author": { "name": "Expo Team", diff --git a/plugins/expo/.codex-plugin/plugin.json b/plugins/expo/.codex-plugin/plugin.json index e6b360b..3f1d82b 100644 --- a/plugins/expo/.codex-plugin/plugin.json +++ b/plugins/expo/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "expo", - "version": "1.3.0", + "version": "1.4.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", "author": { "name": "Expo Team", diff --git a/plugins/expo/.cursor-plugin/plugin.json b/plugins/expo/.cursor-plugin/plugin.json index d063d66..6a1ce91 100644 --- a/plugins/expo/.cursor-plugin/plugin.json +++ b/plugins/expo/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "expo", "displayName": "Expo", - "version": "1.3.0", + "version": "1.4.0", "description": "Official Expo skills for building, deploying, upgrading, and debugging Expo and React Native apps.", "author": { "name": "Expo Team", diff --git a/plugins/expo/README.md b/plugins/expo/README.md index 47979cc..602a94d 100644 --- a/plugins/expo/README.md +++ b/plugins/expo/README.md @@ -88,7 +88,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, These skills send **anonymous** usage events (which skill ran, and whether the AI or a user `/slash` command invoked it) so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent — never source code, prompts, file paths, or personal data. Every skill also ends with an *Expo Skill Feedback* footer for a quick rating + note. Both run on **Claude Code** (via the plugin's hooks and footer); other agents send nothing. -Turn it off any time — ask your agent to **"turn off Expo skills telemetry"**, or set `DO_NOT_TRACK=1` (or `EXPO_SKILLS_TELEMETRY=0`). See the **skill-feedback** skill for full details, the event shape, and how to configure the PostHog project key. +Turn it off any time — ask your agent to **"turn off Expo skills telemetry"**, or set `DO_NOT_TRACK=1` (or `EXPO_SKILLS_TELEMETRY=0`). See the **expo-skill-feedback** skill for full details, the event shape, and how to configure the PostHog project key. ## License diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index a6d534f..23dfd40 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.sh\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.sh\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-event.sh\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.sh\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index 3fd99d3..c9ba35a 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -283,6 +283,6 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 8dd46ed..1551bce 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -324,6 +324,6 @@ export default function Layout({ segment }) { ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index 5a9ff24..a352544 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -231,6 +231,6 @@ Human-readable group details plus 30 days of launches/failures per platform — ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index 25b1aa5..d6d48f6 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -371,6 +371,6 @@ export async function GET(request: Request) { ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 7a30dea..68f5a26 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -57,6 +57,6 @@ Pin the same Expo SDK across both the RN project and any embedded dependencies. ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 1ae999c..11b3200 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -95,6 +95,6 @@ When users ask about available options (job types, triggers, runner types, etc.) ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 8792264..65eb678 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -193,6 +193,6 @@ eas submit:list ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index 24cbe59..c30614f 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -167,6 +167,6 @@ eas update ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index 34c6c8d..f6e5e4f 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -144,6 +144,6 @@ Note: iOS uses just the class name; Android uses the fully-qualified class name ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index 571048f..1e6b470 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -32,6 +32,6 @@ The three reference files in `./references/` cover the three things people typic ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md similarity index 72% rename from plugins/expo/skills/skill-feedback/SKILL.md rename to plugins/expo/skills/expo-skill-feedback/SKILL.md index 40dbf93..0d7fd26 100644 --- a/plugins/expo/skills/skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -1,6 +1,6 @@ --- -name: skill-feedback -description: Submit feedback on an Expo skill, or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving — or when the user wants to disable, opt out of, turn off, stop, or understand the anonymous usage tracking these skills send. +name: expo-skill-feedback +description: Submit feedback on an Expo skill — or on Expo itself — or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving; when something fell short because of Expo (an SDK bug or confusing framework behavior) rather than the skill; or when the user wants to disable, opt out of, turn off, stop, or understand the anonymous usage tracking these skills send. --- # Skill Feedback @@ -39,16 +39,20 @@ Node/Bun is a hard requirement for Expo development, so it's effectively always or missing something, send a quick note so the team can fix it (a note when it worked well helps too). Keep it to 1-3 short sentences, prefer concrete observations, and pass **the skill you're giving feedback on** to `--skill` — usually the skill whose footer sent you -here, not `skill-feedback` itself (`${CLAUDE_PLUGIN_ROOT}` resolves to the plugin's install path): +here, not `expo-skill-feedback` itself (`${CLAUDE_PLUGIN_ROOT}` resolves to the plugin's install path): ```bash -sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/skill-feedback.sh" \ +sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.sh" \ --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Add `--dry-run` to print the payload without sending. +If the trouble is with **Expo itself** rather than the skill (e.g. an SDK bug or confusing +framework behavior the skill couldn't paper over), add `--about expo` (default `skill`) so +the team can tell framework issues from skill issues. + **Never include** secrets, private data, source code, long prompts, stack traces, API keys, or tokens. @@ -60,7 +64,7 @@ script checks before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run it: ```bash -sh "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/skill-feedback/scripts/telemetry.js" --off +sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.js" --off ``` For CI or a global opt-out, set an env var instead (`--on` re-enables, `--status` checks): @@ -83,4 +87,4 @@ harnesses have no automatic signal, and the prompt-capture and Windows notes. ## Expo Skill Feedback -Help make these skills better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: flag anything wrong, confusing, outdated, or missing (a quick note when something worked well helps too). See *Submitting feedback* above for the command. Never include secrets, source code, or long prompts. +Help make these skills better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: flag anything wrong, confusing, outdated, or missing — in a skill or in **Expo itself** (`--about expo`). A quick note when something worked well helps too. See *Submitting feedback* above for the command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md new file mode 100644 index 0000000..d3fb1f1 --- /dev/null +++ b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md @@ -0,0 +1,50 @@ +# Telemetry internals (maintainer reference) + +Background for the scripts under `scripts/`. The runtime surface — what's sent, how to +submit feedback, how to turn it off — is in `../SKILL.md`; this file is for auditing or +changing the telemetry. + +## Event shape + +- `event`: `skill_invoked` | `skill_feedback` +- `distinct_id`: `expo-skills-installation:` +- `properties.source`: `expo-skills` · `properties.$process_person_profile`: `false` +- `properties.skill`: skill folder name (e.g. `expo-deployment`) +- `properties.agent_harness`: `claude-code` (auto-detected), else `unknown`; `--agent-harness` overrides +- `properties.os` / `arch`: platform (e.g. `macos` / `arm64`), non-PII +- `properties.installation_id_hash` / `session_id_hash`: short hashes only — raw values never sent +- `skill_invoked` adds `properties.initiator`: `ai` (Skill tool) | `user` (`/slash`) +- `skill_feedback` adds `properties.rating`, `properties.feedback_text`, and `properties.about`: `skill` (default) | `expo` (the trouble is Expo itself, not the skill) + +That list **is** the whole privacy surface: only the skill name, anonymous hashes, and +platform leave the machine — never prompts, chat history, code, file paths, or tool inputs. +Qualitative context is opt-in only, as a scrubbed note in `skill_feedback`'s `--text`. + +## PostHog key + +The `phc_…` key in `scripts/telemetry_common.js` is a **write-only, public** ingestion key +(safe to commit; it's what makes telemetry on by default). Override per environment with +`EXPO_SKILLS_POSTHOG_KEY`. Never put a *personal* key (`phx_…`) here. Strip the key (empty +or `phc_REPLACE_ME`, e.g. a fork) and the scripts go inert. + +## Why Claude Code only + +`skill_invoked` fires from `hooks/hooks.json` — `PostToolUse[Skill]` (→ `ai`) and +`UserPromptExpansion` (→ `user`), both verified end-to-end. Other harnesses get no automatic +signal and we ship no hooks file for them: on **Codex** (codex-cli 0.138 + the `openai/codex` +source) plugin hooks are a removed feature (`Feature::PluginHooks` = `Stage::Removed`) and +there's no skill event in `HookEventName`; **Cursor** has no plugin hooks. If OpenAI restores +`plugin_hooks` *and* adds a skill event, wiring is small — Codex already aliases +`${CLAUDE_PLUGIN_ROOT}`. + +## Implementation notes + +The how lives in the scripts' own comments — read those before changing them. + +- **Off the critical path** — the hook runs `skill-event.sh`, which detaches the POST into a + new session (`setsid`→`perl`→`nohup`) and returns in ~20ms; the payload reaches the + detached `skill-event.js` via a temp file + `--hook-input-file` (a backgrounded process's + stdin is `/dev/null`). `skill-feedback.sh` stays foreground — it reports success to the user. +- **Windows** — `sh` is absent on a bare Windows shell, so the hook no-ops (fail-open) and + Windows usage is under-counted. A Node-level detach (`spawn(…, {detached:true}).unref()`) + would drop the `sh` dependency. diff --git a/plugins/expo/skills/skill-feedback/scripts/package.json b/plugins/expo/skills/expo-skill-feedback/scripts/package.json similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/package.json rename to plugins/expo/skills/expo-skill-feedback/scripts/package.json diff --git a/plugins/expo/skills/skill-feedback/scripts/run.sh b/plugins/expo/skills/expo-skill-feedback/scripts/run.sh similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/run.sh rename to plugins/expo/skills/expo-skill-feedback/scripts/run.sh diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/skill-event.js rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-event.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/skill-event.sh rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js similarity index 83% rename from plugins/expo/skills/skill-feedback/scripts/skill-feedback.js rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js index 5450f2c..e58aeb1 100644 --- a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js @@ -3,7 +3,7 @@ // // Usage: // node skill-feedback.js --skill --rating --text "..." \ -// [--agent-harness ] [--dry-run] +// [--about skill|expo] [--agent-harness ] [--dry-run] const crypto = require("crypto"); @@ -22,10 +22,13 @@ const { const EVENT_NAME = "skill_feedback"; const RATINGS = ["useful", "confusing", "bug", "idea", "other"]; +// What the feedback is about: the skill's own guidance (default), or Expo the +// framework — when a skill fell short because of Expo itself, not the skill. +const ABOUT_VALUES = ["skill", "expo"]; const MAX_FEEDBACK_CHARS = 4000; function parseArgs(argv) { - const args = { skill: "", rating: "", text: "", agentHarness: "", dryRun: false }; + const args = { skill: "", rating: "", text: "", about: "skill", agentHarness: "", dryRun: false }; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const next = () => argv[++i] || ""; @@ -33,6 +36,7 @@ function parseArgs(argv) { case "--skill": args.skill = next(); break; case "--rating": args.rating = next(); break; case "--text": args.text = next(); break; + case "--about": args.about = next(); break; case "--agent-harness": args.agentHarness = next(); break; case "--dry-run": args.dryRun = true; break; default: break; @@ -49,10 +53,12 @@ function eventPayload(args) { if (!feedback) throw new Error("--text cannot be empty"); if (!skill) throw new Error("--skill cannot be empty"); if (!RATINGS.includes(args.rating)) throw new Error(`--rating must be one of: ${RATINGS.join(", ")}`); + const about = (args.about || "skill").trim() || "skill"; + if (!ABOUT_VALUES.includes(about)) throw new Error(`--about must be one of: ${ABOUT_VALUES.join(", ")}`); const timestamp = new Date().toISOString(); const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { createInstallation: !args.dryRun }); - const insertSource = stableStringify({ agent_harness: agentHarness, skill, rating: args.rating, feedback, timestamp }); + const insertSource = stableStringify({ agent_harness: agentHarness, skill, about, rating: args.rating, feedback, timestamp }); return { api_key: POSTHOG_PROJECT_API_KEY, @@ -68,6 +74,7 @@ function eventPayload(args) { agent_harness: agentHarness, ...platformProps(), skill, + about, rating: args.rating, feedback_text: feedback, }, diff --git a/plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/skill-feedback.sh rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry.js b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/telemetry.js rename to plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js diff --git a/plugins/expo/skills/skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js similarity index 100% rename from plugins/expo/skills/skill-feedback/scripts/telemetry_common.js rename to plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 27d85ef..8f5abc3 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -483,6 +483,6 @@ type Props = React.ComponentProps & { className?: string }; ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index e814fcd..36b2e85 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -47,6 +47,6 @@ references/ ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index 9adb4b9..76fb926 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -510,6 +510,6 @@ User: "How do I load data for a page in Expo Router?" ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/skill-feedback/references/telemetry.md b/plugins/expo/skills/skill-feedback/references/telemetry.md deleted file mode 100644 index a9bcb64..0000000 --- a/plugins/expo/skills/skill-feedback/references/telemetry.md +++ /dev/null @@ -1,95 +0,0 @@ -# Telemetry internals (maintainer reference) - -Background detail for the `skill-feedback` scripts. The day-to-day surface — what's -sent, how to submit feedback, and how to turn it off — lives in `../SKILL.md`. This -file is for people changing or auditing the telemetry, not for the agent at runtime. - -## Configuring the PostHog project - -The project key lives in `scripts/telemetry_common.js`. It is a **write-only, public** -ingestion key (`phc_...`) — the same kind embedded in browser snippets — so it is safe -to commit, and it is what makes telemetry **on by default**. Override it per environment -(e.g. a staging project) without editing the file: - -```bash -export EXPO_SKILLS_POSTHOG_KEY="phc_your_project_key" -``` - -Only ever use a *project* key (`phc_...`) here. Never a *personal* API key (`phx_...`) — -those are secret and used only for reading/querying (e.g. the PostHog MCP). If the key is -stripped to empty or `phc_REPLACE_ME` (e.g. a fork), `telemetryConfigured()` returns false -and the scripts go inert — no installation ID, no network. - -## Event shape - -- `event`: `skill_invoked` | `skill_feedback` -- `distinct_id`: `expo-skills-installation:` -- `properties.source`: `expo-skills` -- `properties.$process_person_profile`: `false` -- `properties.skill`: skill folder name (e.g. `expo-deployment`) -- `properties.agent_harness`: auto-detected (`claude-code`, `codex`), else `unknown`; override with `--agent-harness` -- `properties.initiator` (on `skill_invoked`): `ai` (invoked via the `Skill` tool) or `user` (a `/slash` command) -- `properties.os` / `properties.arch`: platform, e.g. `macos` / `arm64` (non-PII) -- `properties.installation_id_hash`: anonymous hash of the local random installation ID -- `properties.session_id_hash`: short hash only; raw session IDs are never sent -- `skill_feedback` adds `properties.rating` and `properties.feedback_text` - -## Why prompts / chat history are never captured - -Richer context (the prompt that triggered a skill, surrounding chat) would help study how -to improve a skill, but capturing it would break the anonymity guarantee that justifies -on-by-default collection. So the automatic `skill_invoked` event carries **only the skill -name** and the anonymous properties above — never prompt text, code, file paths, or tool -inputs. If a maintainer wants qualitative context, the path is **explicit and -user-approved**: a human (or an agent with the user's consent) puts a short, scrubbed note -in the `--text` field of `skill_feedback`. Automatic prompt/history capture is intentionally -off the table. - -## How the auto path stays off the critical path - -`skill-event.sh` is launched from a Claude Code hook, which waits for the command to exit. -To avoid adding the POST's latency to every turn, the wrapper detaches the send into its -own session (`setsid`, falling back to `perl POSIX::setsid`, then `nohup`) and returns -immediately; the child completes the POST in the background. `skill-event.js` still enforces -a hard 3s request timeout so a hung network call can't linger. - -The hook payload (which skill was invoked) arrives on the wrapper's **stdin** — but a -backgrounded process's stdin is `/dev/null` (POSIX), so the detached child can't read it -directly. So the wrapper reads stdin **synchronously** (a fast local pipe read, not the -network) into a `mktemp` temp file and passes its path via `--hook-input-file`; -`skill-event.js` reads that file and then unlinks it. It reads the file **before** the -telemetry-off guards, so the temp file is cleaned up even when nothing is sent. The -detached child has all three of stdin/stdout/stderr redirected to `/dev/null` (stdout and -stderr to keep a stray write from raising SIGPIPE and killing the POST mid-flight). If -`mktemp` is unavailable the wrapper exits without sending, rather than writing the payload -to a predictable path. - -The explicit `skill-feedback.sh` path does **not** detach — it runs in the foreground and -awaits the send, because it reports success/failure back to the user. - -## Why Claude Code only - -`skill_invoked` is wired for Claude Code via `hooks/hooks.json`: `PostToolUse` matcher -`Skill` (the AI's `Skill` tool → `initiator: ai`) and `UserPromptExpansion` (user `/slash` -→ `initiator: user`). Both are verified firing end-to-end. - -Other harnesses have no automatic signal, and we deliberately do **not** ship a -Codex/Cursor hooks file. On **Codex** (verified against codex-cli 0.138 + the `openai/codex` -source): plugin-bundled hooks are a *removed* feature (`Feature::PluginHooks` = -`Stage::Removed`), and Codex runs a skill by reading its `SKILL.md` directly — there is no -`Skill` tool and no skill-invocation event in the `HookEventName` enum, so nothing can -observe skill use. **Cursor** and others have no plugin-hook system. The bundled -`skill_feedback` script still runs anywhere if invoked directly. - -If OpenAI re-introduces `plugin_hooks` *and* adds a skill event, the wiring is small — -Codex already aliases `${CLAUDE_PLUGIN_ROOT}`. - -## Windows - -The hook command and footer commands start with `sh`, which stock Windows lacks (it exists -under Git Bash / WSL, where many Expo devs already work). The design is fail-open: if `sh` -isn't found the hook simply no-ops, so nothing breaks — but automatic `skill_invoked` -tracking won't fire on a bare Windows shell, so Windows usage is under-counted there. -Manual `skill_feedback` still works by running the JS directly with `node`/`bun`. A future -cross-platform fix is to do the detach inside Node (`spawn(..., { detached: true, -stdio: "ignore" }).unref()`) instead of in `sh`. diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index 84034cb..fd19d7e 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -137,6 +137,6 @@ The new architecture is enabled by default, the app.json field `"newArchEnabled" ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index cbe47a8..2dd3b64 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -420,6 +420,6 @@ On web, the `dom` prop is ignored since no webview is needed. ## Expo Skill Feedback -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing, please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. +Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. -To send it, use the **`skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. +To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. From 08c3ec08b2cf9365933fc07051bf4b9ed0703d3a Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 16:04:25 +0200 Subject: [PATCH 17/23] refactor(expo): prune unnecessary telemetry machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove accreted complexity that wasn't earning its keep (no behavior change to what's actually tracked): - Drop client-side dedup: `$insert_id` + the `stableStringify` helper. PostHog doesn't need it (we never retry; events are already distinct). - Drop `schema_version` (premature) and `session_id_hash` (nice-to-have, not in the dashboard). - Drop the first-run stderr notice (`maybeShowFirstRunNotice` + `NOTICE_PATH`) — it was near-dead (hidden hook stderr); disclosure lives in the README + skill. - Trim telemetry_common exports to what's actually imported. - references/telemetry.md: drop the "Implementation notes" section (it duplicated the scripts' own comments — single source of truth) and the session-hash line. telemetry_common.js 224->148, references/telemetry.md 95->41. Event payloads now carry only: skill, agent_harness, initiator/about, os/arch, installation_id_hash (+ rating/feedback_text for skill_feedback). Verified both dry-runs + the toggle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../references/telemetry.md | 15 +- .../scripts/skill-event.js | 22 +-- .../scripts/skill-feedback.js | 7 - .../scripts/telemetry_common.js | 128 ++++-------------- 4 files changed, 31 insertions(+), 141 deletions(-) diff --git a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md index d3fb1f1..4b48d25 100644 --- a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md +++ b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md @@ -12,7 +12,7 @@ changing the telemetry. - `properties.skill`: skill folder name (e.g. `expo-deployment`) - `properties.agent_harness`: `claude-code` (auto-detected), else `unknown`; `--agent-harness` overrides - `properties.os` / `arch`: platform (e.g. `macos` / `arm64`), non-PII -- `properties.installation_id_hash` / `session_id_hash`: short hashes only — raw values never sent +- `properties.installation_id_hash`: anonymous hash of the local random install id (the raw id never leaves the machine) - `skill_invoked` adds `properties.initiator`: `ai` (Skill tool) | `user` (`/slash`) - `skill_feedback` adds `properties.rating`, `properties.feedback_text`, and `properties.about`: `skill` (default) | `expo` (the trouble is Expo itself, not the skill) @@ -37,14 +37,5 @@ there's no skill event in `HookEventName`; **Cursor** has no plugin hooks. If Op `plugin_hooks` *and* adds a skill event, wiring is small — Codex already aliases `${CLAUDE_PLUGIN_ROOT}`. -## Implementation notes - -The how lives in the scripts' own comments — read those before changing them. - -- **Off the critical path** — the hook runs `skill-event.sh`, which detaches the POST into a - new session (`setsid`→`perl`→`nohup`) and returns in ~20ms; the payload reaches the - detached `skill-event.js` via a temp file + `--hook-input-file` (a backgrounded process's - stdin is `/dev/null`). `skill-feedback.sh` stays foreground — it reports success to the user. -- **Windows** — `sh` is absent on a bare Windows shell, so the hook no-ops (fail-open) and - Windows usage is under-counted. A Node-level detach (`spawn(…, {detached:true}).unref()`) - would drop the `sh` dependency. +The *how* (detach, temp-file payload handoff, Windows fail-open) lives in the scripts' own +comments — read those before changing them. diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js index 457629a..f67d0a3 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js @@ -6,21 +6,16 @@ // - UserPromptExpansion -> a user ran a /slash command (--initiator user) // Reads the hook payload from stdin; never blocks (exits 0 under --quiet). -const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const { POSTHOG_PROJECT_API_KEY, SOURCE, - SCHEMA_VERSION, telemetryDisabled, telemetryConfigured, detectHarness, platformProps, - maybeShowFirstRunNotice, - shortHash, - stableStringify, telemetryIdentity, sendToPosthog, } = require("./telemetry_common.js"); @@ -103,30 +98,21 @@ function skillBelongsToPlugin(skill, pluginRoot) { catch { return false; } } -function eventPayload(skill, args, hookInput) { +function eventPayload(skill, args) { const agentHarness = args.agentHarness.trim() || detectHarness(); const initiator = args.initiator.trim(); const timestamp = new Date().toISOString(); - const sessionIdHash = shortHash(hookInput.session_id); const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { createInstallation: !args.dryRun }); - const insertSource = stableStringify({ - skill, event: EVENT, agent_harness: agentHarness, initiator, - session_id_hash: sessionIdHash, timestamp, - }); - const properties = { $process_person_profile: false, - $insert_id: `${EVENT}:` + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), source: SOURCE, - schema_version: SCHEMA_VERSION, skill, agent_harness: agentHarness, ...(initiator ? { initiator } : {}), ...platformProps(), ...identityProperties, }; - if (sessionIdHash) properties.session_id_hash = sessionIdHash; return { api_key: POSTHOG_PROJECT_API_KEY, event: EVENT, distinct_id: distinctId, timestamp, properties }; } @@ -145,17 +131,13 @@ async function main(argv) { if (!skill) return 0; // not a skill invocation if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; // not one of ours - const payload = eventPayload(skill, args, hookInput); + const payload = eventPayload(skill, args); if (args.dryRun) { console.log(JSON.stringify({ ...payload, api_key: "phc_..." }, null, 2)); return 0; } - // Only show the one-time notice on a visible (non-quiet) invocation. The quiet hook path - // runs detached with stderr -> /dev/null, so printing there would burn the once-per-machine - // marker without the user ever seeing it. On-by-default is disclosed in the README + skill. - if (!args.quiet) maybeShowFirstRunNotice(); try { await sendToPosthog(payload, { userAgent: "expo-skills/skill-event", timeoutMs: 3000 }); } catch (err) { diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js index e58aeb1..29723da 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js @@ -5,17 +5,13 @@ // node skill-feedback.js --skill --rating --text "..." \ // [--about skill|expo] [--agent-harness ] [--dry-run] -const crypto = require("crypto"); - const { POSTHOG_PROJECT_API_KEY, SOURCE, - SCHEMA_VERSION, telemetryDisabled, telemetryConfigured, detectHarness, platformProps, - stableStringify, telemetryIdentity, sendToPosthog, } = require("./telemetry_common.js"); @@ -58,7 +54,6 @@ function eventPayload(args) { const timestamp = new Date().toISOString(); const [distinctId, identityProperties] = telemetryIdentity(agentHarness, { createInstallation: !args.dryRun }); - const insertSource = stableStringify({ agent_harness: agentHarness, skill, about, rating: args.rating, feedback, timestamp }); return { api_key: POSTHOG_PROJECT_API_KEY, @@ -67,9 +62,7 @@ function eventPayload(args) { timestamp, properties: { $process_person_profile: false, - $insert_id: "skill_feedback:" + crypto.createHash("sha256").update(insertSource).digest("hex").slice(0, 32), source: SOURCE, - schema_version: SCHEMA_VERSION, ...identityProperties, agent_harness: agentHarness, ...platformProps(), diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js index 9ebad0a..5588f7b 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js @@ -21,21 +21,13 @@ const POSTHOG_PROJECT_API_KEY = process.env.EXPO_SKILLS_POSTHOG_KEY || "phc_w8xRytdAAwkV3oExnuUozqH64PMzCmDLnyoChpPBcNXs"; const SOURCE = "expo-skills"; -const SCHEMA_VERSION = 1; -const INSTALLATION_ID_PATH = path.join( - os.homedir(), - ".expo-skills", - "installation-id" -); +const INSTALLATION_ID_PATH = path.join(os.homedir(), ".expo-skills", "installation-id"); // Persistent opt-out marker. Checked before anything is sent, so it works // regardless of how the agent was launched (env vars don't always reach hook // subprocesses). Toggle it with scripts/telemetry.js --off / --on. const OPT_OUT_PATH = path.join(os.homedir(), ".expo-skills", "opt-out"); -// Marker so the one-time "we collect anonymous telemetry" notice prints once. -const NOTICE_PATH = path.join(os.homedir(), ".expo-skills", "notice-shown"); - // CI detection — skip telemetry in automated environments so usage data reflects // real humans. Honors the common CI=true convention plus major providers' signals. function isCI() { @@ -53,15 +45,11 @@ function isCI() { ); } -// Opt-out switch. Disabled when EXPO_SKILLS_TELEMETRY is 0/false/off/no, or -// when the cross-tool DO_NOT_TRACK convention (https://consoledonottrack.com) -// is set to anything other than 0/false. +// Opt-out switch: the persistent opt-out file, CI, or the EXPO_SKILLS_TELEMETRY / +// DO_NOT_TRACK (https://consoledonottrack.com) env vars. function telemetryDisabled() { - // 1) Persistent opt-out file — the reliable, launch-independent switch. try { if (fs.existsSync(OPT_OUT_PATH)) return true; } catch {} - // 2) Skip automated / CI environments. if (isCI()) return true; - // 3) Env vars — for global opt-out. const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); if (["0", "false", "off", "no"].includes(flag)) return true; const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); @@ -69,27 +57,18 @@ function telemetryDisabled() { return false; } -// Whether a usable PostHog project key is present. A real key ships in this -// file by default, so telemetry is ON by default (anonymous; opt out via -// telemetryDisabled()). This guard only makes the scripts inert if someone -// strips the key to empty / "phc_REPLACE_ME" (e.g. a fork or a private build). +// A real key ships in this file, so telemetry is ON by default. This guard only +// makes the scripts inert if someone strips the key (e.g. a fork or private build). function telemetryConfigured() { const key = String(POSTHOG_PROJECT_API_KEY || "").trim(); return key.length > 0 && key !== "phc_REPLACE_ME"; } -// Best-effort agent-harness detection from environment signals — used as the -// default when --agent-harness isn't passed. Claude Code sets CLAUDECODE; Codex -// sets CODEX_SANDBOX (in sandboxed mode) and is moving toward AGENT=codex. +// Best-effort agent-harness label for the event (default when --agent-harness isn't passed). function detectHarness() { if (process.env.CLAUDECODE) return "claude-code"; - if ( - process.env.CODEX_SANDBOX || - process.env.CODEX_SANDBOX_NETWORK_DISABLED || - String(process.env.AGENT || "").toLowerCase() === "codex" - ) { - return "codex"; - } + if (process.env.CODEX_SANDBOX || process.env.CODEX_SANDBOX_NETWORK_DISABLED || + String(process.env.AGENT || "").toLowerCase() === "codex") return "codex"; return "unknown"; } @@ -99,36 +78,7 @@ function platformProps() { return { os: osName, arch: process.arch }; } -// Print a one-time transparency notice the first time real telemetry is sent. -// Best-effort and gated by an atomic marker, so it appears at most once per machine. -function maybeShowFirstRunNotice() { - try { - fs.mkdirSync(path.dirname(NOTICE_PATH), { recursive: true, mode: 0o700 }); - const fd = fs.openSync(NOTICE_PATH, "wx", 0o600); // succeeds only the first time - fs.closeSync(fd); - process.stderr.write( - "expo-skills: sending anonymous usage analytics (skill name only — no code, prompts, " + - "or personal data). Turn off with `telemetry.js --off` or EXPO_SKILLS_TELEMETRY=0.\n" - ); - } catch { - // Already shown, or filesystem unavailable — stay silent. - } -} - -function shortHash(value, length = 16) { - if (!value) return null; - return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, length); -} - -// Deterministic JSON with sorted keys for a consistent $insert_id hash. (Events are -// still unique per invocation — callers include `timestamp` in the hashed input.) -function stableStringify(value) { - if (value === null || typeof value !== "object") return JSON.stringify(value); - if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"; - const keys = Object.keys(value).sort(); - return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(value[k])).join(",") + "}"; -} - +// Random, anonymous, per-install id — created once at 0600, only its hash is ever sent. function readInstallationId(create = true) { try { if (fs.existsSync(INSTALLATION_ID_PATH)) { @@ -136,10 +86,8 @@ function readInstallationId(create = true) { if (existing) return existing; } if (!create) return null; - fs.mkdirSync(path.dirname(INSTALLATION_ID_PATH), { recursive: true, mode: 0o700 }); try { fs.chmodSync(path.dirname(INSTALLATION_ID_PATH), 0o700); } catch {} - const installationId = crypto.randomUUID().replace(/-/g, ""); try { // 'wx' = O_CREAT | O_EXCL | O_WRONLY — atomic create, fails if it exists. @@ -147,10 +95,7 @@ function readInstallationId(create = true) { try { fs.writeFileSync(fd, installationId + "\n"); } finally { fs.closeSync(fd); } return installationId; } catch (err) { - if (err && err.code === "EEXIST") { - const existing = fs.readFileSync(INSTALLATION_ID_PATH, "utf8").trim(); - return existing || null; - } + if (err && err.code === "EEXIST") return fs.readFileSync(INSTALLATION_ID_PATH, "utf8").trim() || null; throw err; } } catch { @@ -158,15 +103,10 @@ function readInstallationId(create = true) { } } -function installationIdHash(create = true) { - return shortHash(readInstallationId(create), 32); -} - function telemetryIdentity(agentHarness, { createInstallation = true } = {}) { - const installHash = installationIdHash(createInstallation); - if (installHash) { - return [`expo-skills-installation:${installHash}`, { installation_id_hash: installHash }]; - } + const id = readInstallationId(createInstallation); + const installHash = id ? crypto.createHash("sha256").update(id).digest("hex").slice(0, 32) : null; + if (installHash) return [`expo-skills-installation:${installHash}`, { installation_id_hash: installHash }]; return [`expo-skills-events:${agentHarness}`, {}]; } @@ -174,27 +114,19 @@ function sendToPosthog(payload, { userAgent, timeoutMs }) { return new Promise((resolve, reject) => { const url = new URL("/i/v0/e/", POSTHOG_HOST); const body = Buffer.from(JSON.stringify(payload), "utf8"); - const req = https.request( - url, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": body.length, - "User-Agent": userAgent, - }, - timeout: timeoutMs, - }, - (res) => { - const chunks = []; - res.on("data", (chunk) => chunks.push(chunk)); - res.on("end", () => { - const status = res.statusCode || 0; - if (status >= 200 && status < 300) return resolve(); - reject(new Error(`HTTP ${status} ${Buffer.concat(chunks).toString("utf8")}`)); - }); - } - ); + const req = https.request(url, { + method: "POST", + headers: { "Content-Type": "application/json", "Content-Length": body.length, "User-Agent": userAgent }, + timeout: timeoutMs, + }, (res) => { + const chunks = []; + res.on("data", (c) => chunks.push(c)); + res.on("end", () => { + const status = res.statusCode || 0; + if (status >= 200 && status < 300) return resolve(); + reject(new Error(`HTTP ${status} ${Buffer.concat(chunks).toString("utf8")}`)); + }); + }); req.on("timeout", () => req.destroy(new Error("request timed out"))); req.on("error", reject); req.write(body); @@ -203,22 +135,14 @@ function sendToPosthog(payload, { userAgent, timeoutMs }) { } module.exports = { - POSTHOG_HOST, POSTHOG_PROJECT_API_KEY, SOURCE, - SCHEMA_VERSION, - INSTALLATION_ID_PATH, OPT_OUT_PATH, telemetryDisabled, telemetryConfigured, detectHarness, isCI, platformProps, - maybeShowFirstRunNotice, - shortHash, - stableStringify, - readInstallationId, - installationIdHash, telemetryIdentity, sendToPosthog, }; From d8c6f57e80798138114b0e5821ed289452390bb3 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 16:24:16 +0200 Subject: [PATCH 18/23] =?UTF-8?q?refactor(expo):=20drop=20scripts/package.?= =?UTF-8?q?json=20=E2=80=94=20use=20.cjs=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scripts/package.json existed only to pin "type":"commonjs" so require() works regardless of any parent "type":"module". Renaming the four scripts to .cjs declares CommonJS via the extension itself — Node and Bun both honor it — so the package.json is no longer needed and is removed. One fewer file, and the module type is now self-evident instead of hidden in a config file. - skill-event/skill-feedback/telemetry/telemetry_common .js -> .cjs - updated require()s, the run.sh/skill-event.sh/skill-feedback.sh references, and the SKILL.md / AGENTS.md / references mentions - verified under both node and bun, and through the run.sh wrapper chain Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/skills/expo-skill-feedback/SKILL.md | 2 +- .../expo-skill-feedback/references/telemetry.md | 2 +- .../expo-skill-feedback/scripts/package.json | 6 ------ .../skills/expo-skill-feedback/scripts/run.sh | 2 +- .../scripts/{skill-event.js => skill-event.cjs} | 2 +- .../expo-skill-feedback/scripts/skill-event.sh | 8 ++++---- .../{skill-feedback.js => skill-feedback.cjs} | 8 ++++---- .../scripts/skill-feedback.sh | 2 +- .../scripts/{telemetry.js => telemetry.cjs} | 16 ++++++++-------- ...{telemetry_common.js => telemetry_common.cjs} | 2 +- 11 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 plugins/expo/skills/expo-skill-feedback/scripts/package.json rename plugins/expo/skills/expo-skill-feedback/scripts/{skill-event.js => skill-event.cjs} (99%) rename plugins/expo/skills/expo-skill-feedback/scripts/{skill-feedback.js => skill-feedback.cjs} (94%) rename plugins/expo/skills/expo-skill-feedback/scripts/{telemetry.js => telemetry.cjs} (81%) rename plugins/expo/skills/expo-skill-feedback/scripts/{telemetry_common.js => telemetry_common.cjs} (98%) diff --git a/AGENTS.md b/AGENTS.md index c8e41c5..f71b887 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,4 +257,4 @@ codex plugin marketplace add expo/skills --ref main The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks; see the `expo-skill-feedback` skill's `references/telemetry.md` for why. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). -**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. +**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/skills/expo-skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md index 0d7fd26..46304c9 100644 --- a/plugins/expo/skills/expo-skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -64,7 +64,7 @@ script checks before sending. Easiest: ask your agent to **"turn off Expo skills telemetry."** Or run it: ```bash -sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.js" --off +sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --off ``` For CI or a global opt-out, set an env var instead (`--on` re-enables, `--status` checks): diff --git a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md index 4b48d25..b40e83d 100644 --- a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md +++ b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md @@ -22,7 +22,7 @@ Qualitative context is opt-in only, as a scrubbed note in `skill_feedback`'s `-- ## PostHog key -The `phc_…` key in `scripts/telemetry_common.js` is a **write-only, public** ingestion key +The `phc_…` key in `scripts/telemetry_common.cjs` is a **write-only, public** ingestion key (safe to commit; it's what makes telemetry on by default). Override per environment with `EXPO_SKILLS_POSTHOG_KEY`. Never put a *personal* key (`phx_…`) here. Strip the key (empty or `phc_REPLACE_ME`, e.g. a fork) and the scripts go inert. diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/package.json b/plugins/expo/skills/expo-skill-feedback/scripts/package.json deleted file mode 100644 index 0ebb97d..0000000 --- a/plugins/expo/skills/expo-skill-feedback/scripts/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "expo-skills-telemetry", - "private": true, - "type": "commonjs", - "description": "Zero-dependency telemetry scripts run by node or bun. No install step needed." -} diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/run.sh b/plugins/expo/skills/expo-skill-feedback/scripts/run.sh index 98f4a08..8fe78e3 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/run.sh +++ b/plugins/expo/skills/expo-skill-feedback/scripts/run.sh @@ -6,7 +6,7 @@ # script with it. If neither is installed, it exits 0 and does nothing, so a # missing runtime never blocks a tool call. # -# Usage: sh run.sh [args...] +# Usage: sh run.sh [args...] script="$1" [ -n "$script" ] || exit 0 diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs similarity index 99% rename from plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs index f67d0a3..0c31f77 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs @@ -18,7 +18,7 @@ const { platformProps, telemetryIdentity, sendToPosthog, -} = require("./telemetry_common.js"); +} = require("./telemetry_common.cjs"); const EVENT = "skill_invoked"; diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh index 595d0ee..99dc090 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh @@ -11,7 +11,7 @@ # child read stdin directly: POSIX assigns a backgrounded command's stdin to /dev/null # unless redirected, so the child would see no payload and emit nothing. Instead we read # stdin SYNCHRONOUSLY here (a fast local pipe read, not the network) into a temp file and -# hand the path to skill-event.js via --hook-input-file; it reads then unlinks the file. +# hand the path to skill-event.cjs via --hook-input-file; it reads then unlinks the file. # # stdout/stderr -> /dev/null on the child so a stray write after the harness pipe closes # can't raise SIGPIPE and kill the POST mid-flight. @@ -27,12 +27,12 @@ payload=$(mktemp "${TMPDIR:-/tmp}/expo-skills-hook.XXXXXX" 2>/dev/null) || exit cat > "$payload" if command -v setsid >/dev/null 2>&1; then - setsid sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & + setsid sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & elif command -v perl >/dev/null 2>&1; then perl -e 'use POSIX qw(setsid); setsid(); exec @ARGV or exit 127;' \ - sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & + sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & else - nohup sh "$dir/run.sh" "$dir/skill-event.js" --hook-input-file "$payload" "$@" /dev/null 2>&1 & + nohup sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & fi exit 0 diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs similarity index 94% rename from plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js rename to plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs index 29723da..19aea81 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs @@ -2,7 +2,7 @@ // Submit feedback about an Expo skill to PostHog. // // Usage: -// node skill-feedback.js --skill --rating --text "..." \ +// node skill-feedback.cjs --skill --rating --text "..." \ // [--about skill|expo] [--agent-harness ] [--dry-run] const { @@ -14,7 +14,7 @@ const { platformProps, telemetryIdentity, sendToPosthog, -} = require("./telemetry_common.js"); +} = require("./telemetry_common.cjs"); const EVENT_NAME = "skill_feedback"; const RATINGS = ["useful", "confusing", "bug", "idea", "other"]; @@ -78,11 +78,11 @@ async function main(argv) { const args = parseArgs(argv); if (telemetryDisabled()) { - console.error("skill-feedback: telemetry is disabled (opt-out file or EXPO_SKILLS_TELEMETRY/DO_NOT_TRACK); nothing sent. Re-enable with telemetry.js --on."); + console.error("skill-feedback: telemetry is disabled (opt-out file or EXPO_SKILLS_TELEMETRY/DO_NOT_TRACK); nothing sent. Re-enable with telemetry.cjs --on."); return 0; } if (!telemetryConfigured() && !args.dryRun) { - console.error("skill-feedback: no PostHog key in this build (key stripped to placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or restore the key in telemetry_common.js."); + console.error("skill-feedback: no PostHog key in this build (key stripped to placeholder); nothing sent. Set EXPO_SKILLS_POSTHOG_KEY or restore the key in telemetry_common.cjs."); return 0; } diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh index 3731fa7..f14f1bc 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh @@ -8,4 +8,4 @@ # Usage: sh skill-feedback.sh --skill --rating --text "..." [--dry-run] dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -exec sh "$dir/run.sh" "$dir/skill-feedback.js" "$@" +exec sh "$dir/run.sh" "$dir/skill-feedback.cjs" "$@" diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs similarity index 81% rename from plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js rename to plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs index 64f8770..8694449 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs @@ -2,9 +2,9 @@ // Turn Expo skills usage telemetry on or off, or check its status. // // Usage: -// node telemetry.js --status # show whether telemetry is on/off and why -// node telemetry.js --off # disable (writes the opt-out file) -// node telemetry.js --on # re-enable (removes the opt-out file) +// node telemetry.cjs --status # show whether telemetry is on/off and why +// node telemetry.cjs --off # disable (writes the opt-out file) +// node telemetry.cjs --on # re-enable (removes the opt-out file) // // The opt-out file is the reliable switch: it works no matter how the agent was // launched. The DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY env vars also disable @@ -12,7 +12,7 @@ const fs = require("fs"); const path = require("path"); -const { OPT_OUT_PATH, telemetryConfigured, isCI } = require("./telemetry_common.js"); +const { OPT_OUT_PATH, telemetryConfigured, isCI } = require("./telemetry_common.cjs"); function disabledByEnv() { const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); @@ -33,9 +33,9 @@ function printStatus() { if (byEnv) reasons.push(`env var ${byEnv}`); console.log(`Expo skills telemetry: DISABLED — via ${reasons.join(" and ")}.`); } else if (!telemetryConfigured()) { - console.log("Expo skills telemetry: ENABLED, but no PostHog key in this build (key stripped to placeholder) — nothing is created or sent. Disable permanently with: telemetry.js --off"); + console.log("Expo skills telemetry: ENABLED, but no PostHog key in this build (key stripped to placeholder) — nothing is created or sent. Disable permanently with: telemetry.cjs --off"); } else { - console.log("Expo skills telemetry: ENABLED (anonymous). Disable with: telemetry.js --off"); + console.log("Expo skills telemetry: ENABLED (anonymous). Disable with: telemetry.cjs --off"); } } @@ -45,7 +45,7 @@ if (cmd === "--off" || cmd === "--disable") { fs.mkdirSync(path.dirname(OPT_OUT_PATH), { recursive: true, mode: 0o700 }); fs.writeFileSync(OPT_OUT_PATH, "Expo skills telemetry disabled by user.\n"); console.log(`Telemetry disabled — wrote ${OPT_OUT_PATH}`); - console.log("Re-enable any time with: telemetry.js --on"); + console.log("Re-enable any time with: telemetry.cjs --on"); } else if (cmd === "--on" || cmd === "--enable") { try { fs.rmSync(OPT_OUT_PATH, { force: true }); } catch {} const byEnv = disabledByEnv(); @@ -54,6 +54,6 @@ if (cmd === "--off" || cmd === "--disable") { } else if (cmd === "--status" || cmd === undefined) { printStatus(); } else { - console.error(`Unknown option: ${cmd}\nUsage: telemetry.js [--status | --off | --on]`); + console.error(`Unknown option: ${cmd}\nUsage: telemetry.cjs [--status | --off | --on]`); process.exit(2); } diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs similarity index 98% rename from plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js rename to plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs index 5588f7b..d4b3c1b 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.js +++ b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs @@ -25,7 +25,7 @@ const INSTALLATION_ID_PATH = path.join(os.homedir(), ".expo-skills", "installati // Persistent opt-out marker. Checked before anything is sent, so it works // regardless of how the agent was launched (env vars don't always reach hook -// subprocesses). Toggle it with scripts/telemetry.js --off / --on. +// subprocesses). Toggle it with scripts/telemetry.cjs --off / --on. const OPT_OUT_PATH = path.join(os.homedir(), ".expo-skills", "opt-out"); // CI detection — skip telemetry in automated environments so usage data reflects From 422f03e490367c5f08d3d6c76c6c8679ab8a0e38 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 16:27:00 +0200 Subject: [PATCH 19/23] refactor(expo): prune expo-skill-feedback SKILL.md to its two jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the writing-great-skills lens to the skill body — the agent that loads this skill only needs to know how to submit feedback and how to turn telemetry off. - Drop "## Runtime" (node/bun/run.sh internals — a no-op for the agent; it just runs the command). - Drop the "## The two signals" table (duplicated the intro; the full mechanism lives in references/telemetry.md). - Drop "## Notes for maintainers" (maintainer info in the agent's context) — keep only a one-line pointer to references/telemetry.md. - Tighten the intro, submit, and opt-out sections. SKILL.md ~90 -> 52 lines. No behavior change; the two functions + the privacy one-liner + the references pointer remain. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../expo/skills/expo-skill-feedback/SKILL.md | 78 +++++-------------- 1 file changed, 19 insertions(+), 59 deletions(-) diff --git a/plugins/expo/skills/expo-skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md index 46304c9..c37f57d 100644 --- a/plugins/expo/skills/expo-skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -1,45 +1,21 @@ --- name: expo-skill-feedback -description: Submit feedback on an Expo skill — or on Expo itself — or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving; when something fell short because of Expo (an SDK bug or confusing framework behavior) rather than the skill; or when the user wants to disable, opt out of, turn off, stop, or understand the anonymous usage tracking these skills send. +description: Submit feedback on an Expo skill — or on Expo itself — or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving; when something fell short because of Expo (an SDK bug or confusing framework behavior) rather than the skill; or when the user wants to disable, turn off, opt out of, or understand the anonymous usage tracking these skills send. --- -# Skill Feedback +# Expo Skill Feedback -The shared telemetry + feedback helper for the Expo skills on **Claude Code**. It powers -two **anonymous** signals, sent to PostHog so the Expo team can see how skills are used and -where they fall short: - -- **Automatic usage** (`skill_invoked`) — emitted by the plugin's hooks when a skill runs. - No manual step. **On by default.** -- **Explicit feedback** (`skill_feedback`) — a rating + short note you (or the agent, with - the user's OK) send deliberately by running the command below. - -Both send only a short **hash** of a random local installation ID -(`~/.expo-skills/installation-id`, created on first send). Hostnames, usernames, emails, -file paths, source code, **prompts, and chat history** are **never** sent. Telemetry is -**skipped automatically in CI**. To turn everything off, see *Turning it off* below. - -## Runtime - -Scripts are plain JavaScript and run under **Node or Bun** — both ship the built-ins they -use (`crypto`, `https`, `fs`). `scripts/run.sh` picks whichever is installed (`node`, then -`bun`) and does nothing if neither is found, so a missing runtime never blocks a tool call. -Node/Bun is a hard requirement for Expo development, so it's effectively always present. - -## The two signals - -| Event | Trigger | Mechanism | -| --- | --- | --- | -| `skill_invoked` | A skill runs — by the **AI** (`Skill` tool) or a **user** `/slash` command | Automatic, via the plugin's `hooks/hooks.json`; tagged `initiator: ai`\|`user`, scoped to this plugin | -| `skill_feedback` | You deliberately send a rating + note | Manual — the command below | +Send anonymous feedback on an Expo skill — or on Expo itself — and turn the bundled usage +telemetry on or off. Feedback goes to the Expo team via PostHog. Only the skill name, your +rating + note, platform, and a hash of a random local install id are sent — never code, +prompts, file paths, secrets, or personal data. (A separate automatic `skill_invoked` event +fires when a skill runs; *Turning it off* silences both.) ## Submitting feedback -**We want this — please be proactive.** When an Expo skill was wrong, confusing, outdated, -or missing something, send a quick note so the team can fix it (a note when it worked well -helps too). Keep it to 1-3 short sentences, prefer concrete observations, and pass **the -skill you're giving feedback on** to `--skill` — usually the skill whose footer sent you -here, not `expo-skill-feedback` itself (`${CLAUDE_PLUGIN_ROOT}` resolves to the plugin's install path): +Be proactive: when a skill was wrong, confusing, outdated, or missing — or the trouble is +with **Expo itself** — send 1-3 concrete sentences. Pass the skill you're rating to +`--skill` (the one whose footer sent you here, not `expo-skill-feedback`): ```bash sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.sh" \ @@ -47,41 +23,25 @@ sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.sh" --text "TestFlight steps were clear and worked first try." ``` -Ratings: `useful`, `confusing`, `bug`, `idea`, `other`. Add `--dry-run` to print the payload without sending. +- `--rating`: `useful` · `confusing` · `bug` · `idea` · `other` +- `--about`: `skill` (default) · `expo` (the issue is Expo itself, not the skill) +- `--dry-run` prints the payload without sending -If the trouble is with **Expo itself** rather than the skill (e.g. an SDK bug or confusing -framework behavior the skill couldn't paper over), add `--about expo` (default `skill`) so -the team can tell framework issues from skill issues. - -**Never include** secrets, private data, source code, long prompts, stack traces, API -keys, or tokens. +Never include secrets, source code, long prompts, or stack traces. ## Turning it off -Telemetry is anonymous and **on by default**. Opting out silences **both** signals. The -reliable switch is the bundled toggle; it writes `~/.expo-skills/opt-out`, which every -script checks before sending. Easiest: ask your agent to **"turn off Expo skills -telemetry."** Or run it: +Telemetry is anonymous and on by default; opting out silences both feedback and the +automatic event. Ask your agent to **"turn off Expo skills telemetry"**, or run: ```bash sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --off ``` -For CI or a global opt-out, set an env var instead (`--on` re-enables, `--status` checks): - -```bash -export EXPO_SKILLS_TELEMETRY=0 # or: false / off / no -export DO_NOT_TRACK=1 # cross-tool standard, https://consoledonottrack.com -``` - -When disabled, no installation ID is created and nothing is sent. Uninstalling the plugin -removes everything. - -## Notes for maintainers +`--on` re-enables, `--status` checks. For CI or a global switch: `EXPO_SKILLS_TELEMETRY=0` +or `DO_NOT_TRACK=1`. -`skill_invoked` is wired for **Claude Code** via the plugin's `hooks/hooks.json`. See -`./references/telemetry.md` for the event shape, PostHog key configuration, why other -harnesses have no automatic signal, and the prompt-capture and Windows notes. +Maintainer detail — event shape, the PostHog key, why Claude Code only → `./references/telemetry.md`. --- From e5d48abcfedecffbbbcbb81a7803e4f202fda82c Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 16:51:41 +0200 Subject: [PATCH 20/23] =?UTF-8?q?refactor(expo):=20drop=20references/telem?= =?UTF-8?q?etry.md=20=E2=80=94=20it=20duplicated=20the=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ~90% of references/telemetry.md restated the code: the event shape is built in skill-event.cjs / skill-feedback.cjs (and summarized in SKILL.md's privacy line), and the PostHog-key override is already a comment in telemetry_common.cjs. The only unique content — why Claude Code only — was one sentence; folded it into the AGENTS.md telemetry note (which previously just pointed here). Removed the SKILL.md and AGENTS.md pointers and deleted the file + the now-empty references/ dir. expo-skill-feedback is now just SKILL.md + scripts/. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- .../expo/skills/expo-skill-feedback/SKILL.md | 2 - .../references/telemetry.md | 41 ------------------- 3 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 plugins/expo/skills/expo-skill-feedback/references/telemetry.md diff --git a/AGENTS.md b/AGENTS.md index f71b887..ceac26e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks; see the `expo-skill-feedback` skill's `references/telemetry.md` for why. When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). +The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks (Codex's `plugin_hooks` is a *removed* feature with no skill-invocation hook event, verified against codex 0.138 + the `openai/codex` source; Cursor has no plugin-hook system). When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). **Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/skills/expo-skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md index c37f57d..c6c73b6 100644 --- a/plugins/expo/skills/expo-skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -41,8 +41,6 @@ sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_P `--on` re-enables, `--status` checks. For CI or a global switch: `EXPO_SKILLS_TELEMETRY=0` or `DO_NOT_TRACK=1`. -Maintainer detail — event shape, the PostHog key, why Claude Code only → `./references/telemetry.md`. - --- ## Expo Skill Feedback diff --git a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md b/plugins/expo/skills/expo-skill-feedback/references/telemetry.md deleted file mode 100644 index b40e83d..0000000 --- a/plugins/expo/skills/expo-skill-feedback/references/telemetry.md +++ /dev/null @@ -1,41 +0,0 @@ -# Telemetry internals (maintainer reference) - -Background for the scripts under `scripts/`. The runtime surface — what's sent, how to -submit feedback, how to turn it off — is in `../SKILL.md`; this file is for auditing or -changing the telemetry. - -## Event shape - -- `event`: `skill_invoked` | `skill_feedback` -- `distinct_id`: `expo-skills-installation:` -- `properties.source`: `expo-skills` · `properties.$process_person_profile`: `false` -- `properties.skill`: skill folder name (e.g. `expo-deployment`) -- `properties.agent_harness`: `claude-code` (auto-detected), else `unknown`; `--agent-harness` overrides -- `properties.os` / `arch`: platform (e.g. `macos` / `arm64`), non-PII -- `properties.installation_id_hash`: anonymous hash of the local random install id (the raw id never leaves the machine) -- `skill_invoked` adds `properties.initiator`: `ai` (Skill tool) | `user` (`/slash`) -- `skill_feedback` adds `properties.rating`, `properties.feedback_text`, and `properties.about`: `skill` (default) | `expo` (the trouble is Expo itself, not the skill) - -That list **is** the whole privacy surface: only the skill name, anonymous hashes, and -platform leave the machine — never prompts, chat history, code, file paths, or tool inputs. -Qualitative context is opt-in only, as a scrubbed note in `skill_feedback`'s `--text`. - -## PostHog key - -The `phc_…` key in `scripts/telemetry_common.cjs` is a **write-only, public** ingestion key -(safe to commit; it's what makes telemetry on by default). Override per environment with -`EXPO_SKILLS_POSTHOG_KEY`. Never put a *personal* key (`phx_…`) here. Strip the key (empty -or `phc_REPLACE_ME`, e.g. a fork) and the scripts go inert. - -## Why Claude Code only - -`skill_invoked` fires from `hooks/hooks.json` — `PostToolUse[Skill]` (→ `ai`) and -`UserPromptExpansion` (→ `user`), both verified end-to-end. Other harnesses get no automatic -signal and we ship no hooks file for them: on **Codex** (codex-cli 0.138 + the `openai/codex` -source) plugin hooks are a removed feature (`Feature::PluginHooks` = `Stage::Removed`) and -there's no skill event in `HookEventName`; **Cursor** has no plugin hooks. If OpenAI restores -`plugin_hooks` *and* adds a skill event, wiring is small — Codex already aliases -`${CLAUDE_PLUGIN_ROOT}`. - -The *how* (detach, temp-file payload handoff, Windows fail-open) lives in the scripts' own -comments — read those before changing them. From 772d40a907ad3b8a2b6c837871d39130246ef64e Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 18 Jun 2026 18:41:22 +0200 Subject: [PATCH 21/23] refactor(expo): run skill telemetry via node, drop sh wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skill_invoked hook and the feedback/opt-out commands shelled out via `sh`, which isn't present on stock Windows. Switch every entry point to call `node` directly — portable across cmd/PowerShell/sh, and the convention top skills.sh skills already use ("name the interpreter, relative path"). Fold the non-blocking detach into skill-event.cjs itself: the foreground process reads the hook payload from stdin, resolves the skill, runs the cheap local gates, then re-execs a detached copy (spawn detached + unref + windowsHide) to do the PostHog POST off the agent's critical path. Because Node reads stdin before detaching, the old temp-file / setsid / perl / nohup handoff is gone. - hooks/hooks.json: sh skill-event.sh -> node skill-event.cjs --detach - SKILL.md: sh skill-feedback.sh / run.sh -> node skill-feedback.cjs / telemetry.cjs - AGENTS.md: prefix the opt-out command with node - delete run.sh, skill-event.sh, skill-feedback.sh (-67 lines of shell) Same anonymous, opt-out, Claude-Code-only behavior; now cross-platform. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- plugins/expo/hooks/hooks.json | 4 +- .../expo/skills/expo-skill-feedback/SKILL.md | 4 +- .../skills/expo-skill-feedback/scripts/run.sh | 18 ---- .../scripts/skill-event.cjs | 87 ++++++++++++------- .../scripts/skill-event.sh | 38 -------- .../scripts/skill-feedback.sh | 11 --- 7 files changed, 60 insertions(+), 104 deletions(-) delete mode 100644 plugins/expo/skills/expo-skill-feedback/scripts/run.sh delete mode 100644 plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh delete mode 100644 plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh diff --git a/AGENTS.md b/AGENTS.md index ceac26e..d958992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,4 +257,4 @@ codex plugin marketplace add expo/skills --ref main The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks (Codex's `plugin_hooks` is a *removed* feature with no skill-invocation hook event, verified against codex 0.138 + the `openai/codex` source; Cursor has no plugin-hook system). When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). -**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. +**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `node plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. diff --git a/plugins/expo/hooks/hooks.json b/plugins/expo/hooks/hooks.json index 23dfd40..fbc5913 100644 --- a/plugins/expo/hooks/hooks.json +++ b/plugins/expo/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.sh\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.cjs\" --skill auto --initiator ai --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --detach --quiet", "timeout": 5 } ] @@ -17,7 +17,7 @@ "hooks": [ { "type": "command", - "command": "sh \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.sh\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --quiet", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-event.cjs\" --skill auto --initiator user --plugin-root \"${CLAUDE_PLUGIN_ROOT}\" --detach --quiet", "timeout": 5 } ] diff --git a/plugins/expo/skills/expo-skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md index c6c73b6..219e413 100644 --- a/plugins/expo/skills/expo-skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -18,7 +18,7 @@ with **Expo itself** — send 1-3 concrete sentences. Pass the skill you're rati `--skill` (the one whose footer sent you here, not `expo-skill-feedback`): ```bash -sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.sh" \ +node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.cjs" \ --skill expo-deployment --rating useful \ --text "TestFlight steps were clear and worked first try." ``` @@ -35,7 +35,7 @@ Telemetry is anonymous and on by default; opting out silences both feedback and automatic event. Ask your agent to **"turn off Expo skills telemetry"**, or run: ```bash -sh "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/run.sh" "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --off +node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --off ``` `--on` re-enables, `--status` checks. For CI or a global switch: `EXPO_SKILLS_TELEMETRY=0` diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/run.sh b/plugins/expo/skills/expo-skill-feedback/scripts/run.sh deleted file mode 100644 index 8fe78e3..0000000 --- a/plugins/expo/skills/expo-skill-feedback/scripts/run.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# Launch a bundled telemetry script with whatever JS runtime is available. -# -# Expo development requires a JS runtime, but it may be Node *or* Bun (and the -# binary may not be named `node`). This picks the first one present and runs the -# script with it. If neither is installed, it exits 0 and does nothing, so a -# missing runtime never blocks a tool call. -# -# Usage: sh run.sh [args...] - -script="$1" -[ -n "$script" ] || exit 0 -shift - -if command -v node >/dev/null 2>&1; then exec node "$script" "$@"; fi -if command -v bun >/dev/null 2>&1; then exec bun "$script" "$@"; fi - -exit 0 diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs index 0c31f77..220924e 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs @@ -1,10 +1,15 @@ #!/usr/bin/env node -// Submit a `skill_invoked` event to PostHog. +// Submit a `skill_invoked` event to PostHog — fire-and-forget. // -// Fires from two Claude Code hooks (see ../../../hooks/hooks.json): -// - PostToolUse[Skill] -> the AI invoked a skill (--initiator ai) -// - UserPromptExpansion -> a user ran a /slash command (--initiator user) -// Reads the hook payload from stdin; never blocks (exits 0 under --quiet). +// Invoked two ways, both cross-platform (plain `node`, no shell wrapper, so it runs the +// same on macOS / Linux / Windows): +// 1. From Claude Code hooks (../../../hooks/hooks.json) as +// `node skill-event.cjs --skill auto --initiator --plugin-root --detach --quiet`. +// The foreground process reads the hook payload from stdin, resolves which skill ran, +// runs the cheap local gates, then — for `--detach` — re-launches a DETACHED copy of +// itself to do the network POST and returns immediately, so the agent turn never blocks. +// 2. Directly (that detached child, or manual / --dry-run testing) with a resolved +// `--skill `, which builds the event and sends it inline. const fs = require("fs"); const path = require("path"); @@ -23,7 +28,7 @@ const { const EVENT = "skill_invoked"; function parseArgs(argv) { - const args = { skill: "", agentHarness: "", initiator: "", pluginRoot: "", hookInputFile: "", dryRun: false, quiet: false }; + const args = { skill: "", agentHarness: "", initiator: "", pluginRoot: "", dryRun: false, quiet: false, detach: false }; for (let i = 0; i < argv.length; i++) { const flag = argv[i]; const next = () => argv[++i] || ""; @@ -32,7 +37,7 @@ function parseArgs(argv) { case "--agent-harness": args.agentHarness = next(); break; case "--initiator": args.initiator = next(); break; case "--plugin-root": args.pluginRoot = next(); break; - case "--hook-input-file": args.hookInputFile = next(); break; + case "--detach": args.detach = true; break; case "--dry-run": args.dryRun = true; break; case "--quiet": args.quiet = true; break; default: break; // ignore unknown flags @@ -41,21 +46,13 @@ function parseArgs(argv) { return args; } -// Read the hook payload. The detaching wrapper (skill-event.sh) stashes stdin in a temp -// file and passes --hook-input-file, because a backgrounded process's stdin is /dev/null -// (POSIX), so the detached send can't read the pipe directly. We read that file then -// unlink it. With no file (foreground / direct invocation), fall back to stdin (fd 0). -function readHookInput(file) { +// Read the hook payload from stdin (fd 0) and parse it as JSON. Only used to resolve +// `--skill auto` in the foreground hook process; the detached sender is handed the +// already-resolved name and never touches stdin. +function readHookInput() { try { - let raw; - if (file) { - raw = fs.readFileSync(file, "utf8"); - try { fs.unlinkSync(file); } catch {} - } else { - if (process.stdin.isTTY) return {}; - raw = fs.readFileSync(0, "utf8"); // fd 0 = stdin - } - raw = (raw || "").trim(); + if (process.stdin.isTTY) return {}; + const raw = (fs.readFileSync(0, "utf8") || "").trim(); // fd 0 = stdin if (!raw) return {}; const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; @@ -71,8 +68,6 @@ function readHookInput(file) { // - Claude Code Skill tool: tool_input.skill (e.g. "expo:expo-observe") // - Claude Code /slash command: command_name (UserPromptExpansion) // - other payload shapes: tool_input.skill_name, top-level skill / skill_name -// We check every plausible field so the resolver stays robust across payload shapes; the -// strict skillBelongsToPlugin() scoping below keeps it safe even when permissive. // Plugin skills are namespaced (e.g. "expo:expo-observe") — keep the final segment. function skillFromHook(hookInput) { const ti = hookInput && typeof hookInput.tool_input === "object" && hookInput.tool_input ? hookInput.tool_input : {}; @@ -83,7 +78,7 @@ function skillFromHook(hookInput) { } function pluginRootFor(args) { - // Self-derive from this script's location: /skills/skill-feedback/scripts. + // Self-derive from this script's location: /skills/expo-skill-feedback/scripts. return args.pluginRoot || path.resolve(__dirname, "..", "..", ".."); } @@ -117,20 +112,48 @@ function eventPayload(skill, args) { return { api_key: POSTHOG_PROJECT_API_KEY, event: EVENT, distinct_id: distinctId, timestamp, properties }; } +// Re-launch this script DETACHED to perform the network POST off the agent's critical +// path — the cross-platform equivalent of `node skill-event.cjs … &`. We pass the already +// resolved `--skill ` (not "auto") and drop `--detach`, so the child sends inline and +// never reads stdin or re-detaches. It runs under the same runtime that launched us +// (process.execPath = node or bun) and inherits our env (CLAUDECODE, EXPO_SKILLS_*, …). +// windowsHide avoids a console-window flash on Windows; failures are ignored (best-effort). +function spawnDetachedSend(skill, args) { + try { + const { spawn } = require("child_process"); + const childArgs = [__filename, "--skill", skill, "--quiet"]; + if (args.initiator.trim()) childArgs.push("--initiator", args.initiator.trim()); + if (args.agentHarness.trim()) childArgs.push("--agent-harness", args.agentHarness.trim()); + const child = spawn(process.execPath, childArgs, { detached: true, stdio: "ignore", windowsHide: true }); + child.unref(); + } catch { + // best-effort: if the child can't be spawned, skip the send rather than block + } +} + async function main(argv) { const args = parseArgs(argv); - // Read (and unlink) the hook payload FIRST so the temp file from skill-event.sh is - // cleaned up on every path below, including the telemetry-off early returns. (It can - // still be orphaned if no JS runtime starts at all; those are mode 0600 and OS-reaped.) - const hookInput = readHookInput(args.hookInputFile); - if (telemetryDisabled()) return 0; - if (!telemetryConfigured() && !args.dryRun) return 0; // no key in this build (e.g. a fork) -> stay inert + // Resolve which skill ran. `--skill auto` means "read it from the hook payload on + // stdin" — which must happen HERE, in the foreground hook process, because a detached + // child's stdin is /dev/null. let skill = args.skill.trim(); - if (skill === "auto") skill = skillFromHook(hookInput); - if (!skill) return 0; // not a skill invocation + if (skill === "auto") skill = skillFromHook(readHookInput()); + + // Cheap, local, no-network gates: decide up front whether anything will be sent, so the + // common "not an Expo skill / opted out" cases cost nothing and never spawn a child. + if (!skill) return 0; // not a skill invocation + if (telemetryDisabled()) return 0; + if (!telemetryConfigured() && !args.dryRun) return 0; // no key in this build (e.g. a fork) -> inert if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; // not one of ours + // Hook path: hand the network POST to a detached copy of ourselves so the turn never + // blocks on it, then return immediately. (--dry-run stays inline so it can be inspected.) + if (args.detach && !args.dryRun) { + spawnDetachedSend(skill, args); + return 0; + } + const payload = eventPayload(skill, args); if (args.dryRun) { diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh deleted file mode 100644 index 99dc090..0000000 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -# Emit an anonymous `skill_invoked` usage event — FIRE AND FORGET. -# -# Launched from a Claude Code hook (PostToolUse / UserPromptExpansion), which waits for -# the command to exit. We must NOT block the agent turn on a network round-trip, so the -# actual send is detached into its own session and this wrapper returns immediately; the -# child finishes the POST off the critical path even if the hook's process group is torn -# down (`setsid`/POSIX::setsid put it in a fresh group; `nohup` is the last resort). -# -# The hook payload (which skill was invoked) arrives on stdin. We canNOT let the detached -# child read stdin directly: POSIX assigns a backgrounded command's stdin to /dev/null -# unless redirected, so the child would see no payload and emit nothing. Instead we read -# stdin SYNCHRONOUSLY here (a fast local pipe read, not the network) into a temp file and -# hand the path to skill-event.cjs via --hook-input-file; it reads then unlinks the file. -# -# stdout/stderr -> /dev/null on the child so a stray write after the harness pipe closes -# can't raise SIGPIPE and kill the POST mid-flight. -# -# Usage: sh skill-event.sh --skill --initiator [--plugin-root ] [--quiet] - -dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) - -# mktemp gives a fresh, unpredictable, 0600 file. If it's somehow unavailable, bail rather -# than fall back to a predictable /tmp name (world-readable + symlink-followable, and the -# payload can hold tool I/O). Telemetry is best-effort, so skipping a send is fine. -payload=$(mktemp "${TMPDIR:-/tmp}/expo-skills-hook.XXXXXX" 2>/dev/null) || exit 0 -cat > "$payload" - -if command -v setsid >/dev/null 2>&1; then - setsid sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & -elif command -v perl >/dev/null 2>&1; then - perl -e 'use POSIX qw(setsid); setsid(); exec @ARGV or exit 127;' \ - sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & -else - nohup sh "$dir/run.sh" "$dir/skill-event.cjs" --hook-input-file "$payload" "$@" /dev/null 2>&1 & -fi - -exit 0 diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh deleted file mode 100644 index f14f1bc..0000000 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Submit explicit feedback about an Expo skill. -# -# Thin wrapper so skill footers can call ONE short path instead of repeating -# `run.sh` + the script path. Runs in the FOREGROUND and awaits the send, since -# this path reports success/failure back to the user. -# -# Usage: sh skill-feedback.sh --skill --rating --text "..." [--dry-run] - -dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -exec sh "$dir/run.sh" "$dir/skill-feedback.cjs" "$@" From 061f8077ea16771e50eb5af8a936bfc0c6154cb6 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 2 Jul 2026 15:39:38 +0200 Subject: [PATCH 22/23] feat(expo): make telemetry opt-in (off by default), drop skill footers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing is sent — no skill_invoked, no feedback — until the user enables telemetry. The hook stays installed but no-ops behind the new telemetryActive() gate. Precedence: explicit-off (DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY=0) → CI (never sends) → explicit-on (=1) → opt-in marker (~/.expo-skills/opt-in) → default off. - telemetry.cjs: --on writes the opt-in marker, --off removes it (the default); --status explains the current state and why - Conversational switch: the expo-skill-feedback skill teaches the agent to run --on when the user says "enable Expo skills telemetry" (or agrees when offered), to answer status questions by running --status rather than from memory, and to never enable telemetry on its own — a refused feedback send asks the user once, then respects the answer - Remove the "Expo Skill Feedback" footer from all 16 product skills: SKILL.md bodies aren't templated, so a footer can't reflect the opt-in state; feedback now lives only in the expo-skill-feedback skill - READMEs + AGENTS.md: replace the on-by-default telemetry sections with the shorter opt-in story Verified end-to-end in real claude -p sessions with the plugin loaded: "enable expo skills telemetry" triggers the skill and writes the marker; status queries run --status; the hook fires but spawns nothing while off, and sends through the detached child once enabled. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 4 +- README.md | 17 ++-- plugins/expo/README.md | 4 +- plugins/expo/skills/add-app-clip/SKILL.md | 8 -- .../expo/skills/building-native-ui/SKILL.md | 8 -- .../expo/skills/eas-update-insights/SKILL.md | 8 -- plugins/expo/skills/expo-api-routes/SKILL.md | 8 -- plugins/expo/skills/expo-brownfield/SKILL.md | 8 -- .../expo/skills/expo-cicd-workflows/SKILL.md | 8 -- plugins/expo/skills/expo-deployment/SKILL.md | 8 -- plugins/expo/skills/expo-dev-client/SKILL.md | 8 -- plugins/expo/skills/expo-examples/SKILL.md | 8 -- plugins/expo/skills/expo-module/SKILL.md | 8 -- plugins/expo/skills/expo-observe/SKILL.md | 8 -- .../expo/skills/expo-skill-feedback/SKILL.md | 40 ++++++---- .../scripts/skill-event.cjs | 4 +- .../scripts/skill-feedback.cjs | 6 +- .../expo-skill-feedback/scripts/telemetry.cjs | 80 ++++++++++--------- .../scripts/telemetry_common.cjs | 57 +++++++++---- .../expo/skills/expo-tailwind-setup/SKILL.md | 8 -- plugins/expo/skills/expo-ui/SKILL.md | 8 -- .../expo/skills/native-data-fetching/SKILL.md | 8 -- plugins/expo/skills/upgrading-expo/SKILL.md | 8 -- plugins/expo/skills/use-dom/SKILL.md | 8 -- 24 files changed, 119 insertions(+), 221 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d958992..eba9028 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin sends **anonymous**, on-by-default usage telemetry and collects skill feedback (details in the `expo-skill-feedback` skill). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (the footer command). Other harnesses send nothing — Codex/Cursor can't host plugin hooks (Codex's `plugin_hooks` is a *removed* feature with no skill-invocation hook event, verified against codex 0.138 + the `openai/codex` source; Cursor has no plugin-hook system). When adding a skill, copy the `## Expo Skill Feedback` footer from any existing skill and swap in your skill's `--skill ` — the footer is identical everywhere (it references `${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/...`, so no per-skill path edits). +The `expo` plugin can send **anonymous**, **opt-in (off-by-default)** usage telemetry and collect skill feedback (details in the `expo-skill-feedback` skill) — nothing is sent until the user enables it. The hook is always present but no-ops until enabled (`telemetryActive()` gate in `telemetry_common.cjs`). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (via the `expo-skill-feedback` skill). Other harnesses send nothing — Codex/Cursor can't host plugin hooks (Codex's `plugin_hooks` is a *removed* feature with no skill-invocation hook event, verified against codex 0.138 + the `openai/codex` source; Cursor has no plugin-hook system). Product skills carry **no feedback footer** — feedback is offered only through the `expo-skill-feedback` skill itself (`SKILL.md` bodies aren't templated, so a per-skill footer can't be conditionally hidden; we ship none). New skills need no footer or per-skill edits. -**Turn it off:** ask your agent to "turn off Expo skills telemetry", run `node plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --off`, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. +**Turn it on (off by default):** ask your agent to "turn on Expo skills telemetry", run `node plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --on`, or set `EXPO_SKILLS_TELEMETRY=1`. Disable again with `--off`, `EXPO_SKILLS_TELEMETRY=0`, or `DO_NOT_TRACK=1`. The gate precedence: explicit-off (DO_NOT_TRACK / `=0`) → CI (never sends) → explicit-on (`=1`) → opt-in marker (`~/.expo-skills/opt-in`) → default off. diff --git a/README.md b/README.md index 5ed0931..931500a 100644 --- a/README.md +++ b/README.md @@ -149,20 +149,13 @@ Expo documentation, Expo CLI, and EAS CLI are the source of truth. These skills ## Usage telemetry & feedback -These skills send **anonymous** usage events on **Claude Code** so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent. We never send source code, prompts, file paths, or personal data. Scripts are zero-dependency and run under Node or Bun. +**Off by default — these skills send nothing unless you turn telemetry on.** When enabled (Claude Code only), they send anonymous usage events: the skill name, platform, and a hash of a random local install id. Never code, prompts, file paths, or personal data. -- **Usage:** when a skill is invoked — by the AI or a user `/slash` command (`skill_invoked`, tagged with `initiator` = `ai` or `user`). -- **Feedback:** every skill ends with an *Expo Skill Feedback* footer — a one-line command to send a quick rating + note. +- **Turn on:** ask your agent to **"enable Expo skills telemetry"**, or set `EXPO_SKILLS_TELEMETRY=1`. +- **Turn off:** ask it to disable telemetry, or set `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. CI never sends. +- **Feedback:** with telemetry on, the `expo-skill-feedback` skill sends a quick anonymous rating + note about a skill. -The wiring is Claude Code-specific (plugin hooks + footer), so other agents send nothing. - -Turn it off any time — the simplest way is to ask your agent: **"turn off Expo skills telemetry"** (it runs the bundled toggle, which writes `~/.expo-skills/opt-out` — a persistent switch that works regardless of how the agent was launched). For a global or CI opt-out, set an env var instead: - -```bash -export DO_NOT_TRACK=1 # or: export EXPO_SKILLS_TELEMETRY=0 -``` - -See the `expo-skill-feedback` skill for full details, the event shape, and how to configure the PostHog project key. +Details live in the `expo-skill-feedback` skill. ## License diff --git a/plugins/expo/README.md b/plugins/expo/README.md index 602a94d..b4510df 100644 --- a/plugins/expo/README.md +++ b/plugins/expo/README.md @@ -86,9 +86,7 @@ Official AI agent skills from the Expo team for building, deploying, upgrading, ## Usage telemetry & feedback -These skills send **anonymous** usage events (which skill ran, and whether the AI or a user `/slash` command invoked it) so the Expo team can see how they're used and improve them. On first use a random ID is created locally at `~/.expo-skills/installation-id`; only a hash of it is sent — never source code, prompts, file paths, or personal data. Every skill also ends with an *Expo Skill Feedback* footer for a quick rating + note. Both run on **Claude Code** (via the plugin's hooks and footer); other agents send nothing. - -Turn it off any time — ask your agent to **"turn off Expo skills telemetry"**, or set `DO_NOT_TRACK=1` (or `EXPO_SKILLS_TELEMETRY=0`). See the **expo-skill-feedback** skill for full details, the event shape, and how to configure the PostHog project key. +**Off by default** — nothing is sent unless you turn telemetry on. When enabled (Claude Code only), the plugin sends anonymous usage events — the skill name, platform, and a hash of a random local install id — never code, prompts, file paths, or personal data. Ask your agent to **"enable Expo skills telemetry"** to opt in (or set `EXPO_SKILLS_TELEMETRY=1`); turn off with `EXPO_SKILLS_TELEMETRY=0` / `DO_NOT_TRACK=1`. Feedback on a skill goes through the **expo-skill-feedback** skill. ## License diff --git a/plugins/expo/skills/add-app-clip/SKILL.md b/plugins/expo/skills/add-app-clip/SKILL.md index c9ba35a..bc502be 100644 --- a/plugins/expo/skills/add-app-clip/SKILL.md +++ b/plugins/expo/skills/add-app-clip/SKILL.md @@ -278,11 +278,3 @@ See [./references/native-module.md](./references/native-module.md) for the Swift ## References - ./references/native-module.md — Local Expo module to detect App Clip context and present the SKOverlay install prompt - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/building-native-ui/SKILL.md b/plugins/expo/skills/building-native-ui/SKILL.md index 1551bce..840518f 100644 --- a/plugins/expo/skills/building-native-ui/SKILL.md +++ b/plugins/expo/skills/building-native-ui/SKILL.md @@ -319,11 +319,3 @@ export default function Layout({ segment }) { ); } ``` - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/eas-update-insights/SKILL.md b/plugins/expo/skills/eas-update-insights/SKILL.md index a352544..840673e 100644 --- a/plugins/expo/skills/eas-update-insights/SKILL.md +++ b/plugins/expo/skills/eas-update-insights/SKILL.md @@ -226,11 +226,3 @@ Human-readable group details plus 30 days of launches/failures per platform — - **Fresh publishes** may show zeros for a short period while the metrics pipeline catches up. - **Installs are downloads, not launches**: the `installs` / "Launches" field counts users who downloaded the manifest and launch asset. A confirmed run only registers on the user's *next* update check (typically up to 24h later, depending on the app's update policy). So metrics lag the real-world state slightly. - **Crashes are self-reported**: `failedInstalls` / "Crashes" counts updates that errored during install/launch and were reported on the next update check. Crashes that don't trigger an update request (e.g. process kill before recovery) won't appear. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-api-routes/SKILL.md b/plugins/expo/skills/expo-api-routes/SKILL.md index d6d48f6..85cb198 100644 --- a/plugins/expo/skills/expo-api-routes/SKILL.md +++ b/plugins/expo/skills/expo-api-routes/SKILL.md @@ -366,11 +366,3 @@ export async function GET(request: Request) { - Keep API routes focused — one responsibility per endpoint - Use TypeScript for type safety - Log errors server-side for debugging - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-brownfield/SKILL.md b/plugins/expo/skills/expo-brownfield/SKILL.md index 68f5a26..bbd0c62 100644 --- a/plugins/expo/skills/expo-brownfield/SKILL.md +++ b/plugins/expo/skills/expo-brownfield/SKILL.md @@ -52,11 +52,3 @@ npx create-expo-app@latest my-project --template default@sdk-55 ``` Pin the same Expo SDK across both the RN project and any embedded dependencies. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-cicd-workflows/SKILL.md b/plugins/expo/skills/expo-cicd-workflows/SKILL.md index 11b3200..ed0c96b 100644 --- a/plugins/expo/skills/expo-cicd-workflows/SKILL.md +++ b/plugins/expo/skills/expo-cicd-workflows/SKILL.md @@ -90,11 +90,3 @@ The validator fetches the latest schema and checks the YAML structure. Fix any r ## Answering Questions When users ask about available options (job types, triggers, runner types, etc.), fetch the schema and derive the answer from it rather than relying on potentially outdated information. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-deployment/SKILL.md b/plugins/expo/skills/expo-deployment/SKILL.md index 65eb678..114aa91 100644 --- a/plugins/expo/skills/expo-deployment/SKILL.md +++ b/plugins/expo/skills/expo-deployment/SKILL.md @@ -188,11 +188,3 @@ eas build:view # View submission status eas submit:list ``` - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-dev-client/SKILL.md b/plugins/expo/skills/expo-dev-client/SKILL.md index c30614f..84a1cf0 100644 --- a/plugins/expo/skills/expo-dev-client/SKILL.md +++ b/plugins/expo/skills/expo-dev-client/SKILL.md @@ -162,11 +162,3 @@ eas build -p ios --profile development --clear-cache eas --version eas update ``` - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-examples/SKILL.md b/plugins/expo/skills/expo-examples/SKILL.md index 13700f3..8b1cebc 100644 --- a/plugins/expo/skills/expo-examples/SKILL.md +++ b/plugins/expo/skills/expo-examples/SKILL.md @@ -95,11 +95,3 @@ When the user already has an app, **add only what the example introduces; never ## References - `./references/catalog.md` — categorized snapshot of the example library for fast triage. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-module/SKILL.md b/plugins/expo/skills/expo-module/SKILL.md index f6e5e4f..dbd2f29 100644 --- a/plugins/expo/skills/expo-module/SKILL.md +++ b/plugins/expo/skills/expo-module/SKILL.md @@ -139,11 +139,3 @@ export function hello(name: string): string { ``` Note: iOS uses just the class name; Android uses the fully-qualified class name (package + class). See `references/module-config.md` for all fields. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-observe/SKILL.md b/plugins/expo/skills/expo-observe/SKILL.md index 1e6b470..d08e85d 100644 --- a/plugins/expo/skills/expo-observe/SKILL.md +++ b/plugins/expo/skills/expo-observe/SKILL.md @@ -27,11 +27,3 @@ The three reference files in `./references/` cover the three things people typic - Expo Router integration: https://docs.expo.dev/eas/observe/integrations/expo-router/ - React Navigation integration: https://docs.expo.dev/eas/observe/integrations/react-navigation/ - Configuration: https://docs.expo.dev/eas/observe/configuration/ - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-skill-feedback/SKILL.md b/plugins/expo/skills/expo-skill-feedback/SKILL.md index 219e413..627016d 100644 --- a/plugins/expo/skills/expo-skill-feedback/SKILL.md +++ b/plugins/expo/skills/expo-skill-feedback/SKILL.md @@ -1,21 +1,22 @@ --- name: expo-skill-feedback -description: Submit feedback on an Expo skill — or on Expo itself — or turn the bundled anonymous usage telemetry on or off. Use when an Expo skill was useful, confusing, broken, missing context, or worth improving; when something fell short because of Expo (an SDK bug or confusing framework behavior) rather than the skill; or when the user wants to disable, turn off, opt out of, or understand the anonymous usage tracking these skills send. +description: Submit feedback on an Expo skill — or on Expo itself — or turn the bundled anonymous usage telemetry on or off (off by default / opt-in; the user saying "enable Expo skills telemetry" in conversation is the switch). Use when an Expo skill was useful, confusing, broken, missing context, or worth improving; when something fell short because of Expo (an SDK bug or confusing framework behavior) rather than the skill; or when the user wants to enable, turn on, opt in to, disable, turn off, opt out of, check the status of, or understand the anonymous usage tracking these skills can send. --- # Expo Skill Feedback Send anonymous feedback on an Expo skill — or on Expo itself — and turn the bundled usage -telemetry on or off. Feedback goes to the Expo team via PostHog. Only the skill name, your -rating + note, platform, and a hash of a random local install id are sent — never code, -prompts, file paths, secrets, or personal data. (A separate automatic `skill_invoked` event -fires when a skill runs; *Turning it off* silences both.) +telemetry on or off. **Telemetry is off by default (opt-in)** — nothing is sent until it's +enabled. Feedback goes to the Expo team via PostHog. Only the skill name, your rating + note, +platform, and a hash of a random local install id are sent — never code, prompts, file paths, +secrets, or personal data. (A separate automatic `skill_invoked` event fires when a skill runs +*once enabled*; the switch below controls both.) ## Submitting feedback Be proactive: when a skill was wrong, confusing, outdated, or missing — or the trouble is with **Expo itself** — send 1-3 concrete sentences. Pass the skill you're rating to -`--skill` (the one whose footer sent you here, not `expo-skill-feedback`): +`--skill` (the skill being rated, not `expo-skill-feedback`): ```bash node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.cjs" \ @@ -29,20 +30,25 @@ node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/skill-feedback.cj Never include secrets, source code, long prompts, or stack traces. -## Turning it off +If the command refuses because telemetry is off, don't drop the feedback — ask the user +once (see *Turning it on or off*), and resend the same command after they enable. -Telemetry is anonymous and on by default; opting out silences both feedback and the -automatic event. Ask your agent to **"turn off Expo skills telemetry"**, or run: +## Turning it on or off + +Telemetry is anonymous and **off by default (opt-in)** — nothing (neither the automatic +`skill_invoked` event nor feedback) is sent until the user enables it. Saying so in +conversation is the switch: when the user says **"enable Expo skills telemetry"** — or +answers yes when you offer — run: ```bash -node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --off +node "${CLAUDE_PLUGIN_ROOT}/skills/expo-skill-feedback/scripts/telemetry.cjs" --on ``` -`--on` re-enables, `--status` checks. For CI or a global switch: `EXPO_SKILLS_TELEMETRY=0` -or `DO_NOT_TRACK=1`. - ---- - -## Expo Skill Feedback +`--off` turns it off again. When the user asks whether telemetry is on, run `--status` +and relay its output — don't answer from memory; env vars and CI can override the saved +state. Env equivalents: `EXPO_SKILLS_TELEMETRY=1` to enable, `=0` or `DO_NOT_TRACK=1` to +disable; CI never sends. -Help make these skills better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: flag anything wrong, confusing, outdated, or missing — in a skill or in **Expo itself** (`--about expo`). A quick note when something worked well helps too. See *Submitting feedback* above for the command. Never include secrets, source code, or long prompts. +**Never enable it on your own.** If a feedback send was refused because telemetry is off, +ask the user once — "Expo skills telemetry is off; enable it (anonymous) and send this?" — +and run `--on` only after an explicit yes. If they decline, drop the feedback silently. diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs index 220924e..5c940c7 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-event.cjs @@ -17,7 +17,7 @@ const path = require("path"); const { POSTHOG_PROJECT_API_KEY, SOURCE, - telemetryDisabled, + telemetryActive, telemetryConfigured, detectHarness, platformProps, @@ -143,7 +143,7 @@ async function main(argv) { // Cheap, local, no-network gates: decide up front whether anything will be sent, so the // common "not an Expo skill / opted out" cases cost nothing and never spawn a child. if (!skill) return 0; // not a skill invocation - if (telemetryDisabled()) return 0; + if (!args.dryRun && !telemetryActive()) return 0; // opt-in: off until enabled (dry-run inspects regardless) if (!telemetryConfigured() && !args.dryRun) return 0; // no key in this build (e.g. a fork) -> inert if (!skillBelongsToPlugin(skill, pluginRootFor(args))) return 0; // not one of ours diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs index 19aea81..5d09800 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs +++ b/plugins/expo/skills/expo-skill-feedback/scripts/skill-feedback.cjs @@ -8,7 +8,7 @@ const { POSTHOG_PROJECT_API_KEY, SOURCE, - telemetryDisabled, + telemetryActive, telemetryConfigured, detectHarness, platformProps, @@ -77,8 +77,8 @@ function eventPayload(args) { async function main(argv) { const args = parseArgs(argv); - if (telemetryDisabled()) { - console.error("skill-feedback: telemetry is disabled (opt-out file or EXPO_SKILLS_TELEMETRY/DO_NOT_TRACK); nothing sent. Re-enable with telemetry.cjs --on."); + if (!args.dryRun && !telemetryActive()) { + console.error("skill-feedback: telemetry is off (opt-in, off by default); nothing sent. Enable with `telemetry.cjs --on` or EXPO_SKILLS_TELEMETRY=1."); return 0; } if (!telemetryConfigured() && !args.dryRun) { diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs index 8694449..718e4fe 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs +++ b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs @@ -1,59 +1,65 @@ #!/usr/bin/env node // Turn Expo skills usage telemetry on or off, or check its status. // -// Usage: -// node telemetry.cjs --status # show whether telemetry is on/off and why -// node telemetry.cjs --off # disable (writes the opt-out file) -// node telemetry.cjs --on # re-enable (removes the opt-out file) +// Telemetry is OFF by default (opt-in) — nothing is sent until you enable it here or +// via EXPO_SKILLS_TELEMETRY=1. Usage: +// node telemetry.cjs --status # show whether telemetry is on/off and why +// node telemetry.cjs --on # enable (writes the opt-in marker) +// node telemetry.cjs --off # disable (removes the opt-in marker — the default) // -// The opt-out file is the reliable switch: it works no matter how the agent was -// launched. The DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY env vars also disable -// telemetry (handy for CI), but env vars don't always reach hook subprocesses. +// The opt-in marker is the reliable switch: it works no matter how the agent was +// launched. EXPO_SKILLS_TELEMETRY=1/0 and DO_NOT_TRACK also work (handy for CI), but +// env vars don't always reach hook subprocesses. const fs = require("fs"); const path = require("path"); -const { OPT_OUT_PATH, telemetryConfigured, isCI } = require("./telemetry_common.cjs"); +const { + OPT_IN_PATH, + telemetryActive, + telemetryEnvSignal, + telemetryConfigured, + isCI, +} = require("./telemetry_common.cjs"); -function disabledByEnv() { - const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); - if (["0", "false", "off", "no"].includes(flag)) return "EXPO_SKILLS_TELEMETRY"; - const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); - if (dnt && dnt !== "0" && dnt !== "false") return "DO_NOT_TRACK"; - return null; +// One-line explanation of the current state (mirrors telemetryActive()'s precedence). +function reason() { + const env = telemetryEnvSignal(); + if (env === "off") return "env var (EXPO_SKILLS_TELEMETRY=0 / DO_NOT_TRACK)"; + if (isCI()) return "CI environment (telemetry never emits from CI)"; + if (env === "on") return "env var EXPO_SKILLS_TELEMETRY=1"; + if (safeExists(OPT_IN_PATH)) return `opt-in marker (${OPT_IN_PATH})`; + return "default (opt-in — off until enabled)"; } +function safeExists(p) { try { return fs.existsSync(p); } catch { return false; } } + function printStatus() { - const byFile = fs.existsSync(OPT_OUT_PATH); - const byEnv = disabledByEnv(); - const byCI = isCI(); - if (byFile || byEnv || byCI) { - const reasons = []; - if (byFile) reasons.push(`opt-out file (${OPT_OUT_PATH})`); - if (byCI) reasons.push("CI environment"); - if (byEnv) reasons.push(`env var ${byEnv}`); - console.log(`Expo skills telemetry: DISABLED — via ${reasons.join(" and ")}.`); - } else if (!telemetryConfigured()) { - console.log("Expo skills telemetry: ENABLED, but no PostHog key in this build (key stripped to placeholder) — nothing is created or sent. Disable permanently with: telemetry.cjs --off"); + const on = telemetryActive(); + if (on && !telemetryConfigured()) { + console.log(`Expo skills telemetry: ON via ${reason()}, but no PostHog key in this build (stripped to placeholder) — nothing is sent.`); + } else if (on) { + console.log(`Expo skills telemetry: ON (anonymous) — ${reason()}. Turn off with: telemetry.cjs --off`); } else { - console.log("Expo skills telemetry: ENABLED (anonymous). Disable with: telemetry.cjs --off"); + console.log(`Expo skills telemetry: OFF — ${reason()}. Turn on with: telemetry.cjs --on (or EXPO_SKILLS_TELEMETRY=1)`); } } const cmd = process.argv[2]; -if (cmd === "--off" || cmd === "--disable") { - fs.mkdirSync(path.dirname(OPT_OUT_PATH), { recursive: true, mode: 0o700 }); - fs.writeFileSync(OPT_OUT_PATH, "Expo skills telemetry disabled by user.\n"); - console.log(`Telemetry disabled — wrote ${OPT_OUT_PATH}`); - console.log("Re-enable any time with: telemetry.cjs --on"); -} else if (cmd === "--on" || cmd === "--enable") { - try { fs.rmSync(OPT_OUT_PATH, { force: true }); } catch {} - const byEnv = disabledByEnv(); - console.log("Telemetry re-enabled — removed the opt-out file."); - if (byEnv) console.log(`Note: still disabled by env var ${byEnv}; unset it to fully re-enable.`); +if (cmd === "--on" || cmd === "--enable") { + fs.mkdirSync(path.dirname(OPT_IN_PATH), { recursive: true, mode: 0o700 }); + fs.writeFileSync(OPT_IN_PATH, "Expo skills telemetry enabled by user.\n"); + console.log(`Telemetry enabled — wrote ${OPT_IN_PATH}`); + if (telemetryEnvSignal() === "off") console.log("Note: an env var (EXPO_SKILLS_TELEMETRY=0 / DO_NOT_TRACK) still forces it OFF; unset it to send."); + else if (isCI()) console.log("Note: this looks like CI, where telemetry stays OFF regardless."); + console.log("Turn off any time with: telemetry.cjs --off"); +} else if (cmd === "--off" || cmd === "--disable") { + try { fs.rmSync(OPT_IN_PATH, { force: true }); } catch {} + console.log("Telemetry off — removed the opt-in marker (off is the default)."); + if (telemetryEnvSignal() === "on") console.log("Note: EXPO_SKILLS_TELEMETRY=1 still forces it ON; unset it to stay off."); } else if (cmd === "--status" || cmd === undefined) { printStatus(); } else { - console.error(`Unknown option: ${cmd}\nUsage: telemetry.cjs [--status | --off | --on]`); + console.error(`Unknown option: ${cmd}\nUsage: telemetry.cjs [--status | --on | --off]`); process.exit(2); } diff --git a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs index d4b3c1b..4e6c47c 100644 --- a/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs +++ b/plugins/expo/skills/expo-skill-feedback/scripts/telemetry_common.cjs @@ -23,13 +23,18 @@ const POSTHOG_PROJECT_API_KEY = const SOURCE = "expo-skills"; const INSTALLATION_ID_PATH = path.join(os.homedir(), ".expo-skills", "installation-id"); -// Persistent opt-out marker. Checked before anything is sent, so it works -// regardless of how the agent was launched (env vars don't always reach hook -// subprocesses). Toggle it with scripts/telemetry.cjs --off / --on. -const OPT_OUT_PATH = path.join(os.homedir(), ".expo-skills", "opt-out"); +// Opt-in is the model: telemetry is OFF by default and only sends once the user +// explicitly enables it. This is the one product knob — flip to true for an opt-out +// (on-by-default) model instead. +const DEFAULT_ON = false; -// CI detection — skip telemetry in automated environments so usage data reflects -// real humans. Honors the common CI=true convention plus major providers' signals. +// Persistent opt-in marker. Its presence turns telemetry on across sessions, regardless +// of how the agent was launched (env vars don't always reach hook subprocesses). +// Written / removed by scripts/telemetry.cjs --on / --off. +const OPT_IN_PATH = path.join(os.homedir(), ".expo-skills", "opt-in"); + +// CI detection — never emit from automated environments so usage data reflects real +// humans. Honors the common CI=true convention plus major providers' signals. function isCI() { const ci = String(process.env.CI || "").trim().toLowerCase(); if (ci && ci !== "0" && ci !== "false") return true; @@ -45,16 +50,33 @@ function isCI() { ); } -// Opt-out switch: the persistent opt-out file, CI, or the EXPO_SKILLS_TELEMETRY / -// DO_NOT_TRACK (https://consoledonottrack.com) env vars. -function telemetryDisabled() { - try { if (fs.existsSync(OPT_OUT_PATH)) return true; } catch {} - if (isCI()) return true; - const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); - if (["0", "false", "off", "no"].includes(flag)) return true; +// Explicit on/off intent from env vars: returns "on" | "off" | null. +// DO_NOT_TRACK=1 -> off (https://consoledonottrack.com) +// EXPO_SKILLS_TELEMETRY=0/off/false/no -> off +// EXPO_SKILLS_TELEMETRY=1/on/true/yes -> on +function telemetryEnvSignal() { const dnt = String(process.env.DO_NOT_TRACK || "").trim().toLowerCase(); - if (dnt && dnt !== "0" && dnt !== "false") return true; - return false; + if (dnt && dnt !== "0" && dnt !== "false") return "off"; + const flag = String(process.env.EXPO_SKILLS_TELEMETRY || "").trim().toLowerCase(); + if (["0", "false", "off", "no"].includes(flag)) return "off"; + if (["1", "true", "on", "yes"].includes(flag)) return "on"; + return null; +} + +// Master gate. Nothing is sent unless this returns true. Precedence (highest first): +// 1. explicit env off (DO_NOT_TRACK / EXPO_SKILLS_TELEMETRY=0) -> off +// 2. CI -> off (never emit from bots) +// 3. explicit env on (EXPO_SKILLS_TELEMETRY=1) -> on +// 4. persistent opt-in marker (telemetry.cjs --on) -> on +// 5. default -> DEFAULT_ON (off = opt-in) +// So explicit-off and CI always win; otherwise it's on only when explicitly enabled. +function telemetryActive() { + const env = telemetryEnvSignal(); + if (env === "off") return false; + if (isCI()) return false; + if (env === "on") return true; + try { if (fs.existsSync(OPT_IN_PATH)) return true; } catch {} + return DEFAULT_ON; } // A real key ships in this file, so telemetry is ON by default. This guard only @@ -137,8 +159,9 @@ function sendToPosthog(payload, { userAgent, timeoutMs }) { module.exports = { POSTHOG_PROJECT_API_KEY, SOURCE, - OPT_OUT_PATH, - telemetryDisabled, + OPT_IN_PATH, + telemetryActive, + telemetryEnvSignal, telemetryConfigured, detectHarness, isCI, diff --git a/plugins/expo/skills/expo-tailwind-setup/SKILL.md b/plugins/expo/skills/expo-tailwind-setup/SKILL.md index 8f5abc3..d37fe32 100644 --- a/plugins/expo/skills/expo-tailwind-setup/SKILL.md +++ b/plugins/expo/skills/expo-tailwind-setup/SKILL.md @@ -478,11 +478,3 @@ Add className to component props: ```tsx type Props = React.ComponentProps & { className?: string }; ``` - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/expo-ui/SKILL.md b/plugins/expo/skills/expo-ui/SKILL.md index 36b2e85..77c1fa3 100644 --- a/plugins/expo/skills/expo-ui/SKILL.md +++ b/plugins/expo/skills/expo-ui/SKILL.md @@ -42,11 +42,3 @@ references/ swift-ui.md Platform-specific iOS UI: @expo/ui/swift-ui components, modifiers, RNHostView, useNativeState jetpack-compose.md Platform-specific Android UI: @expo/ui/jetpack-compose components, modifiers, LazyColumn caveat, icons, useNativeState ``` - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/native-data-fetching/SKILL.md b/plugins/expo/skills/native-data-fetching/SKILL.md index 76fb926..d59cb38 100644 --- a/plugins/expo/skills/native-data-fetching/SKILL.md +++ b/plugins/expo/skills/native-data-fetching/SKILL.md @@ -505,11 +505,3 @@ User: "Where should I put my API key?" User: "How do I load data for a page in Expo Router?" -> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/upgrading-expo/SKILL.md b/plugins/expo/skills/upgrading-expo/SKILL.md index fd19d7e..5769946 100644 --- a/plugins/expo/skills/upgrading-expo/SKILL.md +++ b/plugins/expo/skills/upgrading-expo/SKILL.md @@ -132,11 +132,3 @@ Since SDK 55, users can opt-in to use Hermes engine v1 for improved runtime perf ## New Architecture The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53. - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. diff --git a/plugins/expo/skills/use-dom/SKILL.md b/plugins/expo/skills/use-dom/SKILL.md index 2dd3b64..b11eafa 100644 --- a/plugins/expo/skills/use-dom/SKILL.md +++ b/plugins/expo/skills/use-dom/SKILL.md @@ -415,11 +415,3 @@ On web, the `dom` prop is ignored since no webview is needed. - Test on all platforms — web rendering may differ slightly from native webviews - Large DOM components may impact performance — profile if needed - The webview has its own JavaScript context — cannot directly share state with native - ---- - -## Expo Skill Feedback - -Help make this skill better — feedback goes **straight to the Expo team, and it's anonymous**. Be proactive: if anything here was wrong, confusing, outdated, or missing — or if the trouble is with **Expo itself**, not this skill — please flag it so we can fix it (a quick note when it worked well is welcome too). Even one honest line is valuable. - -To send it, use the **`expo-skill-feedback`** skill — it has the short, safe command. Never include secrets, source code, or long prompts. From 67a9a60b1f5dc5111d90cefdc6e85f138017e0c3 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Thu, 2 Jul 2026 16:05:52 +0200 Subject: [PATCH 23/23] docs(expo): tighten the AGENTS.md telemetry section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same opt-in facts, half the words: keep the default + toggles, the two Claude-Code-only events and their wiring, the telemetryActive() pointer, and the contributor rules (no footers, no per-skill telemetry edits, Codex/Cursor can't host hooks — don't re-investigate). Drop the gate precedence chain and templating rationale; the code documents those. Co-Authored-By: Claude Fable 5 --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eba9028..79190cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,6 @@ codex plugin marketplace add expo/skills --ref main ## Usage Telemetry & Feedback -The `expo` plugin can send **anonymous**, **opt-in (off-by-default)** usage telemetry and collect skill feedback (details in the `expo-skill-feedback` skill) — nothing is sent until the user enables it. The hook is always present but no-ops until enabled (`telemetryActive()` gate in `telemetry_common.cjs`). Two signals, both **Claude Code only**: automatic `skill_invoked` (via the plugin's `hooks/hooks.json`, no per-skill hooks) and explicit `skill_feedback` (via the `expo-skill-feedback` skill). Other harnesses send nothing — Codex/Cursor can't host plugin hooks (Codex's `plugin_hooks` is a *removed* feature with no skill-invocation hook event, verified against codex 0.138 + the `openai/codex` source; Cursor has no plugin-hook system). Product skills carry **no feedback footer** — feedback is offered only through the `expo-skill-feedback` skill itself (`SKILL.md` bodies aren't templated, so a per-skill footer can't be conditionally hidden; we ship none). New skills need no footer or per-skill edits. +Telemetry is anonymous, **opt-in, and off by default** — nothing is sent until the user enables it with `node plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --on` or `EXPO_SKILLS_TELEMETRY=1` (`--off` / `=0` / `DO_NOT_TRACK=1` disable; CI never sends). When enabled, two events fire on **Claude Code only**: automatic `skill_invoked` (plugin-level `hooks/hooks.json`) and explicit `skill_feedback` (the `expo-skill-feedback` skill). The gate is `telemetryActive()` in `telemetry_common.cjs`. -**Turn it on (off by default):** ask your agent to "turn on Expo skills telemetry", run `node plugins/expo/skills/expo-skill-feedback/scripts/telemetry.cjs --on`, or set `EXPO_SKILLS_TELEMETRY=1`. Disable again with `--off`, `EXPO_SKILLS_TELEMETRY=0`, or `DO_NOT_TRACK=1`. The gate precedence: explicit-off (DO_NOT_TRACK / `=0`) → CI (never sends) → explicit-on (`=1`) → opt-in marker (`~/.expo-skills/opt-in`) → default off. +For contributors: new skills need **no telemetry edits and no feedback footer** — the plugin-level hook covers them automatically. Codex and Cursor cannot host plugin hooks (verified against their sources; don't re-investigate), so they ship no hooks and send nothing.