Synercoding.ClaudeApprover
1.0.0-alpha012
dotnet add package Synercoding.ClaudeApprover --version 1.0.0-alpha012
NuGet\Install-Package Synercoding.ClaudeApprover -Version 1.0.0-alpha012
<PackageReference Include="Synercoding.ClaudeApprover" Version="1.0.0-alpha012" />
<PackageVersion Include="Synercoding.ClaudeApprover" Version="1.0.0-alpha012" />
<PackageReference Include="Synercoding.ClaudeApprover" />
paket add Synercoding.ClaudeApprover --version 1.0.0-alpha012
#r "nuget: Synercoding.ClaudeApprover, 1.0.0-alpha012"
#:package Synercoding.ClaudeApprover@1.0.0-alpha012
#addin nuget:?package=Synercoding.ClaudeApprover&version=1.0.0-alpha012&prerelease
#tool nuget:?package=Synercoding.ClaudeApprover&version=1.0.0-alpha012&prerelease
Synercoding.ClaudeApprover
A .NET library for building Claude Code PreToolUse hook approver scripts. It provides a polymorphic approval system where you subclass BaseApprover and override per-tool handler methods to allow, deny, or ask for each tool invocation. Communication with Claude Code happens via JSON over stdin/stdout.
How It Works
Claude Code supports hooks that run before a tool is executed. This library lets you write a .NET script that acts as a PreToolUse hook, giving you programmatic control over which operations Claude Code is allowed to perform.
The core approval flow:
- Claude Code sends a
ToolInputJSON object via stdin - The library deserializes it into a typed
IToolInput(e.g.BashInput,EditInput,WriteInput) BaseApprover.Handle()dispatches to the appropriate per-tool virtual method- Your handler returns a
PreToolUseOutputwith aPermissionDecision(Allow,Deny, orAsk) and an optional reason - The output is serialized back to Claude Code via stdout
Getting Started
The approver script uses dotnet run with C# file-based programs, which requires the .NET 10 SDK or later installed on the machine running the hook.
Install the NuGet package:
dotnet add package Synercoding.ClaudeApprover
The library targets net10.0.
Usage
1. Create an approver script
Create a C# file (e.g. .claude/hooks/Approver.cs) that uses the library. You can use the built-in InsideProjectAllowedApprover which restricts all file and bash operations to within the project root, or subclass BaseApprover for full control.
A working example is available at .claude/hooks/Approver.cs:
#:sdk Microsoft.NET.Sdk
#:package Synercoding.ClaudeApprover@*
using Synercoding.ClaudeApprover;
using Synercoding.ClaudeApprover.Converters;
using Synercoding.ClaudeApprover.Input;
using Synercoding.ClaudeApprover.Output;
using System.Text.Json;
var (json, input) = InputProcessor.Process(Console.OpenStandardInput());
if (input is null)
{
Console.Error.WriteLine("Input could not be parsed...");
return 1;
}
var approver = new InsideProjectAllowedApprover();
var output = approver.Handle(input);
if (output is not null)
{
var outputJson = JsonSerializer.Serialize(output, ToolOutputJsonContext.Default.PreToolUseOutput);
Console.WriteLine(outputJson);
}
return 0;
2. Configure the hook
Create a .claude/hooks/run-approver.cjs wrapper script that invokes the approver via dotnet run:
const { execFileSync } = require("child_process");
const { join } = require("path");
const workDir = join(process.env.CLAUDE_PROJECT_DIR, ".claude", "hooks");
try {
execFileSync("dotnet", ["run", "-v", "q", "./Approver.cs"], {
cwd: workDir,
stdio: "inherit",
});
} catch (err) {
process.exit(err.status ?? 1);
}
Then add a PreToolUse hook in your .claude/settings.json that runs the wrapper:
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node -e \"require(require('path').join(process.env.CLAUDE_PROJECT_DIR,'.claude','hooks','run-approver.cjs'))\""
}
]
}
]
}
}
The Node.js wrapper ensures the hook works correctly cross-platform. Windows and Linux have different syntax for referencing environment variables in shell commands (%VAR% vs $VAR), which is needed to resolve the hook script path via CLAUDE_PROJECT_DIR. Using a Node.js wrapper with process.env and path.join avoids this issue entirely. The matcher is set to * so the hook runs for every tool invocation. The -v q flag suppresses MSBuild output so only the JSON response is written to stdout.
If you only use the hook in a .claude/settings.local.json (which is not checked into source control and thus platform-specific), you can skip the wrapper and invoke dotnet run directly:
On Linux / macOS:
"command": "cd \"$CLAUDE_PROJECT_DIR\"/.claude/hooks && dotnet run -v q ./Approver.cs"
On Windows:
"command": "cd /d \"%CLAUDE_PROJECT_DIR%\\.claude\\hooks\" && dotnet run -v q Approver.cs"
3. Customize approval logic
To implement custom rules, subclass BaseApprover and override the handler methods you need:
public class MyApprover : BaseApprover
{
public override PreToolUseOutput? Handle(ToolInput input, WriteInput write)
{
if (write.FilePath.EndsWith(".csproj"))
return Deny("Modifying project files is not allowed.");
return Allow();
}
public override PreToolUseOutput? Handle(ToolInput input, BashInput bash)
{
return Ask(); // Always ask the user for bash commands
}
}
Each handler can return:
Allow()- permit the operationDeny(reason)- block the operation with an explanationAsk(reason)- prompt the user for confirmationnull- no opinion (the default)
Built-in Approvers
InsideProjectAllowedApprover
The included InsideProjectAllowedApprover enforces that all file operations (read, edit, write), bash commands, and PowerShell commands stay within the project root directory. It also prevents access to .git directories and ships with bash and PowerShell command parsers, each with per-command approval.
Three dictionaries control per-command approval:
BashApprovers— bash-shell-specific commands (e.g.cd,rm,sed). Case-sensitive.PowerShellApprovers— PowerShell-specific cmdlets and aliases (e.g.Set-Location,Remove-Item,Out-File). Case-insensitive.ExecutableApprovers— executables shared between bash and PowerShell (e.g.dotnet,git,node). Case-insensitive. Consulted when no shell-specific entry matches, so a single registration applies to both shells.
Any command not found in either the shell-specific list or ExecutableApprovers will default to Ask. Shell-specific entries win when the same key exists in both. For example:
var approver = new InsideProjectAllowedApprover();
// Allowed from both bash and PowerShell with one registration:
approver.ExecutableApprovers["dotnet"] = InsideProjectAllowedApprover.AllowCommand;
approver.ExecutableApprovers["git"] = InsideProjectAllowedApprover.AllowCommand;
Additional Directories
By default, all file and bash operations are restricted to the project root. You can allow access to additional directories (e.g. sibling projects) using the AdditionalDirectories property. Paths can be absolute, relative to the project root, or use ~/ to reference the user's home directory:
var approver = new InsideProjectAllowedApprover();
approver.AdditionalDirectories.Add("../sister-project");
approver.AdditionalDirectories.Add("/opt/shared-libs");
approver.AdditionalDirectories.Add("~/.claude"); // allow access to Claude profile directory
The approver can also import additional directories from Claude's own settings files. When ImportAdditionalDirsFromClaude is true (the default), it reads the additionalDirectories JSON array from .claude/settings.json and .claude/settings.local.json:
{
"additionalDirectories": ["../sister-project", "../shared-utils"]
}
To disable importing from Claude settings:
approver.ImportAdditionalDirsFromClaude = false;
MCP Approvers
You can also add custom logic for specific mcp servers:
var approver = new InsideProjectAllowedApprover();
approver.McpApprovers["playwright"] = (tool, input) => BaseApprover.Allow();
Project Root Detection
BaseApprover resolves the project root by checking (in order):
CLAUDE_PROJECT_DIRenvironment variable- Directory containing a
.claudefolder - Directory containing a
.slnor.slnxfile
Build & Test
dotnet build
dotnet test
License
See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-alpha012 | 54 | 5/21/2026 |
| 1.0.0-alpha011 | 65 | 4/10/2026 |
| 1.0.0-alpha010 | 79 | 3/30/2026 |
| 1.0.0-alpha009 | 73 | 3/27/2026 |
| 1.0.0-alpha008 | 56 | 3/26/2026 |
| 1.0.0-alpha007 | 54 | 3/26/2026 |
| 1.0.0-alpha006 | 61 | 3/24/2026 |
| 1.0.0-alpha005 | 56 | 3/24/2026 |
| 1.0.0-alpha004 | 65 | 3/23/2026 |
| 1.0.0-alpha003 | 72 | 3/17/2026 |
| 1.0.0-alpha002 | 78 | 2/2/2026 |
| 1.0.0-alpha001 | 72 | 2/1/2026 |
- Added PowerShell tool approver support: PowerShellInput is dispatched through a new PowerShell command parser, with default approvers for ~30 safe read-only cmdlets and path-validating handlers for Set-Location, Remove-Item, Copy-Item, Move-Item, New-Item, Rename-Item, and content-writing cmdlets (Out-File, Set-Content, Add-Content, Clear-Content) plus their common aliases.
- Unified the duplicated Command, CommandInfo, CommandPermission, Pipeline, and Redirection types into a new Synercoding.ClaudeApprover.Shells namespace shared between bash and PowerShell. Both parsers moved into Synercoding.ClaudeApprover.Shells.Parsers and were renamed to BashParser and PowerShellParser respectively.
- Added a new ExecutableApprovers dictionary (case-insensitive) for executables shared between bash and PowerShell (e.g. dotnet, git, node). Registering an executable once now applies to both shells. Shell-specific entries in BashApprovers / PowerShellApprovers still take precedence.
- BREAKING: CommandApprovers has been renamed to BashApprovers. The PowerShellApprover delegate has been removed; the existing CommandApprover delegate now serves all three approver dictionaries. The PowerShellAllowCommand and PowerShellAskCommand static helpers have been removed in favour of the existing AllowCommand and a new generic AskCommand.