diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6fd4d78..3842e98 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "displayName": "PostHog", "source": "./", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from your AI coding tool. Optionally capture Claude Code sessions to PostHog LLM Analytics.", - "version": "1.1.64", + "version": "1.1.65", "author": { "name": "PostHog", "email": "hey@posthog.com", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index be71d70..43e6528 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "posthog", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from your AI coding tool. Optionally capture Claude Code sessions to PostHog LLM Analytics.", - "version": "1.1.64", + "version": "1.1.65", "author": { "name": "PostHog", "email": "hey@posthog.com", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 31d5083..2414321 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "posthog", - "version": "1.0.61", + "version": "1.0.62", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Codex", "author": { "name": "PostHog", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 40137c8..3de3e49 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "posthog", "displayName": "PostHog", - "version": "1.1.57", + "version": "1.1.58", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Cursor", "author": { "name": "PostHog", diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index c9504fb..a087d7f 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -42,6 +42,11 @@ jobs: -o /tmp/context-mill.zip \ "https://github.com/PostHog/context-mill/releases/latest/download/skills-mcp-resources.zip" + - name: Regenerate MCP write-tool list + run: | + bash scripts/generate-write-tools.sh \ + || echo "::warning::write-tools.txt not regenerated; keeping the committed list" + - name: Sync skills directory run: | MANIFEST="skills/.sync-manifest" @@ -91,7 +96,7 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | - git add -A skills/ commands/*.toml gemini-extension.json + git add -A skills/ commands/*.toml gemini-extension.json hooks/write-tools.txt if git diff --cached --quiet; then echo "No changes detected." echo "has_changes=false" >> "$GITHUB_OUTPUT" diff --git a/gemini-extension.json b/gemini-extension.json index bd00d13..d000c1f 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "posthog", - "version": "1.0.59", + "version": "1.0.60", "description": "Access PostHog analytics, feature flags, experiments, error tracking, and insights directly from Gemini CLI", "mcpServers": { "posthog": { diff --git a/hooks/gate-exec-write.sh b/hooks/gate-exec-write.sh index f7f8bb9..6618344 100755 --- a/hooks/gate-exec-write.sh +++ b/hooks/gate-exec-write.sh @@ -33,6 +33,12 @@ # # export POSTHOG_MCP_EXEC_GATE_ALLOW="llma-skill-*,annotation-create" # +# A tool counts as a write when it appears in `hooks/write-tools.txt` (every +# registry tool with readOnlyHint=false, regenerated by +# scripts/generate-write-tools.sh) or when its name carries a write verb from +# the regex below. The regex is the fallback for a missing list and for tools +# added to PostHog after the list was generated. +# # Pure bash; no jq or other third-party tools required. Relies on the fact # that PostHog tool names are kebab-case alphanumerics, so a narrow regex on # the raw JSON payload is safe. @@ -107,14 +113,16 @@ fi # `mcp__posthog_posthog__exec`). [[ "$tool_name" =~ __exec$ ]] || exit 0 -# Extract the PostHog tool name from `"command":"call [--json] ..."`. -# Tool names are kebab-case [a-zA-Z0-9_-]+ so the regex stops cleanly at the -# first space or escaped quote without needing to parse the trailing JSON. +# Extract the PostHog tool name from `"command":"call [--flag ...] ..."`. +# `call` accepts several leading flags (`--json`, `--confirm`, `--no-skills`), +# so skip every `--` token before the name. Tool names are kebab-case +# [a-zA-Z0-9_-]+ so the regex stops cleanly at the first space or escaped quote +# without needing to parse the trailing JSON. posthog_tool="" -if [[ "$input" =~ \"command\"[[:space:]]*:[[:space:]]*\"call[[:space:]]+(--json[[:space:]]+)?([a-zA-Z0-9_-]+) ]]; then - posthog_tool="${BASH_REMATCH[2]}" +if [[ "$input" =~ \"command\"[[:space:]]*:[[:space:]]*\"call[[:space:]]+((--[a-zA-Z-]+[[:space:]]+)*)([a-zA-Z0-9_-]+) ]]; then + posthog_tool="${BASH_REMATCH[3]}" fi -[[ -n "$posthog_tool" ]] || exit 0 +[[ -n "$posthog_tool" && "$posthog_tool" != -* ]] || exit 0 # Match write-verb fragments as whole hyphen-separated words within the tool # name. Keep this list in sync with the PostHog MCP write surface. @@ -135,8 +143,17 @@ matches_any_glob() { return 1 } +write_tools_file="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/write-tools.txt" +write_tools="" +[[ -r "$write_tools_file" ]] && write_tools=$'\n'"$(<"$write_tools_file")"$'\n' + +is_write_tool() { + [[ -n "$write_tools" && "$write_tools" == *$'\n'"$1"$'\n'* ]] && return 0 + [[ "$1" =~ $write_re ]] +} + shopt -s nocasematch -if [[ "$posthog_tool" =~ $write_re ]]; then +if is_write_tool "$posthog_tool"; then # Allowlist wins — skip the prompt for tools matching any glob in # POSTHOG_MCP_EXEC_GATE_ALLOW. Patterns use bash glob syntax (`*`, `?`). if [[ -n "${POSTHOG_MCP_EXEC_GATE_ALLOW:-}" ]]; then diff --git a/hooks/write-tools.txt b/hooks/write-tools.txt new file mode 100644 index 0000000..3446c49 --- /dev/null +++ b/hooks/write-tools.txt @@ -0,0 +1,458 @@ +# Generated by scripts/generate-write-tools.sh from PostHog/posthog master. Do not edit by hand. +account-relationship-definitions-create +account-relationship-definitions-destroy +account-relationship-definitions-partial-update +accounts-create +accounts-custom-property-values-create +accounts-destroy +accounts-notebooks-create +accounts-notebooks-destroy +accounts-partial-update +accounts-relationships-create +accounts-relationships-end-create +action-create +action-delete +action-update +alert-create +alert-delete +alert-destinations-create +alert-destinations-delete +alert-update +annotation-create +annotation-delete +annotations-partial-update +announcements-create-execute +batch-export-create +batch-export-delete +batch-export-update +billing-alert-create +billing-alert-update +business-knowledge-sources-partial-update +business-knowledge-sources-text-create +business-knowledge-sources-url-create +canvas-create +canvas-draft-create +canvas-edit-create +canvas-layout-patch +canvas-layout-publish +canvas-move +canvas-promote-create +canvas-publish-create +canvas-publish-current-version +canvas-state-set +cdp-functions-create +cdp-functions-delete +cdp-functions-discard-draft +cdp-functions-invocations-create +cdp-functions-partial-update +cdp-functions-publish +cdp-functions-rearrange-partial-update +cdp-functions-restore-revision +change-requests-approve-execute +change-requests-reject-execute +channel-create +channel-instructions-update +cohorts-add-persons-to-static-cohort-partial-update +cohorts-create +cohorts-partial-update +cohorts-rm-person-from-static-cohort-partial-update +comments-create +context-wiki-page-update +conversations-tickets-notes-destroy +conversations-tickets-notes-partial-update +conversations-tickets-reply-create +conversations-tickets-update +conversations-views-create +conversations-views-update +create-feature-flag +custom-property-definitions-create +custom-property-definitions-destroy +custom-property-definitions-partial-update +custom-property-sources-backfill +custom-property-sources-create +custom-property-sources-destroy +custom-property-sources-partial-update +custom-property-sources-sync +customer-tasks-archive-create +customer-tasks-create +customer-tasks-partial-update +customer-tasks-restore-create +dashboard-create +dashboard-create-tile +dashboard-delete +dashboard-delete-tile +dashboard-reorder-tiles +dashboard-tile-copy +dashboard-update +dashboard-update-text-tile +dashboard-widgets-batch-add +dashboard-widgets-batch-update +dashboards-move-tile-partial-update +data-catalog-certification-certify-execute +data-catalog-certification-deprecate-execute +data-catalog-certification-propose +data-catalog-metric-approve-execute +data-catalog-metric-create +data-catalog-metric-delete-execute +data-catalog-metric-update +data-catalog-metrics-refresh-from-insight-create +data-catalog-relationship-accept-execute +data-catalog-relationship-propose +data-catalog-relationship-reject-execute +data-quality-check-create +data-quality-check-delete +data-quality-check-run +data-quality-check-schedule +data-quality-check-update +data-warehouse-source-setup +delete-feature-flag +early-access-feature-create +early-access-feature-destroy +early-access-feature-partial-update +endpoint-create +endpoint-delete +endpoint-update +error-tracking-alerts-create +error-tracking-alerts-delete +error-tracking-alerts-partial-update +error-tracking-assignment-rules-create +error-tracking-bypass-rules-create +error-tracking-bypass-rules-update +error-tracking-external-references-create +error-tracking-grouping-rules-create +error-tracking-grouping-rules-update +error-tracking-issues-assign-partial-update +error-tracking-issues-merge-create +error-tracking-issues-partial-update +error-tracking-issues-split-create +error-tracking-settings-update +error-tracking-severity-rules-create +error-tracking-severity-rules-update +error-tracking-suppression-rules-create +error-tracking-suppression-rules-update +event-definition-create +event-definition-update +event-streams-add-account +event-streams-create +event-streams-destroy +event-streams-partial-update +event-streams-remove-account +event-streams-send-test-message +experiment-archive +experiment-copy-to-project +experiment-create +experiment-create-from-prompt +experiment-delete +experiment-duplicate +experiment-end +experiment-freeze-exposure +experiment-holdouts-create +experiment-holdouts-destroy +experiment-holdouts-partial-update +experiment-launch +experiment-metrics-recalculation-create +experiment-pause +experiment-reset +experiment-resume +experiment-saved-metrics-create +experiment-saved-metrics-destroy +experiment-saved-metrics-partial-update +experiment-ship-variant +experiment-unarchive +experiment-unfreeze-exposure +experiment-update +experiments-bulk-update-tags-create +external-data-schemas-cancel +external-data-schemas-delete-data +external-data-schemas-partial-update +external-data-schemas-reload +external-data-schemas-resync +external-data-sources-bulk-update-schemas +external-data-sources-create +external-data-sources-create-webhook-create +external-data-sources-delete-webhook-create +external-data-sources-destroy +external-data-sources-partial-update +external-data-sources-preview-resource +external-data-sources-refresh-schemas +external-data-sources-reload +external-data-sources-repair-cdc-create +external-data-sources-update-webhook-inputs-create +feature-flag-archive +feature-flag-disable +feature-flag-enable +feature-flag-unarchive +feature-flags-bulk-delete-create +feature-flags-bulk-update-tags-create +feature-flags-copy-flags-create +feature-request-product-areas-create +feature-request-product-areas-partial-update +feature-requests-add-account-create +feature-requests-add-evidence-create +feature-requests-archive-create +feature-requests-create +feature-requests-partial-update +feature-requests-remove-evidence-create +feature-requests-restore-create +feature-requests-update-evidence-create +field-notes-partial-update +file-download-batch-exports-cancel-create +file-download-batch-exports-create +heatmaps-saved-create +heatmaps-saved-regenerate +heatmaps-saved-update +inbox-report-artefacts-create +inbox-report-artefacts-delete +inbox-report-artefacts-update +inbox-report-checks-create +inbox-reports-bulk-set-state +inbox-reports-claim +inbox-reports-set-state +inbox-reports-update +inbox-source-configs-create +inbox-source-configs-partial-update +inbox-source-configs-update +insight-create +insight-delete +insight-update +integration-delete +llma-clustering-config-set-event-filters +llma-clustering-job-create +llma-clustering-job-delete +llma-clustering-job-update +llma-dataset-archive +llma-dataset-create +llma-dataset-item-archive +llma-dataset-item-create +llma-dataset-item-restore +llma-dataset-item-update +llma-dataset-restore +llma-dataset-update +llma-evaluation-config-set-active-key +llma-evaluation-create +llma-evaluation-delete +llma-evaluation-directory-create +llma-evaluation-directory-delete +llma-evaluation-directory-update +llma-evaluation-report-create +llma-evaluation-report-generate +llma-evaluation-report-update +llma-evaluation-run +llma-evaluation-test-hog +llma-evaluation-update +llma-parser-recipe-create +llma-prompt-create +llma-prompt-duplicate +llma-prompt-label-delete +llma-prompt-label-set +llma-prompt-update +llma-review-queue-create +llma-review-queue-delete +llma-review-queue-item-create +llma-review-queue-item-delete +llma-review-queue-item-update +llma-review-queue-update +llma-score-definition-create +llma-score-definition-new-version +llma-score-definition-update +llma-skill-archive +llma-skill-create +llma-skill-duplicate +llma-skill-file-create +llma-skill-file-delete +llma-skill-file-rename +llma-skill-update +llma-summarization-create +llma-tagger-create +llma-trace-review-create +llma-trace-review-delete +llma-trace-review-update +logs-alerts-create +logs-alerts-destinations-create +logs-alerts-destinations-delete-create +logs-alerts-destroy +logs-alerts-partial-update +loop-channel-instructions-update +loop-context-wiki-page-update +loops-create-execute +loops-destroy +loops-partial-update +loops-run-create +marketing-analytics-create-conversion-goal +marketing-analytics-delete-conversion-goal +marketing-analytics-update-conversion-goal +mcp-analytics-intent-clusters-recompute +mcp-analytics-sessions-generate-intent +mcp-missing-capability-report +media-image-upload-complete +media-image-upload-start +notebook-edit +notebooks-add-cell +notebooks-configure-compute +notebooks-create +notebooks-create-markdown +notebooks-delete-cell +notebooks-destroy +notebooks-partial-update +notebooks-run-cell-interrupt +notebooks-set-variables +notebooks-update-cell +notebooks-widget-attach +notebooks-widget-cancel +notebooks-widget-generate +opt-outs-add +opt-outs-remove +organization-enforce-2fa-execute +path-cleaning-rules-update +persons-bulk-delete +persons-property-delete +persons-property-set +posthog-connection-call +posthog-connection-forward +products-enable +project-create +project-settings-update +property-definition-update +proxy-create +proxy-delete +proxy-retry +reminder-create +reminder-delete +reminder-update +review-hog-reviews-trigger +saved-query-column-annotations-create +scheduled-changes-create +scheduled-changes-delete +scheduled-changes-update +scout-check-record-result +scout-config-create +scout-config-delete +scout-config-sync +scout-config-update +scout-create +scout-edit-report +scout-emit-report +scout-emit-signal +scout-lighthouse-audit +scout-notes-create +scout-notes-delete +scout-record-output +scout-report-check-cancel +scout-report-check-create +scout-run-now +scout-scratchpad-forget +scout-scratchpad-remember +session-recording-bulk-delete +session-recording-delete +session-recording-playlist-create +session-recording-playlist-update +signals-scout-config-create +signals-scout-config-delete +signals-scout-config-sync +signals-scout-config-update +signals-scout-edit-report +signals-scout-emit-report +signals-scout-emit-signal +signals-scout-run-now +signals-scout-scratchpad-forget +signals-scout-scratchpad-remember +skill-archive +skill-create +skill-duplicate +skill-file-create +skill-file-delete +skill-file-rename +skill-rename +skill-store-install-command +skill-update +sql-variables-create +sql-variables-delete +sql-variables-update +stamphog-repo-configs-delete +streamlit-apps-create +streamlit-apps-delete +streamlit-apps-set-source +streamlit-apps-start +streamlit-apps-stop +streamlit-apps-update +subscriptions-create +subscriptions-delete +subscriptions-partial-update +subscriptions-test-delivery-create +survey-create +survey-delete +survey-launch +survey-stop +survey-update +switch-organization +switch-project +task-context-wiki-page-propose +task-context-wiki-page-update +tasks-config-create +tasks-create +tasks-create-and-run +tasks-me-config-create +tasks-run-create +update-feature-flag +usage-metrics-create +usage-metrics-destroy +usage-metrics-partial-update +user-home-settings-update +user-interview-topics-add-interviewee +user-interview-topics-create +user-interview-topics-generate-links +user-interview-topics-interviewees-bulk-create +user-interview-topics-interviewees-create +user-interview-topics-interviewees-destroy +user-interview-topics-interviewees-partial-update +user-interview-topics-links-csv +user-interview-topics-partial-update +user-interview-topics-remove-interviewee +user-interview-topics-send-invites +user-interviews-partial-update +user-settings-update +view-create +view-delete +view-materialize +view-run +view-unmaterialize +view-update +vision-observations-label-create +vision-observations-label-destroy +vision-scanners-affected-cohort-create +vision-scanners-create +vision-scanners-delete +vision-scanners-inline-scan-create +vision-scanners-prompt-suggestions-apply +vision-scanners-prompt-suggestions-dismiss +vision-scanners-prompt-suggestions-generate +vision-scanners-scan-session +vision-scanners-update +visual-review-repos-partial-update +visual-review-runs-approve-create +visual-review-runs-finalize-create +visual-review-runs-tolerate-create +warehouse-column-annotations-create +warehouse-column-annotations-partial-update +warehouse-tables-create +warehouse-tables-refresh-schema-create +web-analytics-bot-rules-create +web-analytics-bot-rules-destroy +web-analytics-path-cleaning-suggestions-apply +web-analytics-path-cleaning-suggestions-generate +workflows-archive +workflows-create +workflows-create-email-template +workflows-discard-draft +workflows-enable +workflows-patch-action-email +workflows-patch-email-template +workflows-patch-graph +workflows-publish +workflows-restore-revision +workflows-run-batch +workflows-schedule-create +workflows-test-run +workflows-update +workflows-update-email-template +workflows-update-schedule diff --git a/scripts/generate-write-tools.sh b/scripts/generate-write-tools.sh new file mode 100755 index 0000000..39c0fe2 --- /dev/null +++ b/scripts/generate-write-tools.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Writes hooks/write-tools.txt: every PostHog MCP tool whose readOnlyHint is +# false, read from the tool registry on PostHog/posthog master. The exec write +# gate treats a tool on this list as a write before it falls back to its verb +# regex. Requires curl and jq. +set -euo pipefail + +REGISTRY_URL="https://raw.githubusercontent.com/PostHog/posthog/master/services/mcp/schema/tool-definitions-all.json" +HERE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +OUT="$HERE/../hooks/write-tools.txt" + +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +{ + echo "# Generated by scripts/generate-write-tools.sh from PostHog/posthog master. Do not edit by hand." + curl -fsSL --retry 2 --retry-delay 3 "$REGISTRY_URL" \ + | jq -r 'to_entries[] | select(.value.annotations.readOnlyHint == false) | .key' \ + | LC_ALL=C sort -u +} > "$tmp" + +count="$(grep -c -v '^#' "$tmp")" +if (( count < 100 )); then + echo "refusing to write $OUT: only $count tools parsed from $REGISTRY_URL" >&2 + exit 1 +fi +mv "$tmp" "$OUT" +trap - EXIT +echo "wrote $count write tools to $OUT" diff --git a/tests/test_gate_exec_write.sh b/tests/test_gate_exec_write.sh index cbdc1aa..51402d3 100755 --- a/tests/test_gate_exec_write.sh +++ b/tests/test_gate_exec_write.sh @@ -164,9 +164,9 @@ run_case "disable=0 leaves the gate active" \ # --- POSTHOG_MCP_EXEC_GATE_DENY overrides the default set --- -run_case 'deny="*" restores prompting on every write' \ - "$(exec_call experiment-update)" \ - prompt experiment-update \ +run_case 'deny="*" restores prompting on every write (survey-stop has no regex verb)' \ + "$(exec_call survey-stop)" \ + prompt survey-stop \ POSTHOG_MCP_EXEC_GATE_DENY="*" run_case "deny narrowed to feature flags: experiment-update is silent" \ @@ -188,6 +188,10 @@ run_case "deny with --json flag still extracts tool" \ '{"tool_name":"mcp__posthog__exec","tool_input":{"command":"call --json delete-feature-flag {\"id\":1}"}}' \ prompt delete-feature-flag +run_case "--confirm before the tool name still extracts tool" \ + '{"tool_name":"mcp__posthog__exec","tool_input":{"command":"call --confirm insight-delete {}"}}' \ + prompt insight-delete + run_case "sensitive write via plugin-prefixed exec name prompts" \ "$(exec_call delete-feature-flag mcp__posthog_posthog__exec)" \ prompt delete-feature-flag @@ -246,6 +250,10 @@ run_case "embedded substring is not a write verb (e.g. updates-feed)" \ "$(exec_call some-updates-feed)" \ silent +run_case "write verb regex still applies to a tool absent from write-tools.txt" \ + "$(exec_call made-up-tool-delete)" \ + prompt made-up-tool-delete + # --- fail-open contract: the hook must never break a tool call --- # # The hook signals "ask" only via stdout JSON + exit 0. Every other path must