Skip to main content

Claude Code integration

AI.Sentinel.ClaudeCode.Cli is a native hook adapter for Anthropic's Claude Code CLI. It runs the AI.Sentinel detector pipeline against every prompt, tool call, and tool result without modifying your application — Claude Code invokes the hook via stdin/stdout per the Claude Code hooks spec.

Install​

dotnet tool install -g AI.Sentinel.ClaudeCode.Cli

The CLI installs as sentinel-hook on your PATH, with no runtime configuration in your project. Installing needs the .NET 10 SDK. On linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64 (Alpine), osx-arm64 and win-x64, dotnet tool install picks the Native AOT build, which starts in a few milliseconds instead of paying ~190 ms of JIT startup on every hook call. Other platforms get a framework-dependent build that runs on the .NET 10 runtime.

A machine with an SDK older than .NET 10 can no longer install the tool. Install the .NET 10 SDK, or use the release binary, which needs no .NET at all.

Wire it into Claude Code​

Edit ~/.claude/settings.json (user-level) or .claude/settings.json (project-level):

{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "sentinel-hook user-prompt-submit" }] }
],
"PreToolUse": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "sentinel-hook pre-tool-use" }] }
],
"PostToolUse": [
{ "matcher": "*", "hooks": [{ "type": "command", "command": "sentinel-hook post-tool-use" }] }
]
}
}

Three hook points:

HookWhen it firesWhat gets scanned
UserPromptSubmitEvery user messageThe prompt text
PreToolUseBefore any tool callTool name + arguments
PostToolUseAfter every tool callTool result text

Restart Claude Code to pick up the new settings (or re-source the workspace).

Severity → action​

Both Claude Code and Copilot share the same env-var contract:

VariableDefaultValues
SENTINEL_HOOK_ON_CRITICALBlockBlock / Warn / Allow
SENTINEL_HOOK_ON_HIGHBlockBlock / Warn / Allow
SENTINEL_HOOK_ON_MEDIUMWarnBlock / Warn / Allow
SENTINEL_HOOK_ON_LOWAllowBlock / Warn / Allow
SENTINEL_HOOK_VERBOSEfalse1 / true / yes → grep-friendly stderr diagnostic
SENTINEL_HOOK_SUPPRESS_SEMANTIC_WARNINGfalse1 / true / yes → suppress the startup warning that semantic detection is inert. Set only after accepting that SEC-01 and SEC-05 will not fire
SENTINEL_HOOK_REDACTonoff / 0 / false / no → turn off redaction of tool output and the placeholder guard. Anything else, or unset, keeps them on
SENTINEL_HOOK_COMPLIANCEoffon / 1 / true / yes → run the four Art. 5 compliance detectors, which are off by default. Their findings warn instead of following SENTINEL_HOOK_ON_HIGH. Needs SENTINEL_EMBEDDING_ENDPOINT. Thresholds are code-only
SENTINEL_HOOK_DETECTORSsecuritysecurity → the security preset the MCP proxy uses: 16 security detectors plus the four Art. 5 compliance detectors, which run only with SENTINEL_HOOK_COMPLIANCE=on. all → every detector, including the operational and hallucination detectors that judge a model's reply and fire on tool output, see Detectors. Anything else, or unset, keeps security
SENTINEL_HOOK_MAX_SCAN_BYTES262144A positive integer → the scan budget, SentinelOptions.MaxScanBytes, in UTF-8 bytes. An event over it is blocked as SEC-00 under the default SENTINEL_HOOK_ON_CRITICAL=Block: a large tool result, and a large tool input too, so a PreToolUse for a write of a file over the budget is blocked before the tool runs; raise it to scan more. Anything else, or unset, keeps the default
SENTINEL_EMBEDDING_ENDPOINT—Full embeddings URL, e.g. https://api.openai.com/v1/embeddings or http://localhost:11434/v1/embeddings. Enables semantic detection
SENTINEL_EMBEDDING_MODEL—Model identifier, e.g. text-embedding-3-small or nomic-embed-text
SENTINEL_EMBEDDING_API_KEY—Bearer token. Omit for local servers that need none
SENTINEL_EMBEDDING_DIMENSIONS—Vector length, when the model allows a choice
SENTINEL_EMBEDDING_CACHE_DIRper-user cache dirWhere reference vectors are cached. Must not be world-writable — see below
SENTINEL_REVIEW_ENDPOINT—Full chat completions URL of the second-stage reviewer, e.g. https://api.openai.com/v1/chat/completions or http://localhost:11434/v1/chat/completions. Without a reviewer, semantic detections are capped at Medium and only warn
SENTINEL_REVIEW_MODEL—Model identifier of the reviewer. Required with the endpoint
SENTINEL_REVIEW_API_KEY—Bearer token. Omit for local servers that need none
SENTINEL_REVIEW_MAX_PER_SCAN4Reviews per scan. 0 turns the review off
SENTINEL_REVIEW_TIMEOUT_MS5000Per-review timeout in milliseconds. A hook waits at most this long for the judge; lowering it trades recall on confirmed findings for latency

What the review endpoint receives. SENTINEL_REVIEW_ENDPOINT is sent each flagged text, up to its last 64 KiB. That text can contain the secret or personal data a credential or PII detector flagged, just as the embedding endpoint receives the texts it embeds, so choose a provider you would trust with that data, or a local one. The reviewer's reason comes back the other way: it becomes part of the finding's reason, which the hook's block or warning text passes on to the model. It is sanitised to one line and capped at 200 characters.

Detectors​

The hook scans prompts, tool arguments and tool output, never a model's reply. So by default it runs the same security preset as the MCP proxy: 16 of the 32 security detectors and the four compliance detectors. It leaves out the operational and hallucination detectors, which were written for a model's reply and fire as noise on tool traffic. Build logs and directory listings repeat lines, so OPS-02 flags them. OPS-11 rates a tool output more than 100 times the size of the tool call, or above 50,000 characters, as High, which blocks under the default SENTINEL_HOOK_ON_HIGH=Block. SENTINEL_HOOK_DETECTORS=all runs every detector.

Behaviour change. Until this preset, the hooks ran every detector. The default now also leaves out the 16 security detectors that are not in the preset, including the rule-based SEC-32 SystemPromptEcho, which fires without an embedding endpoint. A hook that relied on any detector outside the preset needs SENTINEL_HOOK_DETECTORS=all.

What each action does​

The hook writes the output the Claude Code hooks spec documents:

  • Block on UserPromptSubmit or PreToolUse → the hook exits 2 with the reason on stderr and writes nothing to stdout. Claude Code stops the prompt or the tool call and shows the reason.
  • Block on PostToolUse → the tool has already run, so the hook exits 0 and withholds its output. stdout carries "decision": "block", the reason, and an updatedToolOutput in place of the tool's output. See Withheld output.
  • Warn → exit 0 with {"systemMessage": "AI.Sentinel: <reason>"} on stdout. Claude Code shows the message to the user.
  • Allow → exit 0 with {} on stdout.

Redaction​

With SENTINEL_HOOK_REDACT on, the default, PostToolUse redacts personal data and secrets in the tool output. The hook returns the redacted output as updatedToolOutput, so the model sees [EMAIL_1_k3f9x2] in place of the address, together with an additionalContext note such as:

AI.Sentinel redacted 1 EMAIL in this tool output. Placeholders such as [EMAIL_n_tag] stand for the real values; do not write them to files or commands.

The redaction is recorded as a REDACTED audit entry with counts only. The hook does not restore placeholders: the model keeps them for the rest of the session.

Not everything can be redacted. A command hook cannot rewrite the prompt on UserPromptSubmit or the tool arguments on PreToolUse, so a PII or credential finding there uses the action of its severity, and its audit summary ends with ; not redacted. See Redaction → Per surface.

Placeholder guard. Because the hook cannot restore a placeholder, writing one into a file or a command would replace the real value. On PreToolUse, the hook therefore denies a Write, Edit, MultiEdit, NotebookEdit or Bash call whose input contains text of the exact placeholder shape, such as [EMAIL_1_k3f9x2], with exit 2 and a fixed reason that asks the model to get the value from the user. Ordinary text of that shape is denied too. SENTINEL_HOOK_REDACT=off disables the guard.

When a tool rejects the output. Claude Code checks updatedToolOutput against the tool's own output shape. Live checks with Claude Code 2.1.296 showed that Read accepts the redacted output, and that Read and Bash accept the withheld output. A built-in tool that rejects it has its original output shown to the model. The additionalContext note, which names the redacted types or the block reason, is then the only mitigation, and it does not hide the value.

Withheld output​

When a PostToolUse scan blocks, the output is withheld, also with SENTINEL_HOOK_REDACT=off. The withheld output keeps the shape of the tool's tool_response, because Claude Code rejects an output of another shape and then shows the original:

  • identifier-like type values, such as "type": "text", are kept;
  • empty strings stay empty;
  • numbers, booleans, nulls and property names are kept;
  • every other string becomes withheld by AI.Sentinel: <reason>.

A first version also replaced "type": "text". Claude Code rejected it and showed the original Read output, which is why type values are kept.

Set the env vars in your shell profile or directly in settings.json:

{
"env": {
"SENTINEL_HOOK_ON_HIGH": "Block",
"SENTINEL_HOOK_ON_MEDIUM": "Warn",
"SENTINEL_HOOK_VERBOSE": "1"
},
"hooks": { /* ... */ }
}

When the hook itself fails​

The hook fails closed on PreToolUse. When it cannot evaluate a tool call, it blocks the call with exit 2 and a reason that starts with Sentinel internal error: on stderr. This covers:

  • a SENTINEL_APPROVAL_CONFIG that cannot be loaded or fails validation: Sentinel internal error: approval config '<path>' could not be loaded: <message>. Without this, one typo in the file would turn off every approval rule;
  • an unexpected exception in the hook, for example an approval database that cannot be opened: Sentinel internal error: <ExceptionType>: <message>;
  • input without a tool_name, while SENTINEL_APPROVAL_CONFIG has approval rules: Sentinel internal error: PreToolUse input has no tool name. Such a call cannot be checked against the rules. Without approval rules it is scanned as before;
  • stdin that is empty or not valid JSON: Sentinel internal error: malformed stdin JSON: <message>, or Sentinel internal error: empty stdin JSON: <message> when stdin is the JSON null.

Tool calls stay blocked for as long as the cause persists. Fix the config file, or unset SENTINEL_APPROVAL_CONFIG to run without approval rules. The block is recorded in the audit file as an AUTHZ-DENY entry with the code internal_error and the policy name sentinel-internal; when the input names no tool, the entry has the tool unknown, and for unreadable stdin also a new session id. If the hook times out or is killed, Claude Code does not treat that as a block: that behaviour belongs to Claude Code, not to Sentinel.

On UserPromptSubmit and PostToolUse the same failures exit 1 with the error on stderr. Claude Code shows it and goes on: exit 1 is not a block. A detector that fails, for example because the embedding endpoint is down, does not block on any event: it is skipped for that scan, reported on stderr, and the other detectors still run.

Diagnostics — did the hook fire?​

SENTINEL_HOOK_VERBOSE=1 produces a stderr line on every invocation, including Allow outcomes:

[sentinel-hook] event=user-prompt-submit decision=Allow session=sess-42
[sentinel-hook] event=user-prompt-submit decision=Block detector=SEC-01 severity=Critical session=sess-42

Useful when wiring the hook for the first time ("did it actually run?") or when a block was expected but didn't happen. Leave off in steady state — Block and Warn already report their reason.

Audit destination​

The hook records to a local SQLite file by default, with no configuration. Every entry goes to ai-sentinel/audit.db under the per-user data folder: ~/.local/share on Linux, ~/Library/Application Support on macOS and %LOCALAPPDATA% on Windows.

  • What is recorded: findings, authorization denials and approval holds. Allowed calls are not recorded.
  • Where it is shared: the Copilot hook and the MCP proxy write to the same file, so one database holds everything your agents did. Several processes can write to it at once. Each entry's Source says which tool wrote it: claude-code, copilot or mcp.
  • Session id: hook entries carry the session id Claude Code passes to the hook.
  • Permissions: on Linux and macOS the directory is created 0700 and the files 0600.
  • No data folder: if the machine has no per-user data folder (for instance HOME is unset), recording is off and the hook reports a problem. Set SENTINEL_HOOK_AUDIT_SQLITE to a path to record anyway.
  • Chain: the entries form a SHA-256 hash chain that continues across invocations. Browse and verify the file with sentinel dashboard.
VariableDefaultValues
SENTINEL_HOOK_AUDIT_SQLITE<data folder>/ai-sentinel/audit.dbA database path, or off (any case) to use the in-memory ring buffer and lose entries when the hook exits
SENTINEL_HOOK_AUDIT_NDJSON—A file path. Adds an NDJSON copy of every entry, next to the database
SENTINEL_HOOK_AUDIT_MAX_MB100Size cap in megabytes. 0 means no cap

Size cap. The cap is enforced when the hook opens the file. Usage is measured in pages in use (pages minus free pages, times page size), not in file length, so it drops as soon as rows are deleted. The oldest entries are deleted in batches of 10% of the rows, at least 100, and one trim stops after at most 8 batches. A file far over the cap can therefore need more than one open to come under it. The hook never runs VACUUM, because that would hold the write lock for a long time, so the file stops growing at about the cap instead of shrinking.

Failures never change a decision. If the audit file cannot be opened or written, the hook still allows, warns or blocks exactly as it would have. On a Block that exits 2 the audit problem is not printed to stderr, because stderr carries the reason Claude Code feeds back to the model, unless SENTINEL_HOOK_VERBOSE is set. Otherwise it is printed with the prefix [sentinel-hook] audit:.

NDJSON copy​

export SENTINEL_HOOK_AUDIT_NDJSON=/var/log/ai-sentinel/claude-code.ndjson

One line per audit entry; ship via Filebeat/Vector. See NDJSON file forwarder for shipping recipes. Several hook processes, and the Copilot hook and MCP proxy, can share one NDJSON path: appends are serialised through a lock file, so no line is lost or interleaved.

Browse the audit file​

dotnet tool install -g AI.Sentinel.Cli
sentinel dashboard

sentinel dashboard [--db <path>] [--port <port>] serves the dashboard over the audit file at http://127.0.0.1:5170/ai-sentinel/ by default. It listens on loopback unless --bind says otherwise, and does not open a browser. The other options (--bind, --approval-config, --trusted-proxy, --user-header, --local-approver) are in the options table. On start it prints Audit chain intact: N entries or Audit chain broken at entry {id}. It refuses a file that does not exist. Without an approval config it opens the file read-only and shows no approvals to act on: it never creates the file, never upgrades an older schema (run a hook or the MCP proxy once to upgrade it), and never appends. With a sqlite approval config (--approval-config, or SENTINEL_APPROVAL_CONFIG) it also lists pending requests and lets an approver approve or deny, and it opens the file writable so each decision is appended to the hash chain; see the CLI dashboard as an approval host. SQLite may create -wal and -shm files next to it. It needs the ASP.NET Core runtime, which the .NET SDK includes. The dashboard has a Source column and a source filter, so you can separate Claude Code, Copilot and MCP entries.

Programmatic​

For custom audit destinations or richer integration, reference the AI.Sentinel.ClaudeCode library directly in a C# host. Register AI.Sentinel and your own IAuditStore in DI, then hand the provider to HookAdapter:

using AI.Sentinel.ClaudeCode;

var adapter = new HookAdapter(provider, HookConfig.FromEnvironment(env));
var input = new HookInput { SessionId = "sess-42", Prompt = "hello" };
HookOutput output = await adapter.HandleAsync(HookEvent.UserPromptSubmit, input, ct);
// output.Decision is Allow, Warn or Block; output.Reason explains a Warn or Block.

provider is an IServiceProvider, env an IReadOnlyDictionary<string, string?> and ct a CancellationToken. HookConfig is optional; without it the defaults apply.

Redaction is off in a HookConfig you build yourself, including the default one, so PostToolUse keeps the 3.0 behaviour: a secret in the tool output gives the severity decision, Block by default. HookConfig.FromEnvironment, as in the example, turns it on, as the CLI does, unless SENTINEL_HOOK_REDACT is off. To opt in on a config of your own, set RedactToolResults = true. On PostToolUse your host must then deliver HookOutput.UpdatedToolOutput instead of the original output: the redacted output on Allow, or on Block the withheld body that keeps the output's shape.

The decision follows the action your SentinelOptions give each detection: Quarantine maps through HookConfig by severity, Alert surfaces as Warn, or as Allow when HookConfig maps that severity to Allow, and Log gives Allow. The hook answers the most restrictive of these per-detection decisions, so a compliance Warn is kept when a Low finding mapped to Allow fires alongside it; the reason names the detection behind the decision, the higher severity on a tie. With a stock AddAISentinel(), a High finding is an Alert and so a Warn; the sentinel-hook CLI sets every severity to Quarantine so that SENTINEL_HOOK_ON_* decides.

Useful when you want a hook that ships entries to your own backend (Splunk, custom REST API, etc.) without going through file forwarders.

Native binary without the .NET SDK​

Every GitHub release carries the Native AOT builds as archives, for hosts that have no .NET SDK: sentinel-hook-<version>-<rid>.tar.gz for linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64 and osx-arm64, and sentinel-hook-<version>-win-x64.zip. The glibc builds need glibc 2.28 or later, which covers every glibc distribution .NET 10 supports, RHEL 8 and SLES 15 included; the linux-musl-* builds are for Alpine. sentinel-copilot-hook and sentinel-mcp ship the same way. Each release also carries a SHA256SUMS file, and every archive has a GitHub build attestation that proves it was built by this repository's release workflow.

version=5.0.0
gh release download "v$version" --repo MarcelRoozekrans/AI.Sentinel \
--pattern "sentinel-hook-$version-linux-x64.tar.gz" --pattern SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing
gh attestation verify "sentinel-hook-$version-linux-x64.tar.gz" --repo MarcelRoozekrans/AI.Sentinel
sudo mkdir -p /opt/sentinel-hook
sudo tar -xzf "sentinel-hook-$version-linux-x64.tar.gz" --no-same-owner -C /opt/sentinel-hook

The archive holds the binary and the native SQLite library it loads from its own folder, so keep them together in their own directory. Point the hook command at the binary's full path instead of sentinel-hook:

{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "/opt/sentinel-hook/sentinel-hook user-prompt-submit" }] }
]
}
}

The binaries are not code-signed. gh release download and curl set no quarantine flag, but after a browser download macOS quarantines every extracted file: clear it with xattr -dr com.apple.quarantine /opt/sentinel-hook. On Windows, run Unblock-File on each extracted file after a browser download.

For a platform without a release build, publish one yourself: dotnet publish src/AI.Sentinel.ClaudeCode.Cli -c Release -r <rid> -p:PublishAot=true. This needs the native toolchain: the Visual Studio "Desktop development with C++" workload on Windows, clang and the libc dev packages on Linux, Xcode CLT on macOS.

What gets blocked, in practice​

With SENTINEL_HOOK_ON_HIGH=Block and a typical Claude Code workflow:

  • Prompt injection in user messages (SEC-01) — blocked. The user said "ignore previous instructions"; the hook prevents Claude Code from acting on it.
  • Credential leaks and PII in tool results — redacted. A tool returned a file containing an API key or an email address; PostToolUse replaces each value with a placeholder before Claude Code processes the result. With SENTINEL_HOOK_REDACT=off they follow their severity: an API key blocks and the output is withheld.
  • PII and credentials in prompts and tool arguments — depends on severity, because these cannot be redacted. SSN/credit card → Block; phone numbers → Warn (default OnMedium=Warn).
  • Borderline jailbreaks (SEC-05) — depends on cosine similarity. Exact-phrase matches block; loose paraphrases warn.
  • Repetitive or long tool output — not flagged. The default security preset leaves out OPS-02 and OPS-11; with SENTINEL_HOOK_DETECTORS=all, repeated lines warn and output much larger than the tool call is withheld.

Tune via env vars to match your risk appetite.

Programmatic use​

The underlying library AI.Sentinel.ClaudeCode exposes HookAdapter and the vendor-agnostic HookPipelineRunner as public types. Reference the library package (not the .Cli tool package) to write your own host integration in C# — useful when you want to bundle AI.Sentinel inside a different agent framework or custom CLI.

Next: GitHub Copilot — same shape, different config file​