Replace bash shim with Rust binary

Bash shim had shell quoting risks and depended on socat/nc. Elixir
escript would pay ~300ms BEAM boot per invocation. A second Burrito
binary would unpack on every cold call. Rust gives <1ms startup,
proper timeout handling, and is already in the toolchain for the
tree-sitter NIF.

- Add shim/ Rust crate to directory structure
- Document Rust shim rationale (vs bash, escript, Burrito)
- Update Dependencies with shim crate deps (serde_json, stdlib Unix socket)
- Update install script, README, architecture diagram

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Flo
2026-03-30 12:28:18 +02:00
parent a133271a6c
commit 7645402347
2 changed files with 126 additions and 9 deletions

View File

@@ -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