[FEATURE]
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
There's no way to hook into plan rejection in Claude Code. When a user rejects a plan in plan mode (with or without feedback), no hook fires that exposes the rejected plan or the rejection feedback. This prevents workflows like:
- Logging rejected plans with the user's reasoning to a
rejections.mdor similar - Posting rejected plans to an issue tracker for follow-up
- Building a dataset of "what I usually push back on" to feed back into
CLAUDE.mdor skills - More customized interactions that could be queue'd off of a plan rejection
Existing hooks don't cover this:
PermissionDeniedonly fires for auto-mode classifier denials, not manual rejection (per the hooks reference)PostToolUsematched onExitPlanModeonly fires on approvalPermissionRequestcan intercept the dialog but can't observe the outcome when the user rejects
Proposed Solution
Add a PlanRejected hook event that fires when the user rejects an ExitPlanMode request. Suggested payload:
{
"session_id": "abc123",
"hook_event_name": "PlanRejected",
"transcript_path": "...",
"plan": "<rejected plan content from ExitPlanMode.input.plan>",
"feedback": "<user's rejection message, if any>"
}
Related to but distinct from #14259, which proposes pre/post plan lifecycle events generally — this is specifically the rejection branch, with the user's feedback surfaced as part of the payload.
Alternative Solutions
Workaround via UserPromptSubmit + transcript inspection (a hack). Listen on UserPromptSubmit, scan the transcript for the most recent ExitPlanMode tool_use with no matching tool_result, and treat the incoming prompt as the rejection feedback:
#!/bin/bash
# ~/.claude/hooks/plan-rejection.sh
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path')
PROMPT=$(echo "$INPUT" | jq -r '.prompt')
# Pull the most recent ExitPlanMode plan from the transcript.
# (A real check would also verify no matching tool_result exists.)
LAST_PLAN=$(jq -rs '
[.[] | select(.message.content[]?.type == "tool_use"
and .message.content[].name == "ExitPlanMode")][-1]
| .message.content[] | select(.name=="ExitPlanMode") | .input.plan
' "$TRANSCRIPT")
if [[ -n "$LAST_PLAN" ]]; then
{
echo "## $(date -Iseconds)"
echo "### Rejected plan"
echo "$LAST_PLAN"
echo "### Feedback"
echo "$PROMPT"
} >> "${CLAUDE_PROJECT_DIR:-.}/.plans/rejections.md"
fi
exit 0
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/plan-rejection.sh" }] }
]
}
}
This works today but is brittle: it relies on transcript shape, a heuristic for "unresolved ExitPlanMode," and it can't distinguish a true rejection from other prompt-submission scenarios without more careful state tracking. A first-class PlanRejected event would make this deterministic and pass the rejection feedback explicitly.
Stop hook + sentinel file. Stamp a sentinel when PreToolUse:ExitPlanMode fires, then check for it on Stop / UserPromptSubmit. More moving parts than the transcript-scan approach and same fundamental fragility.
Priority
High - Significant impact on productivity
Feature Category
API and model interactions
Use Case Example
_No response_
Additional Context
_No response_
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗