feat: Global hooks approval modal with plan/question/permission modes
- Add PermissionRequest and Stop approval hooks to local Claude config - Unify PermissionApproval into multi-mode card (permission, plan, question) - Support allowAlways, deny-with-reason, and AskUserQuestion answering - Add cross-process broadcast fallback (HTTP to sync server) - Fix approval scripts to default to .claude/debug/ for local agent
This commit is contained in:
@@ -196,14 +196,14 @@ export async function handleHooksApprovalRespond(req: Request): Promise<Response
|
||||
if (req.method !== 'POST') return null
|
||||
|
||||
try {
|
||||
const body = await req.json() as { requestId: string; decision: 'allow' | 'deny' }
|
||||
const body = await req.json() as { requestId: string; decision: string; reason?: string }
|
||||
|
||||
if (!body.requestId || !body.decision) {
|
||||
return errorResponse('Missing requestId or decision', 400)
|
||||
}
|
||||
|
||||
if (!['allow', 'deny'].includes(body.decision)) {
|
||||
return errorResponse('Decision must be "allow" or "deny"', 400)
|
||||
if (!['allow', 'allowAlways', 'deny'].includes(body.decision)) {
|
||||
return errorResponse('Decision must be "allow", "allowAlways", or "deny"', 400)
|
||||
}
|
||||
|
||||
const pending = pendingRequests.get(body.requestId)
|
||||
@@ -212,30 +212,42 @@ export async function handleHooksApprovalRespond(req: Request): Promise<Response
|
||||
return errorResponse('Permission request not found or expired', 404)
|
||||
}
|
||||
|
||||
console.log(`[HooksApproval] Responding permission ${body.requestId}: ${body.decision}`)
|
||||
console.log(`[HooksApproval] Responding permission ${body.requestId}: ${body.decision}${body.reason ? ' reason=' + body.reason : ''}`)
|
||||
clearTimeout(pending.timer)
|
||||
pendingRequests.delete(body.requestId)
|
||||
|
||||
// Build hookSpecificOutput per PermissionRequest docs:
|
||||
// hookSpecificOutput.decision.behavior = "allow" | "deny"
|
||||
const hookOutput: Record<string, unknown> = body.decision === 'allow'
|
||||
? {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PermissionRequest',
|
||||
decision: {
|
||||
behavior: 'allow'
|
||||
}
|
||||
}
|
||||
}
|
||||
: {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PermissionRequest',
|
||||
decision: {
|
||||
behavior: 'deny',
|
||||
message: 'Denied via UI'
|
||||
}
|
||||
// Build hookSpecificOutput per PermissionRequest docs
|
||||
let hookOutput: Record<string, unknown>
|
||||
|
||||
if (body.decision === 'allow') {
|
||||
hookOutput = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PermissionRequest',
|
||||
decision: { behavior: 'allow' }
|
||||
}
|
||||
}
|
||||
} else if (body.decision === 'allowAlways') {
|
||||
// Allow + add tool to allowed permissions for session
|
||||
const toolName = pending.payload.tool_name as string || '*'
|
||||
hookOutput = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PermissionRequest',
|
||||
decision: { behavior: 'allow' },
|
||||
allowedPermissions: [{ tool_name: toolName }]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// deny — with optional custom message
|
||||
hookOutput = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: 'PermissionRequest',
|
||||
decision: {
|
||||
behavior: 'deny',
|
||||
message: body.reason || 'Denied via UI'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pending.resolve(hookOutput)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user