A Claude Code PreToolUse hook that checks every tool call against a YAML rule file before it runs — and blocks the ones you've denied.
Claude Code lets you register hooks that run before each tool call. sentnel installs one Python script (hook.py) as a PreToolUse hook on all tools. Before Claude executes a Bash command, file read, or file write, the hook receives the tool call as JSON on stdin, matches it against a set of hardcoded static rules plus your editable rules.yaml, and either lets it through or exits with code 2 — which makes Claude Code block the call and show Claude the rule that fired. Every decision (allow and deny) is appended to a local SQLite database, so you get an audit trail of what your agent tried to do. No daemon, no network calls, no dependencies beyond Python 3 and PyYAML.
Claude Code
│ tool call (JSON on stdin)
▼
sentnel hook.py ── static rules, then rules.yaml (first match wins)
│
├── allow (exit 0) ──▶ tool executes
└── deny (exit 2) ──▶ call blocked, Claude sees:
"Sentnel blocked this action — rule: <id>"
One-liner (clones to ~/.sentnel and registers the hook):
curl -sSL https://raw.githubusercontent.com/sentnelops/sentnel/main/setup.sh | bashOr from a local clone ("sidecar mode" — the hook runs from wherever you cloned it):
git clone https://github.com/sentnelops/sentnel.git
cd sentnel
bash setup.shsetup.sh does exactly this: installs pyyaml via pip3, creates audit.db (git-ignored), registers python3 <repo>/hook.py under hooks.PreToolUse in ~/.claude/settings.json (idempotent — running it twice is safe), then runs a smoke test that pipes a fake rm -rf through the hook and verifies it exits 2.
A rule looks like this (from the shipped rules.yaml):
rules:
- id: block_rm
match:
tool: Bash
patterns_any:
- "rm "
- "shutil.rmtree"
- "git clean"
action: deny
- id: block_network_exfil
match:
tool: Bash
patterns_any:
- "curl "
- "wget "
- "requests.get"
action: denyWhen a rule fires, the hook prints a block decision on stdout and exits 2:
{"decision": "block", "reason": "Sentnel blocked this action — rule: block_rm"}Claude Code hard-blocks the tool call and feeds the reason back to Claude. You can test it directly without Claude Code:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /test"}}' | python3 hook.py
# → {"decision": "block", "reason": "Sentnel blocked this action — rule: static_rm"}; exit code 2rules.yaml is re-read on every tool call, so edits apply immediately — no restart.
Hook event. One event: PreToolUse, registered with an empty matcher (""), so it runs for every tool. There is no PostToolUse component.
Decision logic (hook.py):
- Parse the tool-call JSON from stdin. If parsing fails, block (exit 2).
- Evaluate ~11 static rules hardcoded in
hook.py(deletion viarm/shutil/os.remove/find -delete/git clean, reads of.env/id_rsa/.aws/credentials-style paths,curl/wgetand Python network calls, reverse-shell indicators,DROP TABLE, and writes to.mcp.json/.claude/settings.json). These run first and can't be changed by editing YAML. - Then evaluate
rules.yamltop to bottom. First matching rule wins. - No match → allow (exit 0).
Matching is case-insensitive substring matching: patterns_any entries are tested against the command field of the tool input, path entries against the path field. No regex, no globs.
Logging. Every call — allowed or denied — is written to audit.db (SQLite, next to hook.py) in a single events table: ts (unix float), tool, decision, reason (rule id, or NULL on allow), and raw (the full tool-call JSON). Query it with:
sqlite3 audit.db "SELECT ts, tool, decision, reason FROM events ORDER BY ts DESC LIMIT 20;"The CLAUDE.md in this repo is an intent-level prompt (hard rules against deletion, secret reads, and network calls). Claude Code auto-loads it only when you're working inside this repo's directory — the installer does not register it globally.
- This is a defense layer, not a security boundary. The hook runs in-process on your machine against string patterns. A determined or clever model can compose a command that no substring matches (base64, variable expansion, writing a script then running it). Treat it as a seatbelt, not a sandbox.
- Substring matching means false positives too — the pattern
"rm "blocks any command containing it, e.g.npm rm some-packageor agrepfor the literal string. - If
rules.yamlis malformed, the hook logs the error to stderr and continues with static rules only — your custom rules silently stop applying until the YAML is fixed. - Only a stdin parse failure fails closed. An unexpected crash elsewhere in
hook.pyexits non-zero-but-not-2, which Claude Code does not treat as a block. - File rules match the
file_pathkey Claude Code sends forRead/Write(legacypathis accepted too).
Fields actually parsed by hook.py:
| Field | Type | Behavior |
|---|---|---|
id |
string | Rule name; reported in the block reason and logged to audit.db |
match.tool |
string | Exact tool name (Bash, Read, Write, …). If set and it doesn't match, the rule is skipped |
match.patterns_any |
list of strings | Case-insensitive substrings tested against tool_input.command; any hit fires the rule |
match.pattern |
string | Single case-insensitive substring tested against tool_input.command |
match.path |
list of strings | Case-insensitive substrings tested against tool_input.path; any hit fires the rule |
action |
string | deny blocks the call (exit 2). Any other value allows it — and since first match wins, a matching non-deny rule short-circuits later rules |
Evaluation order: static rules in hook.py, then rules.yaml rules in file order. First match decides.
bash uninstall.shThis removes the sentnel entry from hooks.PreToolUse in ~/.claude/settings.json (matching this repo's hook.py path) and leaves everything else in your settings untouched. Restart Claude Code to take effect. Your audit.db and the cloned repo stay on disk; delete them yourself if you want them gone.
SentnelOps is an AI agent runtime governance platform. It acts as an MCP firewall between AI agents (Claude Code, Cursor, custom agents) and the systems they access, giving every agent an identity, enforcing YAML policies on every tool call before it executes, and producing an audit-ready evidence trail. sentnel is the free, local, in-process complement for Claude Code specifically. More: sentnelops.com · Securing Claude Code · AI agent runtime governance
MIT — see LICENSE.