diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f21125 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# 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 +``` + +1. The AI tool calls a hook before each tool use (bash command, file edit, MCP call) +2. A tiny Rust binary forwards the payload to a long-running Elixir daemon +3. 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) +4. Returns `allow`, `deny` (with a nudge message), or `ask` (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 `~/.ssh` or `~/.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 + +```toml +# 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): + +```toml +[executables] +append = ["my-custom-tool", "deno"] +exclude = ["curl"] + +[rules] +disabled = ["force-push"] +``` + +## Architecture + +- **Rust shim** — ~1MB static binary, <1ms startup, forwards payloads via Unix socket +- **Elixir daemon** — distributed as a [Burrito](https://github.com/burrito-elixir/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/superpowers/specs/2026-03-26-security-hooks-design.md`](docs/superpowers/specs/2026-03-26-security-hooks-design.md) for the full spec. + +## License + +TBD diff --git a/docs/superpowers/specs/2026-03-26-security-hooks-design.md b/docs/superpowers/specs/2026-03-26-security-hooks-design.md index 1757f34..2807f69 100644 --- a/docs/superpowers/specs/2026-03-26-security-hooks-design.md +++ b/docs/superpowers/specs/2026-03-26-security-hooks-design.md @@ -13,16 +13,19 @@ A general-purpose, distributable set of security hooks for AI coding agents (Cla Four components: -### 1. Shell shim (`security-hook`) +### 1. Shim (`security-hook`) -A short bash script that AI coding tools invoke as a hook command. It: +A small Rust binary (~1MB static, <1ms startup) that AI coding tools invoke as a hook command. Rust is chosen over bash to avoid shell quoting bugs, over Elixir escript to avoid ~300ms BEAM boot, and over a second Burrito binary to avoid unpack-on-every-invocation overhead. Since tree-sitter-bash already requires Rust in the build toolchain, this adds no new dependencies. + +The shim: - Accepts an `--adapter` flag to specify the calling tool (`claude`, `gemini`, `codex`) - Reads the JSON hook payload from stdin -- Passes the payload and adapter name to the daemon over a Unix socket -- Prints the daemon's response to stdout (formatted for the calling tool) +- Connects to the daemon's Unix socket and sends the payload with the adapter name +- Reads the daemon's response and prints it to stdout +- Handles timeouts and fail-closed behavior natively (no shell `timeout` command) -The shim is deliberately simple — it does not manage daemon lifecycle. That responsibility belongs to the platform service manager (see Daemon Lifecycle below). +It does not manage daemon lifecycle. That responsibility belongs to the platform service manager (see Daemon Lifecycle below). Usage: ``` @@ -147,8 +150,10 @@ When installed, the entire tree below is copied to `$SECURITY_HOOKS_HOME` (defau ``` security-hooks/ -├── bin/ -│ └── security-hook # shell shim +├── shim/ # Rust shim binary +│ ├── Cargo.toml +│ └── src/ +│ └── main.rs ├── service/ │ ├── security-hookd.service # systemd user service unit │ ├── security-hookd.socket # systemd socket activation unit @@ -838,7 +843,7 @@ Future: streaming connectors for centralized logging (stdout, webhook, syslog). The install script: 1. Downloads the Burrito binary for the current platform (macOS aarch64/x86_64, Linux x86_64/aarch64) or builds from source if Elixir is available -2. Installs the binary and shell shim to `~/.local/bin/` (or user-specified location) +2. Installs both binaries (`security-hookd` daemon + `security-hook` shim) to `~/.local/bin/` (or user-specified location) 3. Copies default rules and config to `~/.config/security-hooks/` 4. Creates `config.local.toml` from a template if it does not exist 5. Auto-detects installed MCP servers from Claude Code/Gemini CLI config and pre-populates the MCP allowlist in `config.local.toml` @@ -981,5 +986,10 @@ Elixir/Hex packages required by the daemon: - `file_system` — cross-platform file watcher for hot-reload - `burrito` — compile to single-binary for distribution -Rust NIF (compiled into the Burrito binary): +Rust (compiled into the Burrito binary as a NIF, and used standalone for the shim): - `tree-sitter` + `tree-sitter-bash` — bash AST parser for structural command analysis (primary parser, see Bash Parser Strategy) + +Shim binary (`security-hook`, Rust): +- `std::os::unix::net::UnixStream` — Unix socket client (stdlib, no external deps) +- `serde_json` — JSON parsing +- Cross-compiled for the same platform targets as the daemon