Compare commits

..

1 Commits

Author SHA1 Message Date
Claude
411f17c228 feat: Add accept-with-feedback plugin
Add a new plugin that allows users to approve permission requests while
providing guidance to Claude. This addresses the request for an "accept
with feedback" option (similar to the existing "reject with feedback"
for plans).

The plugin includes:
- PermissionRequest hook to intercept permission prompts
- /accept-feedback command for one-time feedback
- /configure-feedback command for persistent rules
- Configuration file support for automated feedback rules

Users can now provide context like "make sure to add error handling"
when approving operations, and Claude receives this as guidance.
2025-12-14 16:13:27 +00:00
11 changed files with 473 additions and 270 deletions

View File

@@ -12,6 +12,7 @@ Learn more in the [official plugins documentation](https://docs.claude.com/en/do
| Name | Description | Contents |
|------|-------------|----------|
| [accept-with-feedback](./accept-with-feedback/) | Accept permission requests with feedback - provide guidance to Claude when approving operations | **Commands:** `/accept-feedback`, `/configure-feedback` - Set one-time or persistent feedback<br>**Hook:** PermissionRequest - Intercepts permissions to add user guidance |
| [agent-sdk-dev](./agent-sdk-dev/) | Development kit for working with the Claude Agent SDK | **Command:** `/new-sdk-app` - Interactive setup for new Agent SDK projects<br>**Agents:** `agent-sdk-verifier-py`, `agent-sdk-verifier-ts` - Validate SDK applications against best practices |
| [claude-opus-4-5-migration](./claude-opus-4-5-migration/) | Migrate code and prompts from Sonnet 4.x and Opus 4.1 to Opus 4.5 | **Skill:** `claude-opus-4-5-migration` - Automated migration of model strings, beta headers, and prompt adjustments |
| [code-review](./code-review/) | Automated PR code review using multiple specialized agents with confidence-based scoring to filter false positives | **Command:** `/code-review` - Automated PR review workflow<br>**Agents:** 5 parallel Sonnet agents for CLAUDE.md compliance, bug detection, historical context, PR history, and code comments |

View File

@@ -0,0 +1,9 @@
{
"name": "accept-with-feedback",
"version": "1.0.0",
"description": "Accept permission requests with feedback - provide guidance to Claude when approving operations",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"
}
}

View File

@@ -0,0 +1,151 @@
# Accept with Feedback
A Claude Code plugin that lets you approve operations while providing guidance to Claude. Instead of just accepting or rejecting permission requests, you can accept *with feedback* - approving the operation while giving Claude additional context or instructions.
## Why?
Sometimes you want to approve an operation but also want to guide Claude's behavior:
- "Yes, edit that file, but make sure to add error handling"
- "Okay to run those tests, but skip the slow integration tests"
- "Go ahead and commit, but use conventional commit format"
This plugin bridges the gap between simple approval and rejection-with-feedback.
## Installation
This plugin is included in the Claude Code repository. Enable it in your settings or use:
```bash
claude /plugin install accept-with-feedback
```
## Usage
### One-time feedback
Use the `/accept-feedback` command to queue feedback for the next permission request:
```
/accept-feedback Make sure to preserve backwards compatibility
```
When Claude next asks for permission (e.g., to edit a file), the operation will be automatically approved and Claude will receive your guidance as a system message.
### Persistent feedback rules
Create rules that automatically provide feedback for certain types of operations. Add configuration to `.claude/accept-feedback.json`:
```json
{
"rules": [
{
"matcher": "Edit|Write",
"conditions": {
"file_path": ".py"
},
"feedback": "Follow PEP 8 style and add type hints to all functions."
},
{
"matcher": "Bash",
"conditions": {
"command": "npm"
},
"feedback": "Use --legacy-peer-deps if you encounter peer dependency issues."
}
]
}
```
Use `/configure-feedback` for an interactive configuration experience.
## Configuration
### Rule properties
| Property | Description | Example |
|----------|-------------|---------|
| `matcher` | Tool name pattern | `"Edit"`, `"Write\|Edit"`, `"*"` |
| `conditions` | Optional filters on tool input | `{"file_path": ".ts"}` |
| `feedback` | Guidance message for Claude | `"Add JSDoc comments"` |
### Configuration locations
- **User-level**: `~/.claude/accept-feedback.json`
- **Project-level**: `.claude/accept-feedback.json` (takes precedence)
## Commands
| Command | Description |
|---------|-------------|
| `/accept-feedback <message>` | Queue feedback for the next permission request |
| `/configure-feedback` | Interactive configuration of persistent rules |
## How it works
1. The plugin uses a `PermissionRequest` hook to intercept permission requests
2. When a permission request occurs:
- If there's pending feedback (from `/accept-feedback`), approve with that feedback
- If a configured rule matches, approve with the rule's feedback
- Otherwise, let the normal permission flow proceed
3. Feedback is sent to Claude as a system message, providing guidance for the operation
## Examples
### Example 1: One-time guidance
```
You: /accept-feedback Please add comprehensive error handling
Claude: I'll edit src/api.ts to add the new endpoint...
[Permission automatically approved with your feedback]
Claude: I've added the endpoint with try-catch blocks and proper error responses...
```
### Example 2: Persistent Python style rules
`.claude/accept-feedback.json`:
```json
{
"rules": [
{
"matcher": "Edit|Write",
"conditions": {
"file_path": ".py"
},
"feedback": "Use Google-style docstrings and add type hints to all function signatures."
}
]
}
```
Every time Claude edits a Python file, it receives this style guidance.
### Example 3: Git workflow rules
```json
{
"rules": [
{
"matcher": "Bash",
"conditions": {
"command": "git commit"
},
"feedback": "Use conventional commit format: type(scope): description"
}
]
}
```
## Tips
- Use specific matchers to avoid unnecessary approvals
- Conditions use substring matching - `".py"` matches any path containing `.py`
- Combine with other permission management for a comprehensive workflow
- Feedback is visible in the conversation, so Claude can reference it
## Related
- Rejecting with feedback: Built into Claude Code's plan rejection flow
- Permission hooks: See the [hook development guide](../plugin-dev/skills/hook-development/SKILL.md)

View File

@@ -0,0 +1,63 @@
---
description: Set feedback to provide when accepting the next permission request
argument_name: feedback
---
# Accept with Feedback
You are helping the user set feedback that will be automatically provided to Claude when the next permission request is approved.
## What the user wants
The user wants to approve an upcoming operation but also provide guidance or feedback to Claude. This feedback will:
1. Automatically approve the next permission request
2. Send the feedback message to Claude as guidance
## Instructions
1. Parse the user's feedback from the argument: `$ARGUMENTS`
2. If feedback was provided, save it for the next permission request:
- Create/update the file `~/.claude/pending-accept-feedback.json`
- Store the feedback with the current session ID
3. Confirm to the user that their feedback has been queued
## Saving the feedback
Use this Python code to save the pending feedback:
```python
import json
import os
from pathlib import Path
feedback = """$ARGUMENTS"""
session_id = os.environ.get("CLAUDE_SESSION_ID", "default")
pending_file = Path.home() / ".claude" / "pending-accept-feedback.json"
pending_file.parent.mkdir(parents=True, exist_ok=True)
try:
existing = json.loads(pending_file.read_text()) if pending_file.exists() else {}
except:
existing = {}
existing[session_id] = {
"message": feedback,
"one_time": True
}
pending_file.write_text(json.dumps(existing, indent=2))
print(f"Feedback queued for next permission request.")
```
## Example usage
User runs: `/accept-feedback Make sure to add error handling`
Then when Claude asks for permission to edit a file, the operation is automatically approved and Claude receives the guidance: "Make sure to add error handling"
## Response
After saving, confirm: "Your feedback has been queued. The next permission request will be automatically approved, and Claude will receive your guidance."

View File

@@ -0,0 +1,80 @@
---
description: Configure persistent feedback rules for accept-with-feedback
---
# Configure Accept-with-Feedback Rules
You are helping the user configure persistent feedback rules that automatically provide guidance to Claude when certain operations are approved.
## Configuration file location
Rules are stored in `.claude/accept-feedback.json` in either:
- User's home directory (`~/.claude/accept-feedback.json`) for global rules
- Project directory (`.claude/accept-feedback.json`) for project-specific rules
Project rules take precedence over user rules.
## Configuration format
```json
{
"rules": [
{
"matcher": "Edit|Write",
"conditions": {
"file_path": ".py"
},
"feedback": "Ensure all Python code follows PEP 8 style guidelines and includes type hints."
},
{
"matcher": "Bash",
"conditions": {
"command": "git"
},
"feedback": "Use conventional commit format for commit messages."
},
{
"matcher": "*",
"feedback": "Please explain your reasoning before making changes."
}
]
}
```
## Rule properties
- **matcher**: Tool name pattern (e.g., "Edit", "Write|Edit", "*" for all)
- **conditions**: Optional key-value pairs to match against tool input
- **feedback**: The guidance message to send to Claude when this rule matches
## Instructions
1. Ask the user what kind of feedback rules they want to configure
2. Help them create appropriate rules based on their needs
3. Save the configuration to the appropriate location
## Common use cases
1. **Code style guidance**: Provide style guidelines when editing specific file types
2. **Git workflow**: Add commit message format requirements for git operations
3. **Safety reminders**: Add warnings when working with sensitive files
4. **Project conventions**: Enforce project-specific patterns and practices
## Example interaction
User: "I want to always remind Claude to add tests when editing Python files"
You would create:
```json
{
"rules": [
{
"matcher": "Edit|Write",
"conditions": {
"file_path": ".py"
},
"feedback": "Remember to add or update tests for any code changes."
}
]
}
```

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Accept with Feedback Hook for Claude Code
This hook intercepts permission requests and allows users to provide feedback
when accepting operations. The feedback is passed to Claude as a system message,
giving users a way to provide guidance while still approving the operation.
Usage:
1. Set pending feedback: /accept-feedback "your guidance here"
2. When a permission prompt appears, if pending feedback exists:
- The operation is automatically approved
- Your feedback is sent to Claude as guidance
3. Or configure always-on feedback rules in .claude/accept-feedback.json
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
def get_config_paths():
"""Get paths for config and state files."""
claude_dir = Path.home() / ".claude"
project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", "."))
return {
"user_config": claude_dir / "accept-feedback.json",
"project_config": project_dir / ".claude" / "accept-feedback.json",
"pending_feedback": claude_dir / "pending-accept-feedback.json",
}
def load_json_file(path):
"""Load JSON from file, return empty dict if not found."""
try:
if path.exists():
with open(path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
pass
return {}
def get_pending_feedback(session_id):
"""Get pending feedback for this session and clear it."""
paths = get_config_paths()
pending_file = paths["pending_feedback"]
pending = load_json_file(pending_file)
# Check for session-specific pending feedback
if session_id in pending:
feedback = pending[session_id]
# Clear the pending feedback after use
del pending[session_id]
try:
if pending:
with open(pending_file, "w") as f:
json.dump(pending, f)
else:
pending_file.unlink(missing_ok=True)
except IOError:
pass
return feedback.get("message"), feedback.get("one_time", True)
return None, True
def get_configured_feedback(tool_name, tool_input):
"""Get feedback configured for this tool type."""
paths = get_config_paths()
# Load both user and project configs
user_config = load_json_file(paths["user_config"])
project_config = load_json_file(paths["project_config"])
# Project config takes precedence
config = {**user_config, **project_config}
rules = config.get("rules", [])
for rule in rules:
# Check if rule matches this tool
matcher = rule.get("matcher", "*")
if matcher == "*" or tool_name in matcher.split("|"):
# Check conditions if specified
conditions = rule.get("conditions", {})
matches = True
for key, pattern in conditions.items():
value = str(tool_input.get(key, ""))
if pattern not in value:
matches = False
break
if matches:
return rule.get("feedback")
return None
def main():
"""Main hook function."""
# Read input from stdin
try:
raw_input = sys.stdin.read()
input_data = json.loads(raw_input)
except json.JSONDecodeError:
# Can't parse input, don't interfere
sys.exit(0)
session_id = input_data.get("session_id", "default")
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Check for pending feedback first (from /accept-feedback command)
pending_message, _ = get_pending_feedback(session_id)
if pending_message:
# We have pending feedback - approve with the feedback as system message
result = {
"hookSpecificOutput": {
"permissionDecision": "allow"
},
"systemMessage": f"[User Feedback on Approval]: {pending_message}"
}
print(json.dumps(result))
sys.exit(0)
# Check for configured feedback rules
configured_feedback = get_configured_feedback(tool_name, tool_input)
if configured_feedback:
# We have configured feedback for this tool type
result = {
"hookSpecificOutput": {
"permissionDecision": "allow"
},
"systemMessage": f"[User Guidance]: {configured_feedback}"
}
print(json.dumps(result))
sys.exit(0)
# No feedback configured - don't interfere with the normal permission flow
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,17 @@
{
"description": "Accept with feedback - allows users to provide guidance when approving tool permissions",
"hooks": {
"PermissionRequest": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/accept_with_feedback.py",
"timeout": 300
}
]
}
]
}
}

View File

@@ -44,17 +44,6 @@
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification.py",
"timeout": 10
}
]
}
]
}
}

View File

@@ -1,143 +0,0 @@
#!/usr/bin/env python3
"""Notification hook executor for hookify plugin.
This script is called by Claude Code when notifications are sent.
It formats teammate idle notifications and other IPC messages for display.
"""
import os
import sys
import json
from datetime import datetime
# CRITICAL: Add plugin root to Python path for imports
PLUGIN_ROOT = os.environ.get('CLAUDE_PLUGIN_ROOT')
if PLUGIN_ROOT:
parent_dir = os.path.dirname(PLUGIN_ROOT)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
if PLUGIN_ROOT not in sys.path:
sys.path.insert(0, PLUGIN_ROOT)
def format_idle_notification(data: dict) -> str:
"""Format an idle notification for display.
Args:
data: The notification data containing type, from, timestamp, etc.
Returns:
Formatted string for display
"""
worker_name = data.get('from', 'worker')
timestamp = data.get('timestamp', '')
# Format timestamp if present
time_str = ''
if timestamp:
try:
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
time_str = dt.strftime('%H:%M:%S')
except (ValueError, AttributeError):
time_str = ''
# Build the formatted output using the suggested format
lines = [f"{worker_name}"]
if time_str:
lines.append(f" ⎿ Status is idle ({time_str})")
else:
lines.append(" ⎿ Status is idle")
return '\n'.join(lines)
def format_notification(notification_content: str) -> dict:
"""Parse and format a notification message.
Args:
notification_content: Raw notification content (may be JSON or plain text)
Returns:
Dict with formatted systemMessage
"""
# Try to parse as JSON first
try:
data = json.loads(notification_content)
# Check if this is an idle notification
if isinstance(data, dict) and data.get('type') == 'idle_notification':
formatted = format_idle_notification(data)
return {"systemMessage": formatted}
# Handle other notification types
notification_type = data.get('type', '') if isinstance(data, dict) else ''
if notification_type == 'status_update':
worker = data.get('from', 'worker')
status = data.get('status', 'unknown')
return {"systemMessage": f"{worker}\n ⎿ Status: {status}"}
if notification_type == 'progress_update':
worker = data.get('from', 'worker')
progress = data.get('progress', '')
return {"systemMessage": f"{worker}\n{progress}"}
# For unknown JSON types, still try to format nicely
if isinstance(data, dict) and 'from' in data:
worker = data.get('from', 'worker')
msg = data.get('message', data.get('status', 'update'))
return {"systemMessage": f"{worker}\n{msg}"}
except (json.JSONDecodeError, TypeError):
# Not JSON, return as-is
pass
return {}
def main():
"""Main entry point for Notification hook."""
try:
# Read input from stdin
input_data = json.load(sys.stdin)
# Get notification content
notification = input_data.get('notification', '')
# Also check for raw notification data in the input
if not notification and input_data.get('type') == 'idle_notification':
# The input itself is an idle notification
formatted = format_idle_notification(input_data)
result = {"systemMessage": formatted}
elif notification:
# Format the notification content
result = format_notification(notification)
else:
# Check if the input looks like an IPC message
if input_data.get('type') in ['idle_notification', 'status_update', 'progress_update']:
if input_data.get('type') == 'idle_notification':
formatted = format_idle_notification(input_data)
result = {"systemMessage": formatted}
else:
worker = input_data.get('from', 'worker')
status = input_data.get('status', input_data.get('type', 'update'))
result = {"systemMessage": f"{worker}\n{status}"}
else:
result = {}
# Always output JSON
print(json.dumps(result), file=sys.stdout)
except Exception as e:
error_output = {
"systemMessage": f"Notification format error: {str(e)}"
}
print(json.dumps(error_output), file=sys.stdout)
finally:
# ALWAYS exit 0
sys.exit(0)
if __name__ == '__main__':
main()

View File

@@ -1,50 +0,0 @@
#!/bin/bash
# Example: Format teammate idle notification
#
# This script demonstrates how to format raw JSON idle notifications
# into user-friendly display format.
#
# Usage: echo '{"type":"idle_notification","from":"worker-1","timestamp":"..."}' | ./format-idle-notification.sh
set -euo pipefail
# Read JSON from stdin
input=$(cat)
# Parse notification type
notification_type=$(echo "$input" | jq -r '.type // empty' 2>/dev/null || echo "")
if [[ "$notification_type" == "idle_notification" ]]; then
# Extract fields
worker_name=$(echo "$input" | jq -r '.from // "worker"')
timestamp=$(echo "$input" | jq -r '.timestamp // empty')
# Format timestamp if present
time_str=""
if [[ -n "$timestamp" ]]; then
# Try to format the timestamp
time_str=$(date -d "$timestamp" '+%H:%M:%S' 2>/dev/null || echo "")
fi
# Output formatted notification using recommended format:
# ⏺ worker-1
# ⎿ Status is idle
echo "$worker_name"
if [[ -n "$time_str" ]]; then
echo " ⎿ Status is idle ($time_str)"
else
echo " ⎿ Status is idle"
fi
# Output JSON for hook system
if [[ -n "$time_str" ]]; then
jq -n --arg msg "$worker_name\n ⎿ Status is idle ($time_str)" \
'{"systemMessage": $msg}'
else
jq -n --arg msg "$worker_name\n ⎿ Status is idle" \
'{"systemMessage": $msg}'
fi
else
# Not an idle notification, pass through
echo "$input"
fi

View File

@@ -344,69 +344,3 @@ fi
- Per-project settings
- Team-specific rules
- Dynamic validation criteria
## Pattern 11: Format Teammate Idle Notifications
Format raw JSON IPC messages from workers/teammates into user-friendly display:
```json
{
"Notification": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification.py",
"timeout": 10
}
]
}
]
}
```
**Example script (format-idle-notification.py):**
```python
#!/usr/bin/env python3
import sys
import json
def format_idle_notification(data):
"""Format idle notification for display."""
worker_name = data.get('from', 'worker')
# Output format:
# ⏺ worker-1
# ⎿ Status is idle
return f"{worker_name}\n ⎿ Status is idle"
def main():
input_data = json.load(sys.stdin)
# Check for idle notification
if input_data.get('type') == 'idle_notification':
formatted = format_idle_notification(input_data)
print(json.dumps({"systemMessage": formatted}))
else:
print(json.dumps({}))
if __name__ == '__main__':
main()
```
**Input (raw JSON IPC message):**
```json
{"type": "idle_notification", "from": "worker-1", "timestamp": "2025-12-15T05:22:40.320Z"}
```
**Output (formatted for display):**
```
⏺ worker-1
⎿ Status is idle
```
**Use for:**
- Formatting teammate/worker status messages
- Converting internal IPC messages to user-friendly display
- Multi-agent swarm coordination UI
- Any notification that shouldn't show raw JSON to users