Hook + MCP fix: GroupMe send_message requires name-based resolution + user approval #160
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ldraney/claude-custom#160
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Standalone incident-fix — no plan phase. Triggered by 2026-03-25 data exposure incident.
Repo
forgejo_admin/claude-custom(hook + settings.json)forgejo_admin/groupme-mcp(tool interface change)User Story
As platform operator,
I want GroupMe messaging to resolve groups by name and require explicit user approval before sending,
So that sensitive data never reaches the wrong group again.
Context
Incident (2026-03-25): Contract status update with parent names and email addresses was sent to the 6-person Coaches & Staff group (
113983384) instead of the 2-person WKQ Stakeholders group (113996175). A forked session inherited conversation context and firedmcp__groupme__send_messagewith the wrong group_id. Marcus caught it and deleted it within 74 seconds. Coaches may have seen it.Root cause — two failures compounded:
send_messagetakes rawgroup_id— caller grabbed wrong ID from a stale docs table that was missing the Stakeholders group--dangerously-skip-permissionsbypassed all approvalFix approach: Name-based resolution (eliminate stale IDs) + mandatory human approval (PreToolUse hook). The MCP calls
list_groups()live from GroupMe API at send time — no caching, no Postgres sync, no drift.File Targets
Files to modify or create:
claude-custom:
hooks/block-groupme-send.sh— NEW. PreToolUse hook, followsblock-mcp-merge.shpattern. Extractsgroup_namefromtool_input, formats tool-specific summary, returnspermissionDecision: "ask".settings.json— ADD PreToolUse entry matchingmcp__groupme__send_message|mcp__groupme__add_member|mcp__groupme__remove_membertests/test_block_groupme_send.sh— NEW. Hook unit tests (createtests/directory).groupme-mcp:
src/groupme_mcp/server.py— ADD shared_resolve_group(group_name)helper. Callslist_groups(per_page=100)or paginates, fuzzy-matches name, returns group dict. Ambiguous → error with options. No match → error with available names.src/groupme_mcp/tools/messages.py— MODIFYsend_message:group_namereplacesgroup_idsrc/groupme_mcp/tools/members.py— MODIFYadd_member,remove_member,list_members:group_namereplacesgroup_idsrc/groupme_mcp/tools/groups.py— MODIFYget_group:group_namereplacesgroup_idFiles NOT to touch:
groupme-sdk— SDK keeps rawgroup_idinterface, MCP resolves above itAcceptance Criteria
send_message,add_member,remove_member,list_members,get_group) acceptgroup_name, not rawgroup_id_resolve_group()helper inserver.pyused by all 5 tools_resolve_group()callslist_groups(per_page=100)or paginates — must not silently miss groups (currently 10 groups, default page size is 10)send_message,add_member,remove_memberlist_groups,list_members,get_group) excluded from hook — they don't modify external state, no approval neededTest Expectations
send_messagewith exact group name resolves correctly_resolve_grouphandles pagination (>10 groups)tool_input.group_name+tool_input.textthroughblock-groupme-send.sh, verify output haspermissionDecision: "ask"and reason contains group nameadd_membershape (group_name + nickname), verify tool-specific summaryremove_membershape (group_name + membership_id), verify tool-specific summarycd ~/groupme-mcp && pytest tests/andbash ~/claude-custom/tests/test_block_groupme_send.shConstraints
block-mcp-merge.shpattern exactly for the hookuvnot pip, Forgejo PyPI for groupme-sdk dependency_resolve_group()must calllist_groups(per_page=100)or paginate — default page size of 10 will silently miss groups at current counttests/directory (create it). Each test is a shell script piping mock JSON through the hook and asserting output. Pattern:tests/test_block_groupme_send.shgroup_namefromtool_input. If the hook deploys first while the old MCP still sendsgroup_id, the permission prompt shows empty group name. Sequence: (1) merge + deploy groupme-mcp, (2) merge claude-custom hook.Checklist
project-groupme-westsideupdated (remove stale ID table, document live resolution)Related
project-groupme-westside— GroupMe project page (needs doc update)project-pal-e-agency— enforcement architecture (hook layer)sop-claude-config-development— claude-custom development SOPLabels
story:GM-5, arch:enforcement, type:incident-fix
Scope Review: NEEDS_REFINEMENT
Review note:
review-357-2026-03-25Ticket is well-structured with all template sections present, but name-based resolution scope is incomplete.
send_message. Same stale-ID vulnerability applies to member operations. Either extend File Targets to includemembers.pyor document as deliberate Phase 2.group_name+text, butadd_member/remove_memberhave different fields (nickname, membership_id). Hook logic needs to account for this.Scope Expansion (post-review
review-357-2026-03-25)Reviewer flagged two issues — both accepted. Expanding scope:
1. All group-scoped tools get name-based resolution (not just send_message)
add_member,remove_member, andlist_membersalso take rawgroup_id. An agent could still calladd_member(group_id="113983384")with a wrong ID even after send_message is fixed.Fix: All four tools (
send_message,add_member,remove_member,list_members) switch fromgroup_idtogroup_name. Internal resolution vialist_groups()for all.Updated File Targets for groupme-mcp:
src/groupme_mcp/tools/messages.py—send_message:group_namereplacesgroup_idsrc/groupme_mcp/tools/members.py—add_member,remove_member,list_members:group_namereplacesgroup_id_resolve_group(group_name)helper insrc/groupme_mcp/server.pythat all tools call2. Hook handles different tool_input shapes
The hook matches three write tools with different parameters:
send_message→ hasgroup_name+textadd_member→ hasgroup_name+nickname+ contact inforemove_member→ hasgroup_name+membership_idFix: Hook extracts
group_name(common to all) and formats a tool-specific summary:3. Minor: remove redundant acceptance criterion
"Hook works under --dangerously-skip-permissions" removed — inherently true for all hooks.
Updated Acceptance Criteria
send_message,add_member,remove_member,list_members) acceptgroup_name, not rawgroup_id_resolve_group()helper used by all toolssend_message,add_member,remove_memberwith tool-specific summariesScope Review (re-review): NEEDS_REFINEMENT
Review note:
review-357-2026-03-25-v2The two issues from the first review are addressed by the scope expansion. Three new issues found after codebase verification.
get_groupingroups.pyalso takes rawgroup_id-- 5th tool with the stale-ID vulnerability, not in scope. Either add to name-based resolution or document exclusion rationale.permissionDecision: ask"). Should be restored.group_nameintool_input). Add to Constraints.Scope Expansion v2 (post-review
review-357-2026-03-25-v2)Three issues flagged — all accepted.
1.
get_groupadded to name-based resolutionsrc/groupme_mcp/tools/groups.pyline 42 —get_group(group_id=...)is the 5th tool taking rawgroup_id. Read-only but same stale-ID vulnerability. Now included.All 5 group-scoped tools get
group_name:send_message,add_member,remove_member,list_members,get_group. All use shared_resolve_group()helper.Updated File Targets for groupme-mcp:
src/groupme_mcp/tools/messages.py—send_messagesrc/groupme_mcp/tools/members.py—add_member,remove_member,list_memberssrc/groupme_mcp/tools/groups.py—get_groupsrc/groupme_mcp/server.py— shared_resolve_group(group_name)helper2. Hook unit test restored
Add to Test Expectations:
tool_input.group_name+tool_input.textthroughblock-groupme-send.sh, verify output JSON haspermissionDecision: "ask"and reason contains group nameadd_membershape (group_name + nickname), verify tool-specific summaryremove_membershape (group_name + membership_id), verify tool-specific summary3. Deploy ordering constraint
Added to Constraints:
group_namefromtool_input. If the hook deploys first while the old MCP still sendsgroup_id, the permission prompt shows empty group name. Sequence: (1) merge + deploy groupme-mcp, (2) merge claude-custom hook.4. Read-only exclusion rationale (non-blocking, documenting for implementing agent)
The PreToolUse hook matches write operations only:
send_message,add_member,remove_member. Read-only tools (list_groups,list_members,get_group) are excluded from the hook — they don't modify external state and don't need human approval.list_membersandget_groupstill get name-based resolution (consistency), just no approval gate.Final Consolidated Acceptance Criteria
group_name, not rawgroup_id_resolve_group()helper inserver.pyused by all 5 toolssend_message,add_member,remove_memberlist_groups,list_members,get_group) excluded from hook — documented rationaleScope Review (re-review v3): NEEDS_REFINEMENT
Review note:
review-357-2026-03-25-v3Both v1 and v2 expansion issues are fully addressed. Two new issues found during codebase verification of the consolidated spec.
list_groupspagination unaddressed —_resolve_group()will call SDK'slist_groups(page=1, per_page=10)which returns max 10 groups. If the user belongs to >10 groups, name resolution silently misses groups and returns false "no match" errors. Add one line to Constraints: helper must paginate or useper_page=100.claude-customhas notests/directory. Scope says "pipe mock JSON through hook" but doesn't say where test scripts live. Implementing agent needs a location decision.Scope Expansion v3 (post-review
review-357-2026-03-25-v3)Two minor refinements — both accepted.
1. SDK pagination:
_resolve_group()must paginategroupme-sdklist_groups(page=1, per_page=10)returns max 10 groups per call. Currently at 10 groups (9 team + 1 stakeholders) — already at the edge.Fix: Add to Constraints:
_resolve_group()must calllist_groups(per_page=100)or implement pagination loop to avoid silently missing groups. Currently 10 groups, default page size is 10 — will break on the 11th group without this.2. Hook test location specified
claude-customhas notests/directory.Fix: Add to Constraints:
tests/directory (create it). Each hook test is a shell script that pipes mock JSON through the hook and asserts output. Pattern:tests/test_block_groupme_send.sh. Run withbash tests/test_block_groupme_send.sh.Scope Review (re-review v4): READY
Review note:
review-357-2026-03-25-v4All three prior rounds of refinement are fully addressed. Consolidated spec is complete and agent-executable.
Verified:
list_groups(page=1, per_page=10)pagination edge confirmed -- currently at 10 groupsblock-mcp-merge.shpattern (19 lines) is a clean template for the new hooksettings.jsonPreToolUse section uses pipe-delimited matchers -- new entry follows conventiongroup_id="12345"and will need rewrite (no silent breakage)group_idreferences in claude-custom or other MCP servers