diff --git a/ClaudeRunner.cs b/ClaudeRunner.cs
index 4706884..2ab3de1 100644
--- a/ClaudeRunner.cs
+++ b/ClaudeRunner.cs
@@ -220,6 +220,8 @@ namespace QuickClaude
Finished?.Invoke(error);
}
+ // File writes also go through a guard hook ("QuickClaude.exe --guard-write") that blocks the
+ // Desktop and AppData, where OpenRules lets things open without asking.
// Shell commands go through QuickClaude's own confirm box: a PreToolUse hook that runs
// "QuickClaude.exe --confirm" (exec form, so no shell quoting). The dialog denies after
// 2 minutes; the hook timeout is a bit longer so Claude Code never cuts it off first.
@@ -237,6 +239,12 @@ namespace QuickClaude
{
matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands
hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } }
+ },
+ new
+ {
+ // Never lets Claude write to the Desktop or AppData (see WriteGuard)
+ matcher = "Write|Edit|MultiEdit|NotebookEdit",
+ hooks = new[] { new { type = "command", command = exe, args = new[] { "--guard-write" }, timeout = 30 } }
}
}
}
diff --git a/Program.cs b/Program.cs
index 3db8703..bfbf20f 100644
--- a/Program.cs
+++ b/Program.cs
@@ -12,6 +12,8 @@ namespace QuickClaude
{
// Run as Claude Code's shell-command confirm hook (separate short-lived process)
if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run();
+ // Run as Claude Code's file-write guard hook (see WriteGuard)
+ if (Array.IndexOf(args, "--guard-write") >= 0) return WriteGuard.Run();
using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first);
if (!first) return 0;
diff --git a/QuickClaude.csproj b/QuickClaude.csproj
index 36f631c..961f2a1 100644
--- a/QuickClaude.csproj
+++ b/QuickClaude.csproj
@@ -10,7 +10,7 @@
disable
QuickClaude
QuickClaude
- 1.3.0
+ 1.4.0
QuickClaude
app.ico
win-x64
diff --git a/WriteGuard.cs b/WriteGuard.cs
new file mode 100644
index 0000000..e41fbbf
--- /dev/null
+++ b/WriteGuard.cs
@@ -0,0 +1,81 @@
+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;
+ }
+}