Address spec review iteration 4: config keys, match targets, launchd
Important fixes: - Document config key resolution (allowed_executables -> executables.allowed, mcp_allowed_servers -> names from [[mcp.servers]]) - Clarify CLAUDE_PROJECT_DIR source (derived from payload cwd field) - MCP rules: regex match_any operates on serialized tool_input, not tool_name - Add with_args_matching semantics (joined argument string) Suggestions also addressed: - Fix launchd plist: use absolute paths, note install.sh expands placeholders - Fix launchd socket path: use $TMPDIR for per-user isolation - Rename SECURITY_HOOKS_CONFIG -> SECURITY_HOOKS_HOME (contains both rules/ and config/ subdirectories) - Document directory discovery via single env var Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,8 @@ Two kinds:
|
|||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
|
When installed, the entire tree below is copied to `$SECURITY_HOOKS_HOME` (default: `~/.config/security-hooks/`). The daemon discovers both `rules/` and `config/` as subdirectories of `$SECURITY_HOOKS_HOME`. This single env var controls all path resolution.
|
||||||
|
|
||||||
```
|
```
|
||||||
security-hooks/
|
security-hooks/
|
||||||
├── bin/
|
├── bin/
|
||||||
@@ -300,6 +302,18 @@ regex_pattern := <any text not starting with IDENT '('> <to end of line>
|
|||||||
|
|
||||||
**Note:** `only_when` / `except_when` conditions are deferred to a future version.
|
**Note:** `only_when` / `except_when` conditions are deferred to a future version.
|
||||||
|
|
||||||
|
### Config key resolution
|
||||||
|
|
||||||
|
Config keys in `match_*_not_in` clauses map to TOML paths as follows:
|
||||||
|
- `match_base_command_not_in allowed_executables` → reads `executables.allowed` (flat list of strings)
|
||||||
|
- `match_server_not_in mcp_allowed_servers` → extracts the `name` field from all `[[mcp.servers]]` entries
|
||||||
|
|
||||||
|
This mapping is hardcoded in the rule engine. Custom config keys are not supported in v1.
|
||||||
|
|
||||||
|
### `with_args_matching` semantics
|
||||||
|
|
||||||
|
The `with_args_matching(regex)` modifier matches the regex against the joined argument string (space-separated) of the matched command node, excluding the command name and flags. For example, for `git push --force origin main`, the argument string is `push --force origin main`.
|
||||||
|
|
||||||
### Tiers
|
### Tiers
|
||||||
|
|
||||||
- **block** — hard deny via `permissionDecision: "deny"`. The nudge is sent as `additionalContext` so the agent can self-correct.
|
- **block** — hard deny via `permissionDecision: "deny"`. The nudge is sent as `additionalContext` so the agent can self-correct.
|
||||||
@@ -309,11 +323,11 @@ regex_pattern := <any text not starting with IDENT '('> <to end of line>
|
|||||||
|
|
||||||
Each hook type matches against a specific field from the Claude Code JSON payload:
|
Each hook type matches against a specific field from the Claude Code JSON payload:
|
||||||
|
|
||||||
| Hook file | Payload field matched | Example value |
|
| Hook file | Default match target | Notes |
|
||||||
|-----------|----------------------|---------------|
|
|-----------|---------------------|-------|
|
||||||
| bash.rules | `tool_input.command` | `rm -rf /tmp/foo` |
|
| bash.rules | `tool_input.command` | Regex matches raw string; AST functions match parsed tree |
|
||||||
| edit.rules | `tool_input.file_path` | `/home/user/project/src/main.rs` |
|
| edit.rules | `tool_input.file_path` | Regex matches the file path string |
|
||||||
| mcp.rules | `tool_name` (parsed into server + tool) | `mcp__context7__query-docs` |
|
| mcp.rules | `tool_name` for server/tool identification; serialized `tool_input` (JSON string) for parameter inspection | `match_server_not_in` extracts server name from `tool_name`; regex `match`/`match_any` matches against JSON-serialized `tool_input` to detect injection patterns |
|
||||||
|
|
||||||
For bash rules, `{base_command}` is extracted as the first whitespace-delimited token of `tool_input.command` after stripping leading environment variable assignments. For AST-matched rules, it is extracted from the parsed command node.
|
For bash rules, `{base_command}` is extracted as the first whitespace-delimited token of `tool_input.command` after stripping leading environment variable assignments. For AST-matched rules, it is extracted from the parsed command node.
|
||||||
|
|
||||||
@@ -441,7 +455,7 @@ suspicious "edit-dependency-manifest"
|
|||||||
nudge "Dependency fields changed in {file_path} — use the package manager CLI"
|
nudge "Dependency fields changed in {file_path} — use the package manager CLI"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: `CLAUDE_PROJECT_DIR` and `HOME` in patterns are expanded to their actual values at rule load time.
|
Note: `CLAUDE_PROJECT_DIR` and `HOME` in patterns are expanded to their actual values. `CLAUDE_PROJECT_DIR` is derived from the `cwd` field of each hook payload (the working directory of the Claude Code session). `HOME` is taken from the process environment. If `cwd` is unavailable, the `edit-outside-project` rule is skipped (not fail-closed, since blocking all edits would be overly disruptive).
|
||||||
|
|
||||||
### mcp.rules
|
### mcp.rules
|
||||||
|
|
||||||
@@ -594,7 +608,7 @@ Type=simple
|
|||||||
ExecStart=%h/.local/bin/security-hookd
|
ExecStart=%h/.local/bin/security-hookd
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=1
|
RestartSec=1
|
||||||
Environment=SECURITY_HOOKS_CONFIG=%h/.config/security-hooks
|
Environment=SECURITY_HOOKS_HOME=%h/.config/security-hooks
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
@@ -617,14 +631,14 @@ After install: `systemctl --user enable --now security-hookd.socket`
|
|||||||
<string>com.security-hooks.daemon</string>
|
<string>com.security-hooks.daemon</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>~/.local/bin/security-hookd</string>
|
<string>/Users/USERNAME/.local/bin/security-hookd</string>
|
||||||
</array>
|
</array>
|
||||||
<key>Sockets</key>
|
<key>Sockets</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Listeners</key>
|
<key>Listeners</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>SockPathName</key>
|
<key>SockPathName</key>
|
||||||
<string>/tmp/security-hooks/sock</string>
|
<string>TMPDIR/security-hooks/sock</string>
|
||||||
<key>SockPathMode</key>
|
<key>SockPathMode</key>
|
||||||
<integer>384</integer> <!-- 0600 -->
|
<integer>384</integer> <!-- 0600 -->
|
||||||
</dict>
|
</dict>
|
||||||
@@ -636,13 +650,15 @@ After install: `systemctl --user enable --now security-hookd.socket`
|
|||||||
</dict>
|
</dict>
|
||||||
<key>EnvironmentVariables</key>
|
<key>EnvironmentVariables</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>SECURITY_HOOKS_CONFIG</key>
|
<key>SECURITY_HOOKS_HOME</key>
|
||||||
<string>~/.config/security-hooks</string>
|
<string>/Users/USERNAME/.config/security-hooks</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: `USERNAME` and `TMPDIR` placeholders are expanded by `install.sh` at install time. launchd does not expand tilde or environment variables in plist values.
|
||||||
|
|
||||||
After install: `launchctl load ~/Library/LaunchAgents/com.security-hooks.daemon.plist`
|
After install: `launchctl load ~/Library/LaunchAgents/com.security-hooks.daemon.plist`
|
||||||
|
|
||||||
### Fallback: shim-managed daemon
|
### Fallback: shim-managed daemon
|
||||||
|
|||||||
Reference in New Issue
Block a user