Files
QuickClaude/WriteGuard.cs
T

82 lines
3.8 KiB
C#

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;
}
}