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:
@@ -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
|
## Threat Model
|
||||||
|
|
||||||
@@ -11,18 +11,26 @@ A general-purpose, distributable set of Claude Code hooks that catch prompt inje
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Three components:
|
Four components:
|
||||||
|
|
||||||
### 1. Shell shim (`security-hook`)
|
### 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
|
- Reads the JSON hook payload from stdin
|
||||||
- Sends it to the daemon over a Unix socket
|
- Passes the payload and adapter name to the daemon over a Unix socket
|
||||||
- Prints the daemon's JSON response to stdout
|
- 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).
|
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.
|
**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:
|
**Timeouts:** Two configurable values:
|
||||||
@@ -49,7 +57,85 @@ Components:
|
|||||||
- **Config manager** — loads `config.toml`, merges `config.local.toml` overrides (see Configuration)
|
- **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`)
|
- **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:
|
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.
|
- **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
|
│ │ │ ├── socket_listener.ex
|
||||||
│ │ │ ├── rule_engine.ex
|
│ │ │ ├── rule_engine.ex
|
||||||
│ │ │ ├── rule_loader.ex
|
│ │ │ ├── rule_loader.ex
|
||||||
│ │ │ ├── bash_analyzer.ex # AST parsing via bash Hex package
|
│ │ │ ├── bash_analyzer.ex # AST parsing
|
||||||
│ │ │ ├── file_watcher.ex
|
│ │ │ ├── file_watcher.ex
|
||||||
│ │ │ ├── config.ex
|
│ │ │ ├── config.ex
|
||||||
│ │ │ ├── logger.ex
|
│ │ │ ├── logger.ex
|
||||||
|
│ │ │ ├── adapters/ # tool-specific adapters
|
||||||
|
│ │ │ │ ├── claude.ex
|
||||||
|
│ │ │ │ ├── gemini.ex
|
||||||
|
│ │ │ │ └── codex.ex
|
||||||
│ │ │ └── validators/
|
│ │ │ └── validators/
|
||||||
│ │ │ ├── unknown_executable.ex
|
│ │ │ ├── unknown_executable.ex
|
||||||
│ │ │ ├── dependency_mutation.ex
|
│ │ │ ├── dependency_mutation.ex
|
||||||
@@ -96,34 +186,80 @@ security-hooks/
|
|||||||
└── README.md
|
└── 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`):
|
```json
|
||||||
```
|
{
|
||||||
security-hook pre bash
|
"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`):
|
### Gemini CLI (`~/.gemini/settings.json`)
|
||||||
```
|
|
||||||
security-hook pre edit
|
```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__.*`):
|
### Codex (`~/.codex/hooks.json`)
|
||||||
```
|
|
||||||
security-hook pre mcp
|
```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
|
### 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
|
### 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:
|
PreToolUse allow:
|
||||||
```json
|
```json
|
||||||
@@ -704,15 +840,19 @@ The install script:
|
|||||||
2. Installs the binary and shell shim to `~/.local/bin/` (or user-specified location)
|
2. Installs the binary and shell shim to `~/.local/bin/` (or user-specified location)
|
||||||
3. Copies default rules and config to `~/.config/security-hooks/`
|
3. Copies default rules and config to `~/.config/security-hooks/`
|
||||||
4. Creates `config.local.toml` from a template if it does not exist
|
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:
|
6. Detects the platform and installs the appropriate service manager integration:
|
||||||
- Linux/WSL with systemd: installs user units, enables socket activation
|
- Linux/WSL with systemd: installs user units, enables socket activation
|
||||||
- macOS: installs launchd plist, loads agent
|
- macOS: installs launchd plist, loads agent
|
||||||
- Fallback: prints instructions noting the shim will manage the daemon directly
|
- 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)
|
7. Auto-detects installed AI coding tools and registers hooks for each:
|
||||||
8. Prints a summary of what was configured
|
- 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.
|
**Uninstall:** `./install.sh --uninstall` removes hook entries from settings and optionally removes the config directory and binary.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user