Skip to content

fix(mcp): parse gate-exec-write input with python json+shlex#69

Open
phjlljp wants to merge 1 commit into
PostHog:mainfrom
phjlljp:fix/gate-exec-write-parser
Open

fix(mcp): parse gate-exec-write input with python json+shlex#69
phjlljp wants to merge 1 commit into
PostHog:mainfrom
phjlljp:fix/gate-exec-write-parser

Conversation

@phjlljp
Copy link
Copy Markdown

@phjlljp phjlljp commented May 8, 2026

Summary

Replaces the bash-regex JSON parser inside hooks/gate-exec-write.sh with python3 + json.loads + shlex.split. python3 is already required by the SessionEnd hook so this introduces no new dependency. Adds four write verbs that the hardcoded list was missing (purge, revoke, truncate, terminate, kill, void, expire, flush, clear, grant).

The previous regex parser silently fell through to exit 0 (no prompt) on five command shapes that the MCP server's underlying CLI tokenizer accepts as ordinary call invocations:

Bypass Before After
"command":" call experiment-update {}" (leading whitespace) silent prompts
"command":"call\texperiment-update {}" (tab separator) silent prompts
"command":"\ncall experiment-update {}" (leading newline) silent prompts
"command":"CALL experiment-update {}" (uppercase verb) silent prompts
"command":"call \"experiment-update\" {}" (JSON-escaped quotes around the tool name) silent prompts

Each one let an agent issue a destructive PostHog write — experiment-update, notebooks-destroy, cdp-functions-delete, etc. — without surfacing the permission prompt this hook exists to enforce. Once a user allow-lists mcp__posthog__exec, the gate is the only thing standing between an agent and a state-changing call. Closing the bypasses restores the property the hook was added in #42 to provide.

The existing POSTHOG_MCP_EXEC_GATE_ALLOW env-var contract is preserved (no behaviour change for users who allow-list specific tools).

Why python3 instead of more bash

Bash regex over raw JSON conflated value-content with field-structure. Each bypass exploited a place where the regex assumed structural simplicity that JSON doesn't actually guarantee — leading whitespace inside a value, JSON \n/\t escapes, alternate field ordering, JSON-escaped inner quotes. json.loads + shlex.split lets the gate parse the command exactly the way a kebab-case CLI would, so any input the server tokenizes the same way is classified the same way.

The hook also now default-denies (returns permissionDecision: ask) when the command can't be cleanly tokenized at all (unbalanced quotes, etc.) or when call is invoked without a recognizable tool name. Better to over-prompt on a malformed payload than silently let it through.

Bash 3.2 compatibility

mapfile would have been the natural way to read the four-line python output, but macOS still ships bash 3.2. The portable read -r chain stays compatible with the plugin's existing local-dev story.

Test plan

  • ./tests/test_gate_exec_write.sh — 32/32 pass (21 original cases unchanged + 11 new: 5 bypass regressions, 4 newly-classified write verbs, 2 default-deny shapes)
  • pytest tests/test_session_parser.py tests/test_posthog_llma.py — 51/51 pass (untouched by this PR)
  • Manually verified each bypass payload against the new parser:
    • leading whitespace, tab, newline, uppercase CALL, JSON-quoted tool name → all prompt
    • call data-purge, api-key-revoke, event-truncate, session-terminate → all prompt
    • call "unterminated quote, call with no tool → default-deny prompt
  • CI green on this PR

Notes

This is one of three security PRs from a strict-zero-trust audit; the other two cover POSTHOG_HOST validation and sync-skills.yml zip-slip / auto-merge. They land independently of this one.

Bumps .claude-plugin/plugin.json to 1.1.21 (Claude-only change, matching the convention used by #59 and #42).

The bash-regex JSON parser silently failed open on five command shapes
that the underlying MCP server's tokenizer happily accepts:

  - leading whitespace before `call`
  - tab separator between `call` and the tool name
  - leading newline inside the JSON command value
  - JSON-escaped quotes around the tool name (`call "experiment-update"`)
  - upper-case `CALL`

Each let an agent issue a destructive PostHog write (experiment-update,
notebooks-destroy, cdp-functions-delete, ...) without surfacing the
permission prompt the hook was added for. The hardcoded write-verb list
was also incomplete — `purge`, `revoke`, `truncate`, and `terminate`
matched no entry and silently fell through.

Switch JSON parsing and command tokenization to python3 (already
required by the SessionEnd hook), so any command-shape the server
accepts is parsed identically by the gate. Default-deny on parse
failure or on a `call` with no extractable tool name. Add the missing
write verbs.

Tests: 11 new cases (5 bypass regressions, 4 new write verbs, 2
default-deny shapes). All 32 pass; 21 prior cases unchanged.

Bumps .claude-plugin/plugin.json to 1.1.21 (Claude-only change).
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