iOS remote control: permission prompt buttons prone to mis-taps, no correction mechanism

Resolved 💬 3 comments Opened Feb 25, 2026 by dk0hn Closed Mar 26, 2026

Problem

When approving tool permissions via the iOS Claude app (remote control), the vertically stacked buttons are narrow with minimal spacing between them.

The most critical adjacency problem: "Allow for session" sits directly above "Deny" — these are opposite actions with the highest consequence difference. A mis-tap denies the agent's work entirely with no way to correct it.

By contrast, mis-tapping between "Allow once" and "Allow for session" is low-consequence — you just get prompted again.

There is currently no UI element to change a permission decision after it's been made.

Proposed Solutions

A) Reorder and group by intent — separate destructive action
import SwiftUI

struct PermissionPromptView: View {
    let toolName: String
    let onAllowOnce: () -> Void
    let onAllowSession: () -> Void
    let onDeny: () -> Void
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Allow \(toolName)?")
                .font(.headline)
            
            // Affirmative actions grouped horizontally
            HStack(spacing: 12) {
                Button(action: onAllowOnce) {
                    Text("Allow Once")
                        .frame(maxWidth: .infinity, minHeight: 44)
                }
                .buttonStyle(.bordered)
                
                Button(action: onAllowSession) {
                    Text("Allow for Session")
                        .frame(maxWidth: .infinity, minHeight: 44)
                }
                .buttonStyle(.borderedProminent)
            }
            
            // Deny isolated with distinct styling and spacing
            Button(action: onDeny) {
                Text("Deny")
                    .frame(maxWidth: .infinity, minHeight: 44)
            }
            .buttonStyle(.bordered)
            .tint(.red)
        }
        .padding()
    }
}

// MARK: - Preview

struct PermissionPromptView_Previews: PreviewProvider {
    static var previews: some View {
        PermissionPromptView(
            toolName: "Bash(git status)",
            onAllowOnce: {},
            onAllowSession: {},
            onDeny: {}
        )
        .previewLayout(.sizeThatFits)
        
        // Test on smallest iPhone screen
        PermissionPromptView(
            toolName: "Bash(npm run build)",
            onAllowOnce: {},
            onAllowSession: {},
            onDeny: {}
        )
        .previewDevice("iPhone SE (3rd generation)")
    }
}
B) Post-decision correction UI

There is currently no UI element after a permission choice is made. This proposes adding one — a tappable permission status badge near the tool call output that allows changing the decision:

import SwiftUI

struct PermissionBadgeView: View {
    @State private var showingOptions = false
    @State var currentPermission: PermissionLevel
    let onPermissionChanged: (PermissionLevel) -> Void
    
    enum PermissionLevel: String, CaseIterable {
        case allowedOnce = "Allowed once"
        case allowedForSession = "Allowed for session"
        case denied = "Denied"
        
        var icon: String {
            switch self {
            case .allowedOnce: return "checkmark.circle"
            case .allowedForSession: return "checkmark.circle.fill"
            case .denied: return "xmark.circle.fill"
            }
        }
        
        var tint: Color {
            switch self {
            case .allowedOnce: return .green
            case .allowedForSession: return .blue
            case .denied: return .red
            }
        }
    }
    
    var body: some View {
        Button {
            showingOptions = true
        } label: {
            HStack(spacing: 4) {
                Image(systemName: currentPermission.icon)
                Text(currentPermission.rawValue)
                    .font(.caption)
                Image(systemName: "chevron.down")
                    .font(.caption2)
            }
            .foregroundColor(currentPermission.tint)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .background(currentPermission.tint.opacity(0.1))
            .cornerRadius(6)
        }
        .confirmationDialog(
            "Change permission",
            isPresented: $showingOptions,
            titleVisibility: .visible
        ) {
            ForEach(PermissionLevel.allCases, id: \.self) { level in
                if level != currentPermission {
                    Button(level.rawValue) {
                        currentPermission = level
                        onPermissionChanged(level)
                    }
                }
            }
        }
    }
}

// MARK: - Tests (Preview-based)

#if DEBUG
struct PermissionBadgeView_Previews: PreviewProvider {
    static var previews: some View {
        VStack(spacing: 16) {
            PermissionBadgeView(
                currentPermission: .allowedOnce,
                onPermissionChanged: { _ in }
            )
            PermissionBadgeView(
                currentPermission: .allowedForSession,
                onPermissionChanged: { _ in }
            )
            PermissionBadgeView(
                currentPermission: .denied,
                onPermissionChanged: { _ in }
            )
        }
        .padding()
        .previewLayout(.sizeThatFits)
    }
}
#endif
C) Unit tests
import XCTest
@testable import ClaudeCode

final class PermissionPromptTests: XCTestCase {
    
    func testButtonMinimumTouchTarget() {
        // Apple HIG: minimum 44pt touch targets
        let minTouchTarget: CGFloat = 44
        // All interactive elements must meet 44pt minimum height
        // Implementation should enforce .frame(minHeight: 44) on all buttons
    }
    
    func testDenyButtonIsSeparatedFromAllowButtons() {
        // Deny must not be adjacent to Allow for Session
        // Layout: [Allow Once | Allow for Session] then gap then [Deny]
        // The VStack spacing between allow group and deny (20pt)
        // should exceed HStack spacing between allow buttons (12pt)
    }
    
    func testPermissionBadgeChangesLevel() {
        var result: PermissionBadgeView.PermissionLevel?
        let badge = PermissionBadgeView(
            currentPermission: .allowedOnce,
            onPermissionChanged: { result = $0 }
        )
        
        badge.onPermissionChanged(.allowedForSession)
        XCTAssertEqual(result, .allowedForSession)
    }
    
    func testAllPermissionLevelsDisplayCorrectIcon() {
        for level in PermissionBadgeView.PermissionLevel.allCases {
            XCTAssertFalse(level.icon.isEmpty)
            XCTAssertFalse(level.rawValue.isEmpty)
        }
    }
}

Summary

| Issue | Impact | Fix |
|---|---|---|
| Deny adjacent to Allow for Session | Opposite actions, highest mis-tap cost | Reorder: group allows horizontally, isolate deny below |
| Buttons below 44pt HIG minimum | Hard to tap accurately on phone | Enforce minHeight: 44 on all buttons |
| No post-decision correction | Mis-taps are permanent | New tappable permission badge with change dialog |

Environment

  • Claude Code 2.1.56
  • iOS Claude app via remote control
  • iPhone standard screen size

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗