using System; using System.IO; using System.Text; using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace QuickClaude { // QuickClaude.exe --confirm: Claude Code's PreToolUse hook for shell commands. // Reads the hook JSON from stdin, asks the user, prints an allow/deny decision. public static class ConfirmHook { public static int Run() { string toolName = "Bash", command = null, cwd = null; try { using var stdin = new StreamReader(Console.OpenStandardInput(), new UTF8Encoding(false)); using var doc = JsonDocument.Parse(stdin.ReadToEnd()); var root = doc.RootElement; toolName = Str(root, "tool_name") ?? toolName; cwd = Str(root, "cwd"); if (root.TryGetProperty("tool_input", out var input)) command = Str(input, "command"); } catch { } // Nothing to show: say nothing, so Claude Code's normal permission rules decide if (string.IsNullOrEmpty(command)) return 0; new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; var win = new ConfirmWindow(toolName, command, cwd); win.ShowDialog(); string decision = win.Allowed ? "allow" : "deny"; string reason = win.Allowed ? "The user allowed this command." : win.TimedOut ? "The user didn't answer within 2 minutes, so the command was not run. Carry on without it." : "The user declined to run this command. Carry on without it."; var json = JsonSerializer.Serialize(new { hookSpecificOutput = new { hookEventName = "PreToolUse", permissionDecision = decision, permissionDecisionReason = reason } }); using var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)); stdout.Write(json); return 0; } static string Str(JsonElement e, string name) => e.ValueKind == JsonValueKind.Object && e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; } public class ConfirmWindow : Window { public bool Allowed { get; private set; } public bool TimedOut { get; private set; } static readonly TimeSpan Limit = TimeSpan.FromMinutes(2); internal static readonly Brush Text = Brushes.White; internal static readonly Brush Faint = new SolidColorBrush(Color.FromArgb(140, 255, 255, 255)); internal static readonly Brush Panel = new SolidColorBrush(Color.FromArgb(225, 20, 20, 20)); internal static readonly Brush Edge = new SolidColorBrush(Color.FromArgb(70, 255, 255, 255)); internal static readonly Brush EdgeBright = new SolidColorBrush(Color.FromArgb(200, 255, 255, 255)); internal static readonly Brush Hover = new SolidColorBrush(Color.FromArgb(40, 255, 255, 255)); internal static readonly Brush Press = new SolidColorBrush(Color.FromArgb(70, 255, 255, 255)); readonly DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; readonly DateTime deadline = DateTime.UtcNow + Limit; readonly TextBlock countdown = new TextBlock(); public ConfirmWindow(string toolName, string command, string cwd) { Title = "QuickClaude - run command?"; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = Brushes.Transparent; ResizeMode = ResizeMode.NoResize; Topmost = true; ShowInTaskbar = true; Width = 520; SizeToContent = SizeToContent.Height; WindowStartupLocation = WindowStartupLocation.CenterScreen; FontFamily = MainWindow.Mono; FontSize = MainWindow.TextSize; Foreground = Text; var header = new TextBlock { Text = "clod wants to run " + toolName + (string.IsNullOrEmpty(cwd) ? "" : " in " + cwd), Foreground = Faint, TextWrapping = TextWrapping.Wrap }; // The exact command, selectable, scrolls if it's very long var cmd = new TextBox { Text = command, IsReadOnly = true, Background = Brushes.Transparent, Foreground = Text, CaretBrush = Text, BorderThickness = new Thickness(0), Padding = new Thickness(0), FocusVisualStyle = null, FontFamily = MainWindow.Mono, FontSize = MainWindow.TextSize, TextWrapping = TextWrapping.Wrap, MaxHeight = 320, Margin = new Thickness(0, 8, 0, 8), Template = MainWindow.BareTextBoxTemplate, IsUndoEnabled = false }; countdown.Foreground = Faint; countdown.VerticalAlignment = VerticalAlignment.Center; UpdateCountdown(); var deny = MakeButton("Deny"); var allow = MakeButton("Allow"); deny.Click += (s, e) => Finish(false); allow.Click += (s, e) => Finish(true); allow.Margin = new Thickness(8, 0, 0, 0); var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; buttons.Children.Add(deny); buttons.Children.Add(allow); var bottom = new Grid(); bottom.Children.Add(countdown); bottom.Children.Add(buttons); var stack = new StackPanel(); stack.Children.Add(header); stack.Children.Add(cmd); stack.Children.Add(bottom); var frame = new Border { Background = Panel, BorderBrush = Edge, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(0), Padding = new Thickness(12), Child = stack }; frame.MouseLeftButtonDown += (s, e) => { try { DragMove(); } catch { } }; Content = frame; PreviewKeyDown += (s, e) => { if (e.Key == Key.Escape) { e.Handled = true; Finish(false); } }; timer.Tick += (s, e) => { if (DateTime.UtcNow >= deadline) { TimedOut = true; Finish(false); } else UpdateCountdown(); }; Loaded += (s, e) => { timer.Start(); Activate(); deny.Focus(); }; Closed += (s, e) => timer.Stop(); } void UpdateCountdown() { var left = deadline - DateTime.UtcNow; if (left < TimeSpan.Zero) left = TimeSpan.Zero; countdown.Text = $"denies in {(int)left.TotalMinutes}:{left.Seconds:00}"; } void Finish(bool allowed) { Allowed = allowed; timer.Stop(); Close(); } static Button MakeButton(string text) => new Button { Content = text, Foreground = Text, Background = Brushes.Transparent, BorderBrush = Edge, Padding = new Thickness(14, 4, 14, 4), FocusVisualStyle = null, Cursor = Cursors.Hand, Template = ButtonTemplate }; // Square, flat button: brighter edge when focused, light fill on hover. No theme chrome. static readonly ControlTemplate ButtonTemplate = MakeButtonTemplate(); static ControlTemplate MakeButtonTemplate() { var bd = new FrameworkElementFactory(typeof(Border), "bd"); bd.SetValue(Border.BorderThicknessProperty, new Thickness(1)); bd.SetValue(Border.BorderBrushProperty, new TemplateBindingExtension(Control.BorderBrushProperty)); bd.SetValue(Border.BackgroundProperty, new TemplateBindingExtension(Control.BackgroundProperty)); bd.SetValue(Border.PaddingProperty, new TemplateBindingExtension(Control.PaddingProperty)); var cp = new FrameworkElementFactory(typeof(ContentPresenter)); cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center); cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center); bd.AppendChild(cp); var t = new ControlTemplate(typeof(Button)) { VisualTree = bd }; var hover = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true }; hover.Setters.Add(new Setter(Border.BackgroundProperty, Hover, "bd")); var pressed = new Trigger { Property = System.Windows.Controls.Primitives.ButtonBase.IsPressedProperty, Value = true }; pressed.Setters.Add(new Setter(Border.BackgroundProperty, Press, "bd")); var focused = new Trigger { Property = UIElement.IsKeyboardFocusedProperty, Value = true }; focused.Setters.Add(new Setter(Border.BorderBrushProperty, EdgeBright, "bd")); t.Triggers.Add(hover); t.Triggers.Add(pressed); t.Triggers.Add(focused); t.Seal(); return t; } } }