Development
Hook
Hooks
What posthog runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add PostHog/ai-pluginShips with posthog. Installing the plugin gets these hooks.
What fires, and when
SessionEnd
python3 "${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}/telemetry/hooks/session-end-llma.py"
PreToolUse
- Matches
__exec$"${CLAUDE_PLUGIN_ROOT:-$PLUGIN_ROOT}/hooks/gate-exec-write.sh"
In the plugin's words
How posthog describes its own hook set.
PostHog LLM Analytics + permission gating for write commands via mcp__posthog__exec
Where it lives
- hooks/gate-exec-write.shRunsGitHub
Read the script
#!/usr/bin/env bash # PreToolUse gate for the PostHog MCP `exec` tool. # # The PostHog MCP exposes a single `exec` tool that dispatches subcommands like # `tools | search | info | schema | call <tool_name> [json]`. Once the user # allow-lists `mcp__posthog__exec`, every subsequent `call` (including writes # like `experiment-update`, `notebooks-destroy`, `cdp-functions-delete`) runs # without a prompt. This hook re-introduces a prompt for write `call`s by # returning `permissionDecision: "ask"`. # # Read-only PostHog tools and non-`call` exec verbs are left alone — the hook # exits 0 so normal permission flow applies. # # By default the prompt fires only for a curated "sensitive" subset of write # tools — feature-flag writes and any delete/destroy — rather than every write. # Three env vars tune this (all matched against the PostHog tool name): # # POSTHOG_MCP_EXEC_GATE_DISABLE — set to a non-empty value (e.g. `1`) to turn # the gate off entirely. Useful for remote devboxes and Claude Cloud runs # where the prompt can't be answered. Example: # # export POSTHOG_MCP_EXEC_GATE_DISABLE=1 # # POSTHOG_MCP_EXEC_GATE_DENY — comma-separated bash globs selecting which # write tools prompt. Overrides the built-in default set. Use `*` to # prompt on every write (the previous behaviour). Example: # # export POSTHOG_MCP_EXEC_GATE_DENY="*feature-flag*,*-delete" # # POSTHOG_MCP_EXEC_GATE_ALLOW — comma-separated bash globs that opt specific # write tools out of the prompt. Applied on top of the deny set (allow # wins). Example: # # export POSTHOG_MCP_EXEC_GATE_ALLOW="llma-skill-*,annotation-create" # # 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. # Fail open — this gate must never break a Claude Code tool call. # # The "ask" decision is delivered entirely through the stdout JSON below; the # exit code is never used to signal it. So we force every exit path to 0 via an # EXIT trap. Any unexpected runtime failure — an unbound variable under `set -u`, # a failed builtin, an unusually old bash — then falls through to normal # permission flow instead of surfacing as a hook error. Crucially, it can never # exit 2, which Claude Code interprets as a hard *block* of the tool call. # # The one failure a trap can't catch is a parse-time syntax error (the trap # isn't installed yet); `tests/test_gate_exec_write.sh` guards that with `bash -n`. trap 'exit 0' EXIT set -u # Codex compatibility: Codex's PreToolUse protocol does not support # `permissionDecision: "ask"` (it is parsed then rejected as unsupported), and # Codex already gates tool calls through its own approval flow. Detect Codex via # its native PLUGIN_ROOT env var — Claude Code only ever sets CLAUDE_PLUGIN_ROOT, # never PLUGIN_ROOT — and skip the gate so the hook neither errors nor fights # Codex's prompt. See https://developers.openai.com/codex/hooks if [[ -n "${PLUGIN_ROOT:-}" ]]; then exit 0 fi # Full opt-out — turn the gate off entirely. Set to any non-empty value other # than `0`. Lets remote devboxes and Claude Cloud runs, where no one can answer # the prompt, run write `call`s without interruption. if [[ -n "${POSTHOG_MCP_EXEC_GATE_DISABLE:-}" && "${POSTHOG_MCP_EXEC_GATE_DISABLE}" != "0" ]]; then exit 0 fi # Default set of write tools that prompt. Comma-separated bash globs, grounded in # the live PostHog MCP tool registry. Covers the genuinely sensitive surface: # # *feature-flag* — feature-flag rollout writes: create-feature-flag, # update-feature-flag, delete-feature-flag, # feature-flags-bulk-{delete,update-tags}-create, # feature-flags-copy-flags-create # *delete* — any deletion: insight-delete, dashboard-delete, # cdp-functions-delete, persons-bulk-delete, # external-data-schemas-delete-data, session-recording-delete, … # *destroy* — any destroy: notebooks-destroy, accounts-destroy, # experiment-saved-metrics-destroy, agent-applications-destroy, … # experiment-launch / experiment-ship-variant / experiment-reset # — start exposing users / roll a variant to everyone / wipe results # survey-launch — start showing a survey to real users # workflows-enable — activate a user-facing automation # # Deliberately silent by default (lower blast radius / easily reverted): routine # create & update (insight, dashboard, annotation, cohort, alert, skill, …), # experiment-pause/resume/end, survey-stop, persons-property-set, # error-tracking-issues-merge/split. Add any of these via # POSTHOG_MCP_EXEC_GATE_DENY, which overrides this set wholesale; use # POSTHOG_MCP_EXEC_GATE_DENY="*" to restore prompting on every write. DEFAULT_DENY='*feature-flag*,*delete*,*destroy*,experiment-launch,experiment-ship-variant,experiment-reset,survey-launch,workflows-enable' input="$(cat)" # Extract `tool_name` — simple identifier, no escaping inside the value. tool_name="" if [[ "$input" =~ \"tool_name\"[[:space:]]*:[[:space:]]*\"([^\"]+)\" ]]; then tool_name="${BASH_REMATCH[1]}" fi # Match any MCP tool whose name ends in `__exec` regardless of plugin/server # namespacing (bare `mcp__posthog__exec` or plugin-prefixed variants like # `mcp__posthog_posthog__exec`). [[ "$tool_name" =~ __exec$ ]] || exit 0 # Extract the PostHog tool name from `"command":"call [--json] <tool>..."`. # 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]}" fi [[ -n "$posthog_tool" ]] || exit 0 # Match write-verb fragments as whole hyphen-separ
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withposthog
Official PostHog plugin for AI clients. Access PostHog products directly from your AI coding tool.
Get the whole plugin
Stats
87
Stars
17
Forks
Active
Maintenance
Python
Language
15h ago
Last commit
7mo ago
Created
Repo: PostHog/ai-plugin

