Skip to content

feat(phai): gate PostHog MCP exec writes behind approval on Codex#115

Open
skoob13 wants to merge 1 commit into
mainfrom
feat/codex-exec-write-approval
Open

feat(phai): gate PostHog MCP exec writes behind approval on Codex#115
skoob13 wants to merge 1 commit into
mainfrom
feat/codex-exec-write-approval

Conversation

@skoob13

@skoob13 skoob13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Problem

The PostHog MCP exposes a single exec umbrella tool. Once a user allow-lists it on Codex, every dispatched write (experiment-update, notebooks-destroy, cdp-functions-delete, …) runs without a prompt. The existing Claude Code write-gate (hooks/gate-exec-write.sh) deliberately skips Codex because Codex's PreToolUse protocol rejects permissionDecision: "ask", so on Codex there was no plugin-level write gate at all.

Changes

Enforce writes-only approval on Codex, mirroring the Claude Code behavior. Codex's PermissionRequest hook can only allow/deny/defer (there is no "ask" verdict), so two pieces are needed:

  • .mcp.json — put the posthog server in prompt approval mode (default_tools_approval_mode), so every exec call reaches the approval flow by default.
  • hooks/codex-gate-exec-write.sh (new) + hooks/codex-hooks.json (new), registered via .codex-plugin/plugin.json — a PermissionRequest hook that returns allow for reads / meta verbs / allow-listed writes (suppressing the prompt) and defers writes so the prompt fires only on them.
  • hooks/lib-exec-gate.sh (new) — extracts the write-verb regex and exec-payload parsing into one sourced helper, now used by both the Claude and Codex gates so the write surface can't drift. Claude Code behavior is unchanged.
  • README — documents the one-time /hooks trust step on Codex and the POSTHOG_MCP_EXEC_GATE_ALLOW opt-out (works on both clients).

Note: a freshly installed plugin hook is untrusted on Codex and does not run until the user trusts it via /hooks (Codex prints a startup warning). The plugin sets prompt as a default; a user can still override it in their own config.

How did you test this code?

  • Unit-tested both gates by piping sample PermissionRequest/PreToolUse payloads through the scripts:
    • Codex gate → allow for meta verbs, read calls, non-exec tools, and allow-listed writes; no decision (defer → prompt) for write calls, allowlist mismatches, and unparseable calls.
    • Claude gate (regression) → ask only for non-allow-listed writes (incl. plugin-prefixed mcp__posthog_posthog__exec), silent otherwise, and still skips on Codex (PLUGIN_ROOT set).
  • jq validation on all touched JSON; bash -n on all scripts.
  • Remaining manual checks (need a live client): confirm Claude Code still connects the posthog MCP server with the new .mcp.json key, and that on Codex a read runs silently while a write prompts after trusting the hook.

Publish to changelog?

no

The PostHog MCP exposes a single `exec` umbrella tool. Once a user allow-lists
it on Codex, every dispatched write (experiment-update, notebooks-destroy, …)
runs without a prompt — the Claude Code write-gate deliberately skips Codex
because its PreToolUse protocol rejects `permissionDecision: "ask"`.

Enforce writes-only approval on Codex via two pieces, since Codex's
PermissionRequest hook can only allow/deny/defer (there is no "ask"):
- put the posthog server in `prompt` approval mode in .mcp.json, so every exec
  call reaches the approval flow by default;
- add a PermissionRequest hook (hooks/codex-gate-exec-write.sh, registered via
  .codex-plugin/plugin.json) that returns `allow` for reads/meta verbs and
  allow-listed writes (suppressing the prompt) and defers writes so the prompt
  fires only on them.

Extract the shared write-verb regex and exec-payload parsing into
hooks/lib-exec-gate.sh, now sourced by both the Claude and Codex gates so the
write surface can't drift. Claude Code behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant