security-hooks
Defense-in-depth security hooks for AI coding agents. Works with Claude Code, Gemini CLI, and Codex.
A single daemon evaluates every tool call against configurable rules — blocking destructive commands, catching data exfiltration, and flagging suspicious behavior before it reaches your system.
How it works
AI Agent ──► Rust Shim ──► Unix Socket ──► Elixir Daemon ──► Verdict
(<1ms) (<1ms) (rule engine) allow/deny/ask
- The AI tool calls a hook before each tool use (bash command, file edit, MCP call)
- A tiny Rust binary forwards the payload to a long-running Elixir daemon
- The daemon evaluates rules using two layers:
- Regex — fast pattern matching for obvious threats (fork bombs, miners)
- AST — structural analysis via tree-sitter-bash that catches evasion (
rm -rf $(echo /), piped exfiltration, obfuscated commands)
- Returns
allow,deny(with a nudge message), orask(falls through to human approval)
The system is fail-closed — if the daemon is unreachable, the tool call is blocked.
Threat coverage
- Prompt injection — malicious content in READMEs, web pages, or MCP responses can't trick the agent into running blocked commands
- Destructive operations —
rm -rf, force push,sudo, cloud resource deletion, and more - Data exfiltration — detects secrets being piped to
curl/nc, reads of~/.sshor~/.aws, env var leaks - Supply chain attacks — flags dependency mutations, unknown executables, lockfile edits
- MCP injection — validates MCP server identity, scans parameters for shell injection via AST
Rule DSL
Rules live in .rules files with a custom syntax designed for regex without escaping pain:
# Regex — pattern is literal to end of line, no quoting needed
block "fork-bomb"
match :\(\)\s*\{.*\|.*&\s*\}\s*;
nudge "Fork bomb detected"
# AST — structural matching that catches evasion
block "destructive-rm"
match command("rm") with_flags("-r", "-rf", "-fr")
nudge "Use trash-cli or move to a temp directory"
block "pipe-to-exfil"
match pipeline_to("curl", "wget", "nc")
nudge "Don't pipe output to network commands"
# Config-referenced allowlist
suspicious "unknown-executable"
match_base_command_not_in allowed_executables
nudge "Unknown command '{base_command}'. Add it to config.toml"
The rule loader auto-detects regex vs AST based on whether the match starts with a function like command(.
Two tiers
- block — hard deny. The agent sees the nudge and self-corrects.
- suspicious — falls through to the human permission prompt with context.
Configuration
# config.toml — defaults ship with the project
[executables]
allowed = ["git", "mix", "cargo", "go", "node", "npm", "python", "rg", "fd", ...]
[secrets]
env_vars = ["AWS_SECRET_ACCESS_KEY", "GITHUB_TOKEN", "DATABASE_URL", ...]
[paths]
sensitive = ["~/.ssh", "~/.aws/credentials", "~/.config/gcloud", ...]
Override with config.local.toml (gitignored):
[executables]
append = ["my-custom-tool", "deno"]
exclude = ["curl"]
[rules]
disabled = ["force-push"]
Architecture
- Rust shim — all three AI tools invoke hooks by spawning a process, piping JSON to stdin, and reading JSON from stdout. That process has to exist, but the rule engine lives in a long-running Elixir daemon for hot-reload and sub-millisecond evaluation. The shim bridges the two: a ~1MB static Rust binary that connects to the daemon's Unix socket and relays the verdict. Rust because it starts in <1ms — bash has quoting bugs and needs
socat, Elixir escript pays ~300ms BEAM boot per call, and a second Burrito binary would unpack on every cold invocation. - Elixir daemon — distributed as a Burrito binary (no Erlang/Elixir install needed)
- Adapter layer — normalizes payloads across Claude Code, Gemini CLI, and Codex
- tree-sitter-bash — Rust NIF for robust AST parsing of shell commands
- Hot-reload — edit rules or config, changes apply on the next tool call
- systemd/launchd — socket activation for zero cold-start latency, automatic crash recovery
Platforms
macOS (aarch64, x86_64) · Linux (x86_64, aarch64) · WSL
Status
Design phase. See docs/specs/2026-03-26-security-hooks-design.md for the full spec.
License
TBD