plugins/lisa/skills/lisa-posthog-access/SKILL.md
Vendor-neutral access layer for PostHog. PostHog skills and observability rules MUST delegate through this skill rather than calling PostHog MCP tools or REST directly. Per the credential-substrate-precedence contract, resolves POSTHOG_PERSONAL_API_KEY bearer auth first when present and identity-matched to the configured project, then falls back to the PostHog MCP.
npx skillsauth add codyswanngt/lisa lisa-posthog-accessInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Single chokepoint for PostHog operations. Caller skills and rules MUST NOT call
mcp__posthog__* tools or PostHog REST directly.
operation: query project_id:<ID> payload:{...}
operation: insights project_id:<ID>
operation: persons project_id:<ID> [query:<QUERY>]
operation: events project_id:<ID> [after:<ISO>] [before:<ISO>]
Return parsed JSON in a <result> block.
Probe in order — the ordering is the shared credential-substrate-precedence
contract, not a PostHog-local choice. The first tier that is ready and
identity-matches the configured project is used; a substrate authenticated against
a different project is skipped, never used.
POSTHOG_PERSONAL_API_KEY bearer
token against the configured PostHog host, resolved through
lisa-secrets-access.POSTHOG_PERSONAL_API_KEY, no REST adapter for the operation, or a PostHog
outage.PostHog documents personal API keys and bearer authentication, and the same key works interactively and headlessly — which is why it leads. The REST tier uses:
POSTHOG_HOST=${POSTHOG_HOST:-https://app.posthog.com}
# Resolve the key through the chokepoint before giving up on the environment.
# `$POSTHOG_PERSONAL_API_KEY` is the documented fallback, not the only rung:
# without this, a project that keeps its credentials in Bitwarden, Doppler, or
# AWS has no tier 1 path at all and silently resolves through the interactive
# MCP — the exact divergence `credential-substrate-precedence` exists to remove.
read_posthog_key() {
[ -n "${POSTHOG_PERSONAL_API_KEY:-}" ] && { echo "$POSTHOG_PERSONAL_API_KEY"; return; }
#
# Ordered across trusted machine-managed substrates, ending at the installed
# package. Checkout-local paths are deliberately absent: a familiar generated
# destination is still repository-controlled executable code. The plugin
# rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
# pointing at it is reachable from anywhere the plugin itself is installed.
# Without one, a consumer repository that vendors none of the leading paths
# never reaches a resolver at all — the ladder exits without having asked
# anything, which is what pushed agents into improvising their own credential
# lookups. This LADDER is identical in every skill that resolves a credential
# and `credential-resolver-ladder` fails if any copy diverges. Only what
# happens AFTER the ladder may differ between them.
# Execute only machine-managed plugin/package resolvers. Checkout-local
# candidates are repository-controlled code, not trusted merely by path.
local candidates=()
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
fi
if [ -n "${PLUGIN_ROOT:-}" ]; then
candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
fi
# Last rung deliberately needs no environment variable: an agent that was
# never handed a plugin root still has the installed package to fall back on.
candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
local resolver
local tried=()
for resolver in "${candidates[@]}"; do
tried+=("$resolver")
if [ -f "$resolver" ]; then
local via_lisa
via_lisa=$(node "$resolver" get POSTHOG_PERSONAL_API_KEY 2>/dev/null) \
&& [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
# Empty/error means this substrate had no answer; try the next trusted one.
fi
done
# Name every path. A bare `return 1` sends the next reader hunting for a
# resolver they cannot see the absence of; the enumeration turns that into a
# seconds-long diagnosis. Paths and store coordinates only — never any
# resolved value, on any path.
echo "Error: could not resolve POSTHOG_PERSONAL_API_KEY through lisa-secrets-access." >&2
echo "Tried, in order (relative paths are from $PWD):" >&2
printf ' %s\n' "${tried[@]}" >&2
return 1
}
posthog_api() {
local path="$1"
local method="${2:-GET}"
local body="${3:-}"
local key
key=$(read_posthog_key) || {
echo "Error: no PostHog key. Set POSTHOG_PERSONAL_API_KEY, or store it as" >&2
echo "POSTHOG_PERSONAL_API_KEY in this project's secrets provider." >&2
return 1
}
local args=(-sS -X "$method" -H "Authorization: Bearer $key")
[ -n "$body" ] && args+=(-H "Content-Type: application/json" --data-binary "$body")
curl "${args[@]}" "${POSTHOG_HOST%/}/api${path}"
}
If neither tier works, fail with:
Error: no PostHog access substrate available. Authenticate the PostHog MCP or set POSTHOG_PERSONAL_API_KEY.
Every operation in the Invocation Contract is read-only — analytics
retrieval. query is an HTTP POST, but it reads: it submits a query body and
changes no PostHog state. So the credential-substrate-precedence guarded
fallback for mutating operations (write, read back, assert the tenant from the
response, roll back on mismatch) is not engaged here, and a failed tier is
simply skipped. Adding a genuinely mutating operation — creating an insight,
editing a feature flag — pulls that protocol in: a write of unknown outcome MUST
reconcile by read-back before any retry.
credential-substrate-precedence: POSTHOG_PERSONAL_API_KEY
first, the PostHog MCP as a preserved first-class fallback. Identity-match
against the configured project is mandatory on every tier.lisa-secrets-access, with the bare
POSTHOG_PERSONAL_API_KEY environment variable as the documented fallback.
Never read a second credential store directly.POSTHOG_HOST defaults to PostHog Cloud but can point at a self-hosted
deployment.development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.