.claude/skills/webui-permission-flow/SKILL.md
This skill should be used when the user asks about "permission request", "control_request", "control_response", "permission prompt tool", "permission-prompt-tool stdio", "permission popup not showing", "permission dialog missing", "authorize tool use", "allow deny button", "webuiSession permission", "respondPermission", "updatedInput", "control_cancel_request", "permission cancelled", "permission_cancelled", "pendingInputs", "permission flow webui", "vibe permission", "前端没有弹出授权", "权限请求不显示", "权限弹窗", "webuiSession vs claudeSession", "webui parity", or needs to debug, extend, or understand how tool permission requests flow between Claude Code CLI, the Go WebUI backend, and the React frontend.
npx skillsauth add liuyu520/cc-connect-fork webui-permission-flowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Document the complete permission request/response pipeline in the WebUI/Vibe Coding path. This is a critical flow that differs structurally from the IM path and has been a source of multi-layer bugs.
Claude Code CLI (subprocess)
│ stdout: {"type":"control_request","request_id":"...","request":{...}}
▼
webuiSession.parseEvent() ← core/webui.go
│ WebSocket: {"type":"permission_request","request_id":"...","tool_name":"..."}
▼
VibeSession.tsx handleServerMessage ← web/src/pages/VibeCoding/VibeSession.tsx
│ User clicks Allow/Deny
▼
WebSocket: {"type":"permission","request_id":"...","behavior":"allow"}
│
▼
webuiSession.respondPermission() ← core/webui.go
│ stdin: {"type":"control_response","response":{"subtype":"success",...}}
▼
Claude Code CLI (resumes execution)
Without --permission-prompt-tool stdio, Claude Code CLI will NOT output
control_request events to stdout. Instead, it handles permissions through
its internal TTY mechanism, which doesn't work in a subprocess context without
a terminal.
// core/webui.go — webuiSession.start()
// MUST include --permission-prompt-tool stdio
args := []string{
"--output-format", "stream-json",
"--input-format", "stream-json",
"--permission-prompt-tool", "stdio", // CRITICAL: enables control_request events
"--verbose",
}
Compare with the IM path (agent/claudecode/session.go):
args := []string{
"--output-format", "stream-json",
"--input-format", "stream-json",
"--permission-prompt-tool", "stdio", // Same flag required
"--verbose",
}
Claude Code CLI outputs permission requests in this format:
{
"type": "control_request",
"request_id": "req_abc123",
"request": {
"subtype": "can_use_tool",
"tool_name": "Bash",
"input": {
"command": "rm -rf /tmp/test",
"description": "Delete temp files"
}
}
}
Key structure points:
event["request"], NOT event["tool"]request["tool_name"], NOT request["name"]request["input"]request["subtype"] == "can_use_tool" before processing{
"type": "permission_request",
"request_id": "req_abc123",
"tool_name": "Bash",
"tool_input": "命令: rm -rf /tmp/test",
"tool_input_full": {"command": "rm -rf /tmp/test", "description": "Delete temp files"}
}
{"type": "permission", "request_id": "req_abc123", "behavior": "allow"}
{
"type": "control_response",
"response": {
"subtype": "success",
"request_id": "req_abc123",
"response": {
"behavior": "allow",
"updatedInput": {"command": "rm -rf /tmp/test", "description": "Delete temp files"}
}
}
}
For deny:
{
"type": "control_response",
"response": {
"subtype": "success",
"request_id": "req_abc123",
"response": {
"behavior": "deny",
"message": "The user denied this tool use. Stop and wait for the user's instructions."
}
}
}
When Claude Code cancels a pending permission request:
{
"type": "control_cancel_request",
"request_id": "req_abc123"
}
Go forwards this as {"type": "permission_cancelled", "request_id": "..."} to
the frontend, which auto-marks the permission dialog as "Cancelled".
The webuiSession maintains a pendingInputs map[string]map[string]any that
caches the original tool input from each control_request. This is needed
because:
control_response must include updatedInput
with the original tool input (see agent/claudecode/session.go:480-487)request_id + behavior, not the tool input// On control_request: cache the input
s.pendingInputs[requestID] = toolInput
// On respondPermission (allow): retrieve and return
updatedInput := s.pendingInputs[requestID]
delete(s.pendingInputs, requestID)
// On control_cancel_request: clean up
delete(s.pendingInputs, requestID)
AskUserQuestion is a Claude Code tool that presents structured questions to
users. It flows through the permission system but needs special handling:
AskUserQuestion in the assistant event handler
to avoid sending a redundant tool_use message to the frontendagent/claudecode/session.go:374)
parses input into structured Questions for rich UI rendering// In parseEvent, assistant/tool_use:
case "tool_use":
toolName, _ := block["name"].(string)
if toolName == "AskUserQuestion" {
continue // Skip — this goes through control_request path
}
When modifying webuiSession (core/webui.go), always cross-check with
claudeSession (agent/claudecode/session.go) for consistency:
| Feature | claudeSession | webuiSession | Notes |
|---------|--------------|--------------|-------|
| --permission-prompt-tool stdio | Yes | Yes | CRITICAL for permission flow |
| --output-format stream-json | Yes | Yes | |
| --input-format stream-json | Yes | Yes | |
| --verbose | Yes (configurable) | Yes | |
| control_request field: event["request"] | Yes | Yes | NOT event["tool"] |
| control_request subtype check | Yes | Yes | Must be can_use_tool |
| respondPermission with updatedInput | Yes | Yes | Required for allow |
| control_cancel_request handling | Yes (log only) | Yes (forward to frontend) | |
| AskUserQuestion filter in tool_use | Yes | Yes | |
| --permission-mode | Yes | No | WebUI always manual |
| --resume / --continue | Yes | No | WebUI fresh sessions only |
| --append-system-prompt | Yes | No | WebUI no system prompt injection |
| --allowedTools / --disallowedTools | Yes | No | WebUI no tool restrictions |
| stderr capture | Yes (Buffer) | No (nil) | WebUI loses error details |
| Auto-approve modes | Yes | No | WebUI always asks user |
Symptoms: Claude Code runs but never asks for permission in the WebUI. The AI's text output may reference needing permission but no dialog shows.
Checklist:
--permission-prompt-tool stdio in the CLI args? (Root cause of original bug)parseEvent reading event["request"] (not event["tool"])? (Second-layer bug)permission_request message type?permission_request messagesSymptoms: User clicks Allow but Claude Code seems stuck.
Checklist:
respondPermission sending updatedInput with the original tool input?control_response format correct? (nested response.response)Symptoms: Permission buttons remain clickable even though CLI moved on.
Checklist:
control_cancel_request handled in parseEvent?permission_cancelled WebSocket message?pendingInputs cleaned up on cancel?# Enable debug logging to see all events
# In Go: slog.Debug messages show event parsing
# In browser: check WebSocket frames in DevTools → Network → WS
# Key log messages:
# "webui: unknown control request subtype" — non-permission control_request received
# "webui: permission cancelled" — control_cancel_request processed
| File | Role |
|------|------|
| core/webui.go | webuiSession.start() (CLI args), parseEvent() (event parsing), respondPermission() (response formatting), pendingInputs cache |
| agent/claudecode/session.go | Reference implementation: handleControlRequest(), RespondPermission() — the authoritative format |
| web/src/pages/VibeCoding/VibeSession.tsx | Frontend: handleServerMessage case permission_request / permission_cancelled, allowPermission(), denyPermission() |
| web/src/pages/VibeCoding/types.ts | ChatMessage.type includes permission_request |
| web/src/i18n/locales/*.json | vibe.permissionRequest, vibe.allow, vibe.deny, vibe.allowed, vibe.denied, vibe.cancelled |
webui-vibe-coding — Overall WebUI architecture, WebSocket protocol, session lifecyclemessage-flow-architecture — IM path permission flow (Engine event loop, pendingPermission channel)webui-attachment-upload — Another WebSocket feature with full-stack data flow referencetools
This skill should be used when the user asks about "webui", "web ui", "vibe coding", "WebUIServer", "web interface", "browser Claude Code", "web frontend", "React admin dashboard", "web/src", "VibeCoding page", "WebSocket /api/vibe/ws", "webui config", "port 9830", "static file serving", "webuiSession", "claude code subprocess from web", "project dropdown", "work dir select", "listProjects", "Management API frontend", "multi tab vibe", "TabBar", "VibeSession component", "copy work dir", "clipboard copy", "disconnect confirm", "断开确认", "复制路径", "copyWorkDir", "AgentSystemPrompt", "project awareness", "/project command in agent", "attachment upload", "sendWithAttachments", "file upload vibe", "image upload vibe", "drag drop vibe", "paste image vibe", or needs to debug, extend, or understand the browser-based Vibe Coding interface and its Go backend.
tools
This skill should be used when the user asks about "export markdown", "export chat", "download markdown", "export conversation", "导出 Markdown", "导出聊天记录", "ExportRequest", "ExportMessage", "handleVibeExportMarkdown", "buildExportMarkdown", "POST /api/vibe/export", "exportVibeSession", "chat export", "markdown download", "sanitizeFilename", "Content-Disposition attachment", "export button vibe", "history export", "tool_use in export", "message type mapping export", or needs to debug, extend, or understand the chat history Markdown export feature including the full-stack data flow from frontend trigger to file download.
tools
This skill should be used when the user asks about "attachment upload", "file upload", "image upload", "paste image", "drag drop file", "vibe attachment", "pendingAttachments", "sendWithAttachments", "fileToAttachment", "AttachmentItem", "base64 attachment", "WebSocket attachment protocol", "multimodal content", "image content block", "file content block", "clipboard paste image", "drag and drop upload", "attachment preview", "file size limit", "10MB limit", "ExtFromMime", "附件上传", "图片上传", "拖拽上传", "粘贴图片", "附件预览", "文件大小限制", or needs to debug, extend, or understand the WebUI attachment upload feature including the full-stack data flow from browser to Claude Code CLI.
development
This skill should be used when the user asks about "vibe history", "chat history persistence", "vibe chat database", "ChatStore biz_type", "cc_sessions biz_type", "cc_chat_messages biz_type", "vibe MySQL", "vibe session save", "/api/vibe/sessions", "VibeHistory component", "handleVibeSessions", "handleVibeSessionMessages", "ListSessions", "GetMessages", "chat history sidebar", "load history session", "continue conversation from history", "vibe chatstore integration", "webuiSession chatStore", "chatStore is nil", "MySQL DSN format", "mysql driver log", "slogWriter", "chatstore log", "database operation log", "vite proxy /api/vibe", "404 api vibe sessions", "unified history", "IM history in vibe", "listAllSessionsSQL", "biz_type filter", "history includes IM", "history source tag", or needs to debug, extend, or understand how Vibe Coding chat messages are persisted to MySQL and loaded as browsable history in the frontend.