1 Commits
Author SHA1 Message Date
weishauptandClaude Opus 5.5 14763a002a v1.4: Block Claude from writing to the Desktop and AppData
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:24:08 -04:00
4 changed files with 92 additions and 1 deletions
+8
View File
@@ -220,6 +220,8 @@ namespace QuickClaude
Finished?.Invoke(error); 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 // 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 // "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. // 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 matcher = "Bash|PowerShell|Monitor", // Monitor also runs shell commands
hooks = new[] { new { type = "command", command = exe, args = new[] { "--confirm" }, timeout = 150 } } 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 } }
} }
} }
} }
+2
View File
@@ -12,6 +12,8 @@ namespace QuickClaude
{ {
// Run as Claude Code's shell-command confirm hook (separate short-lived process) // Run as Claude Code's shell-command confirm hook (separate short-lived process)
if (Array.IndexOf(args, "--confirm") >= 0) return ConfirmHook.Run(); 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); using var mutex = new Mutex(true, "QuickClaude.SingleInstance", out bool first);
if (!first) return 0; if (!first) return 0;
+1 -1
View File
@@ -10,7 +10,7 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<AssemblyName>QuickClaude</AssemblyName> <AssemblyName>QuickClaude</AssemblyName>
<RootNamespace>QuickClaude</RootNamespace> <RootNamespace>QuickClaude</RootNamespace>
<Version>1.3.0</Version> <Version>1.4.0</Version>
<Product>QuickClaude</Product> <Product>QuickClaude</Product>
<ApplicationIcon>app.ico</ApplicationIcon> <ApplicationIcon>app.ico</ApplicationIcon>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+81
View File
@@ -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;
}
}