using System; using System.IO; using System.Linq; using System.Text; using System.Text.Json; namespace QuickClaude { // QuickClaude.exe --guard-write: Claude Code's PreToolUse hook for Write, Edit and NotebookEdit. // Keeps Claude from writing anywhere OpenRules lets things open without a confirm box that a // normal (non-admin) account can write to: your Desktop and all of AppData (which holds your // own Start Menu, AppData\Local\Programs and the Store app aliases). Without this, Claude in // "Edit files" or "Auto" mode could drop a shortcut or program there and then open it unasked. // Every other folder is left to the "Claude can" setting as before. public static class WriteGuard { public static int Run() { string path = 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; cwd = Str(root, "cwd"); if (root.TryGetProperty("tool_input", out var input)) path = Str(input, "file_path") ?? Str(input, "notebook_path") ?? Str(input, "path"); } catch { } if (string.IsNullOrEmpty(path)) return 0; // nothing to check: normal rules decide if (!Blocked(path, cwd)) return 0; var json = JsonSerializer.Serialize(new { hookSpecificOutput = new { hookEventName = "PreToolUse", permissionDecision = "deny", permissionDecisionReason = "QuickClaude doesn't let Claude write to the Desktop or AppData folders (programs and shortcuts there " + "can open without a confirm box). Tell the user, and suggest a different folder if one would do." } }); using var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)); stdout.Write(json); return 0; } static bool Blocked(string path, string cwd) { try { var full = Path.IsPathRooted(path) || string.IsNullOrEmpty(cwd) ? Path.GetFullPath(path) : Path.GetFullPath(Path.Combine(cwd, path)); var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var dirs = new[] { Path.Combine(home, "AppData"), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Microsoft", "Windows", "Start Menu"), }.Where(d => !string.IsNullOrEmpty(d)); return dirs.Any(d => { var root = Path.GetFullPath(d).TrimEnd('\\'); return full.Equals(root, StringComparison.OrdinalIgnoreCase) || full.StartsWith(root + "\\", StringComparison.OrdinalIgnoreCase); }); } catch { return true; } // a path we can't even parse doesn't get written } static string Str(JsonElement e, string name) => e.ValueKind == JsonValueKind.Object && e.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; } }