Add multi-tool support: Claude Code, Gemini CLI, Codex

- Add adapter layer with normalize_input/format_output per tool
- Define common internal payload and verdict formats
- Map event names across tools (PreToolUse/BeforeTool)
- Map payload fields across tools (tool_name, tool_input, cwd)
- Adapter-specific response formatting:
  - Claude: hookSpecificOutput.permissionDecision
  - Gemini: flat decision field
  - Codex: exit code 2 + stderr for deny
- Shell shim takes --adapter flag to select tool
- install.sh auto-detects all installed tools and registers hooks
- Hook registration examples for all three tools
- Add adapters/ directory to daemon source tree

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Flo
2026-03-30 10:11:08 +02:00
parent 4c2226ae57
commit 8be22dac33

View File

@@ -1,6 +1,6 @@
# Security Hooks for Claude Code
# Security Hooks for AI Coding Agents
A general-purpose, distributable set of Claude Code hooks that catch prompt injection, agent autonomy drift, supply chain attacks, and data exfiltration. Ships as a single binary (Elixir daemon via Burrito) with a shell shim, a custom rule DSL, and layered command analysis (regex + AST parsing).
A general-purpose, distributable set of security hooks for AI coding agents (Claude Code, Gemini CLI, Codex) that catch prompt injection, agent autonomy drift, supply chain attacks, and data exfiltration. Ships as a single binary (Elixir daemon via Burrito) with a shell shim, adapter layer for multi-tool support, a custom rule DSL, and layered command analysis (regex + AST parsing).
## Threat Model
@@ -11,18 +11,26 @@ A general-purpose, distributable set of Claude Code hooks that catch prompt inje
## Architecture
Three components:
Four components:
### 1. Shell shim (`security-hook`)
A short bash script that Claude Code invokes as a hook command. It:
A short bash script that AI coding tools invoke as a hook command. It:
- Accepts an `--adapter` flag to specify the calling tool (`claude`, `gemini`, `codex`)
- Reads the JSON hook payload from stdin
- Sends it to the daemon over a Unix socket
- Prints the daemon's JSON response to stdout
- 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)
The shim is deliberately simple — it does not manage daemon lifecycle. That responsibility belongs to the platform service manager (see Daemon Lifecycle below).
Usage:
```
security-hook --adapter claude pre bash # Claude Code
security-hook --adapter gemini pre bash # Gemini CLI
security-hook --adapter codex pre bash # Codex
```
**Fail-closed policy:** If the shim cannot reach the daemon within its timeout, it exits with code 2 (blocking error) and writes a deny reason to stderr. The system never fails open.
**Timeouts:** Two configurable values:
@@ -49,7 +57,85 @@ Components:
- **Config manager** — loads `config.toml`, merges `config.local.toml` overrides (see Configuration)
- **Logger** — writes JSONL to `$XDG_STATE_HOME/security-hooks/hook.log` (macOS: `~/Library/Logs/security-hooks/hook.log`)
### 3. Rule files
### 3. Adapter layer
The adapter layer lives inside the daemon and handles the differences between Claude Code, Gemini CLI, and Codex. Each adapter module implements two functions:
1. **`normalize_input/1`** — transforms the tool-specific JSON payload into a common internal format
2. **`format_output/2`** — transforms the daemon's internal verdict into the tool-specific response format
#### Common internal payload
All adapters normalize to this structure:
```json
{
"adapter": "claude",
"event": "pre_tool_use",
"tool": "bash",
"input": {"command": "rm -rf /"},
"cwd": "/project",
"session_id": "abc123"
}
```
#### Common internal verdict
The rule engine returns:
```json
{
"decision": "deny",
"rule": "destructive-rm",
"match_type": "ast",
"reason": "destructive rm detected",
"nudge": "Use trash-cli or move to a temp directory"
}
```
#### Adapter output differences
**Claude Code adapter** — wraps verdict in `hookSpecificOutput`:
```json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "destructive rm detected",
"additionalContext": "Use trash-cli or move to a temp directory"
}
}
```
**Gemini CLI adapter** — uses flat `decision` field:
```json
{
"decision": "deny",
"reason": "destructive rm detected",
"message": "Use trash-cli or move to a temp directory"
}
```
**Codex adapter** — uses exit code 2 for deny, writes reason to stderr, no stdout JSON needed. For allow, exits 0 with empty stdout.
#### Event name mapping
| Internal event | Claude Code | Gemini CLI | Codex |
|---------------|------------|------------|-------|
| `pre_tool_use` | `PreToolUse` | `BeforeTool` | `PreToolUse` |
| `post_tool_use` | `PostToolUse` | `AfterTool` | `PostToolUse` |
| `session_start` | `SessionStart` | `SessionStart` | `SessionStart` |
#### Payload field mapping
| Internal field | Claude Code | Gemini CLI | Codex |
|---------------|------------|------------|-------|
| `tool` | `tool_name` | `tool_name` | `tool_name` |
| `input` | `tool_input` | `tool_input` | `tool_input` |
| `cwd` | `cwd` | `cwd` (or `$GEMINI_CWD`) | `cwd` |
| `session_id` | `session_id` | `session_id` (or `$GEMINI_SESSION_ID`) | `session_id` |
### 4. Rule files
Two kinds:
- **Pattern rules** in `.rules` files using a custom DSL (see Rule Format below). Rules can use regex patterns for simple matching or AST functions for structural analysis.
@@ -81,10 +167,14 @@ security-hooks/
│ │ │ ├── socket_listener.ex
│ │ │ ├── rule_engine.ex
│ │ │ ├── rule_loader.ex
│ │ │ ├── bash_analyzer.ex # AST parsing via bash Hex package
│ │ │ ├── bash_analyzer.ex # AST parsing
│ │ │ ├── file_watcher.ex
│ │ │ ├── config.ex
│ │ │ ├── logger.ex
│ │ │ ├── adapters/ # tool-specific adapters
│ │ │ │ ├── claude.ex
│ │ │ │ ├── gemini.ex
│ │ │ │ └── codex.ex
│ │ │ └── validators/
│ │ │ ├── unknown_executable.ex
│ │ │ ├── dependency_mutation.ex
@@ -96,34 +186,80 @@ security-hooks/
└── README.md
```
## Hook Events & Claude Code Integration
## Hook Registration
Hooks are registered via `install.sh` into Claude Code's `settings.json`:
`install.sh` auto-detects which AI coding tools are installed and registers hooks for each.
### PreToolUse hooks (can allow/deny/ask)
### Claude Code (`~/.claude/settings.json`)
**Bash** (matcher: `Bash`):
```
security-hook pre bash
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "security-hook --adapter claude pre bash"}]
},
{
"matcher": "Edit|Write",
"hooks": [{"type": "command", "command": "security-hook --adapter claude pre edit"}]
},
{
"matcher": "mcp__.*",
"hooks": [{"type": "command", "command": "security-hook --adapter claude pre mcp"}]
}
]
}
}
```
**Edit/Write** (matcher: `Edit|Write`):
```
security-hook pre edit
### Gemini CLI (`~/.gemini/settings.json`)
```json
{
"hooks": {
"BeforeTool": [
{
"matcher": "shell",
"hooks": [{"type": "command", "command": "security-hook --adapter gemini pre bash"}]
},
{
"matcher": "edit|write",
"hooks": [{"type": "command", "command": "security-hook --adapter gemini pre edit"}]
},
{
"matcher": "mcp_.*",
"hooks": [{"type": "command", "command": "security-hook --adapter gemini pre mcp"}]
}
]
}
}
```
**MCP** (matcher: `mcp__.*`):
```
security-hook pre mcp
### Codex (`~/.codex/hooks.json`)
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{"type": "command", "command": "security-hook --adapter codex pre bash"}]
}
]
}
}
```
Note: Codex currently only supports Bash hooks in PreToolUse/PostToolUse. Edit and MCP hooks will be added when Codex expands its hook events.
### PostToolUse hook
Deferred to a future version. Post-tool-use linting is project-specific and requires detecting the project's toolchain, choosing the right linter, handling timeouts, and distinguishing agent-introduced errors from pre-existing ones. This deserves its own design pass.
Deferred to a future version. Post-tool-use linting is project-specific and requires its own design pass.
### Response format
The daemon returns JSON matching Claude Code's hook output spec.
The adapter layer translates the daemon's internal verdict into tool-specific responses. The examples below show Claude Code format; see the Adapter Layer section for Gemini and Codex formats.
PreToolUse allow:
```json
@@ -704,15 +840,19 @@ The install script:
2. Installs the binary and shell 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 config and pre-populates the MCP allowlist in `config.local.toml`
5. Auto-detects installed MCP servers from Claude Code/Gemini CLI config and pre-populates the MCP allowlist in `config.local.toml`
6. Detects the platform and installs the appropriate service manager integration:
- Linux/WSL with systemd: installs user units, enables socket activation
- macOS: installs launchd plist, loads agent
- Fallback: prints instructions noting the shim will manage the daemon directly
7. Merges hook entries into Claude Code's `~/.claude/settings.json` (preserving existing hooks)
8. Prints a summary of what was configured
7. Auto-detects installed AI coding tools and registers hooks for each:
- Claude Code: merges into `~/.claude/settings.json`
- Gemini CLI: merges into `~/.gemini/settings.json`
- Codex: merges into `~/.codex/hooks.json`
- Preserves existing hooks in all tools
8. Prints a summary: which tools were detected, how many rules loaded, which adapter(s) registered
**Existing hooks:** Claude Code supports multiple hooks per event. `install.sh` appends security-hooks entries without removing existing user hooks. Both run on each tool call.
**Existing hooks:** All three tools support multiple hooks per event. `install.sh` appends security-hooks entries without removing existing user hooks.
**Uninstall:** `./install.sh --uninstall` removes hook entries from settings and optionally removes the config directory and binary.